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} /> 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); });