diff --git a/CHANGELOG.md b/CHANGELOG.md
index fabc7adf..81f5ff8f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,7 @@
#### Accessibility
+* Make the underlying checkbox accessible to screen readers and touch (#276)
* Hide the pseudo-checkbox from the accessibility tree
* Change the clickable label from `role="link"` to `role="button"`
diff --git a/src/js/components/TreeNode.jsx b/src/js/components/TreeNode.jsx
index 5b191241..c3a24fdf 100644
--- a/src/js/components/TreeNode.jsx
+++ b/src/js/components/TreeNode.jsx
@@ -53,7 +53,6 @@ class TreeNode extends React.PureComponent {
super(props);
this.onCheck = this.onCheck.bind(this);
- this.onCheckboxKeyPress = this.onCheckboxKeyPress.bind(this);
this.onCheckboxKeyUp = this.onCheckboxKeyUp.bind(this);
this.onClick = this.onClick.bind(this);
this.onExpand = this.onExpand.bind(this);
@@ -68,19 +67,14 @@ class TreeNode extends React.PureComponent {
});
}
- onCheckboxKeyPress(event) {
+ onCheckboxKeyUp(event) {
const { checkKeys } = this.props;
const { key } = event;
- // Prevent browser scroll when pressing space on the checkbox
- if (key === KEYS.SPACEBAR && checkKeys.includes(key)) {
+ // Prevent default spacebar behavior from interfering with user settings
+ if (KEYS.SPACEBAR) {
event.preventDefault();
}
- }
-
- onCheckboxKeyUp(event) {
- const { checkKeys } = this.props;
- const { key } = event;
if (checkKeys.includes(key)) {
this.onCheck();
@@ -224,16 +218,12 @@ class TreeNode extends React.PureComponent {
indeterminate={checked === 2}
onChange={() => {}}
onClick={this.onCheck}
+ onKeyUp={this.onCheckboxKeyUp}
/>
{this.renderCheckboxIcon()}
diff --git a/src/scss/react-checkbox-tree.scss b/src/scss/react-checkbox-tree.scss
index 47cef535..c2ba5d54 100644
--- a/src/scss/react-checkbox-tree.scss
+++ b/src/scss/react-checkbox-tree.scss
@@ -4,6 +4,11 @@ $rct-label-hover: rgba($rct-icon-color, .1) !default;
$rct-label-active: rgba($rct-icon-color, .15) !default;
$rct-clickable-hover: rgba($rct-icon-color, .1) !default;
$rct-clickable-focus: rgba($rct-icon-color, .2) !default;
+$rct-checkbox-outline-offset: 2px !default;
+$rct-outline-color: rgba($rct-icon-color, .5) !default;
+$rct-outline-radius: 2px;
+$rct-outline-size: 2px !default;
+$rct-outline-offset: -2px !default;
// Force ASCII output until Sass supports it
// https://github.com/sass/dart-sass/issues/568
@@ -30,6 +35,18 @@ $rct-clickable-focus: rgba($rct-icon-color, .2) !default;
}
}
+ button,
+ input {
+ &:focus {
+ outline: $rct-outline-size solid $rct-outline-color;
+ border-radius: $rct-outline-radius;
+ }
+
+ &:not(:focus-visible) {
+ outline: none;
+ }
+ }
+
button {
line-height: normal;
color: inherit;
@@ -37,6 +54,10 @@ $rct-clickable-focus: rgba($rct-icon-color, .2) !default;
&:disabled {
cursor: not-allowed;
}
+
+ &:focus {
+ outline-offset: $rct-outline-offset;
+ }
}
.rct-bare-label {
@@ -44,6 +65,8 @@ $rct-clickable-focus: rgba($rct-icon-color, .2) !default;
}
label {
+ display: flex;
+ align-items: center;
margin-bottom: 0;
cursor: pointer;
@@ -57,12 +80,32 @@ $rct-clickable-focus: rgba($rct-icon-color, .2) !default;
}
}
- &:not(.rct-native-display) input {
- display: none;
+ input {
+ margin: 0 5px;
+ cursor: pointer;
+
+ &:focus {
+ outline-offset: $rct-checkbox-outline-offset;
+ }
}
- &.rct-native-display input {
- margin: 0 5px;
+ &:not(.rct-native-display) input {
+ // Hide the native checkbox (but keep in accessibility tree and accessible via touch)
+ position: absolute;
+ opacity: 0;
+
+ // Show focus effect on pseudo-checkbox
+ &:focus {
+ + .rct-checkbox {
+ outline: $rct-outline-size solid $rct-outline-color;
+ outline-offset: $rct-outline-offset;
+ border-radius: $rct-outline-radius;
+ }
+
+ &:not(:focus-visible) + .rct-checkbox {
+ outline: none;
+ }
+ }
}
.rct-icon {
diff --git a/test/CheckboxTree.jsx b/test/CheckboxTree.jsx
index ebd9fff1..241b4728 100644
--- a/test/CheckboxTree.jsx
+++ b/test/CheckboxTree.jsx
@@ -160,7 +160,7 @@ describe('', () => {
it('should trigger a check event when pressing one of the supplied values', async () => {
let actual = null;
- const { container } = render(
+ render(
', () => {
/>,
);
- await fireEvent.keyUp(container.querySelector('.rct-checkbox'), { key: 'Shift' });
+ await fireEvent.keyUp(screen.getByRole('checkbox'), { key: 'Shift' });
assert.deepEqual(actual, ['jupiter']);
});
diff --git a/test/TreeNode.jsx b/test/TreeNode.jsx
index 6e29f8a8..1daf3368 100644
--- a/test/TreeNode.jsx
+++ b/test/TreeNode.jsx
@@ -378,7 +378,7 @@ describe('', () => {
it('should trigger on key press', async () => {
let actual = {};
- const { container } = render(
+ render(
', () => {
/>,
);
- await fireEvent.keyUp(container.querySelector('.rct-checkbox'), { key: 'Enter' });
+ await fireEvent.keyUp(screen.getByRole('checkbox'), { key: 'Enter' });
assert.isTrue(actual.checked);
});