Skip to content

Commit 829fc28

Browse files
jakezateckyNeurrone
andcommitted
Make checkboxes more accessible
Make the underlying native checkboxes more accessible to screen readers and touch. Resolves #276. Addresses parts of #163. Co-Authored-By: Dickson Tan <16471962+Neurrone@users.noreply.github.com>
1 parent fc8d660 commit 829fc28

File tree

5 files changed

+57
-23
lines changed

5 files changed

+57
-23
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
#### Accessibility
88

9+
* Make the underlying checkbox accessible to screen readers and touch (#276)
910
* Hide the pseudo-checkbox from the accessibility tree
1011
* Change the clickable label from `role="link"` to `role="button"`
1112

src/js/components/TreeNode.jsx

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ class TreeNode extends React.PureComponent {
5353
super(props);
5454

5555
this.onCheck = this.onCheck.bind(this);
56-
this.onCheckboxKeyPress = this.onCheckboxKeyPress.bind(this);
5756
this.onCheckboxKeyUp = this.onCheckboxKeyUp.bind(this);
5857
this.onClick = this.onClick.bind(this);
5958
this.onExpand = this.onExpand.bind(this);
@@ -68,19 +67,14 @@ class TreeNode extends React.PureComponent {
6867
});
6968
}
7069

71-
onCheckboxKeyPress(event) {
70+
onCheckboxKeyUp(event) {
7271
const { checkKeys } = this.props;
7372
const { key } = event;
7473

75-
// Prevent browser scroll when pressing space on the checkbox
76-
if (key === KEYS.SPACEBAR && checkKeys.includes(key)) {
74+
// Prevent default spacebar behavior from interfering with user settings
75+
if (KEYS.SPACEBAR) {
7776
event.preventDefault();
7877
}
79-
}
80-
81-
onCheckboxKeyUp(event) {
82-
const { checkKeys } = this.props;
83-
const { key } = event;
8478

8579
if (checkKeys.includes(key)) {
8680
this.onCheck();
@@ -224,16 +218,12 @@ class TreeNode extends React.PureComponent {
224218
indeterminate={checked === 2}
225219
onChange={() => {}}
226220
onClick={this.onCheck}
221+
onKeyUp={this.onCheckboxKeyUp}
227222
/>
228223
<span
229-
aria-checked={checked === 1}
230-
aria-disabled={disabled}
231224
aria-hidden="true"
232225
className="rct-checkbox"
233-
role="checkbox"
234-
tabIndex={0}
235-
onKeyPress={this.onCheckboxKeyPress}
236-
onKeyUp={this.onCheckboxKeyUp}
226+
role="presentation"
237227
>
238228
{this.renderCheckboxIcon()}
239229
</span>

src/scss/react-checkbox-tree.scss

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ $rct-label-hover: rgba($rct-icon-color, .1) !default;
44
$rct-label-active: rgba($rct-icon-color, .15) !default;
55
$rct-clickable-hover: rgba($rct-icon-color, .1) !default;
66
$rct-clickable-focus: rgba($rct-icon-color, .2) !default;
7+
$rct-checkbox-outline-offset: 2px !default;
8+
$rct-outline-color: rgba($rct-icon-color, .5) !default;
9+
$rct-outline-radius: 2px;
10+
$rct-outline-size: 2px !default;
11+
$rct-outline-offset: -2px !default;
712

813
// Force ASCII output until Sass supports it
914
// https://github.com/sass/dart-sass/issues/568
@@ -30,20 +35,38 @@ $rct-clickable-focus: rgba($rct-icon-color, .2) !default;
3035
}
3136
}
3237

38+
button,
39+
input {
40+
&:focus {
41+
outline: $rct-outline-size solid $rct-outline-color;
42+
border-radius: $rct-outline-radius;
43+
}
44+
45+
&:not(:focus-visible) {
46+
outline: none;
47+
}
48+
}
49+
3350
button {
3451
line-height: normal;
3552
color: inherit;
3653

3754
&:disabled {
3855
cursor: not-allowed;
3956
}
57+
58+
&:focus {
59+
outline-offset: $rct-outline-offset;
60+
}
4061
}
4162

4263
.rct-bare-label {
4364
cursor: default;
4465
}
4566

4667
label {
68+
display: flex;
69+
align-items: center;
4770
margin-bottom: 0;
4871
cursor: pointer;
4972

@@ -57,12 +80,32 @@ $rct-clickable-focus: rgba($rct-icon-color, .2) !default;
5780
}
5881
}
5982

60-
&:not(.rct-native-display) input {
61-
display: none;
83+
input {
84+
margin: 0 5px;
85+
cursor: pointer;
86+
87+
&:focus {
88+
outline-offset: $rct-checkbox-outline-offset;
89+
}
6290
}
6391

64-
&.rct-native-display input {
65-
margin: 0 5px;
92+
&:not(.rct-native-display) input {
93+
// Hide the native checkbox (but keep in accessibility tree and accessible via touch)
94+
position: absolute;
95+
opacity: 0;
96+
97+
// Show focus effect on pseudo-checkbox
98+
&:focus {
99+
+ .rct-checkbox {
100+
outline: $rct-outline-size solid $rct-outline-color;
101+
outline-offset: $rct-outline-offset;
102+
border-radius: $rct-outline-radius;
103+
}
104+
105+
&:not(:focus-visible) + .rct-checkbox {
106+
outline: none;
107+
}
108+
}
66109
}
67110

68111
.rct-icon {

test/CheckboxTree.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ describe('<CheckboxTree />', () => {
160160
it('should trigger a check event when pressing one of the supplied values', async () => {
161161
let actual = null;
162162

163-
const { container } = render(
163+
render(
164164
<CheckboxTree
165165
checkKeys={['Shift']}
166166
checked={[]}
@@ -171,7 +171,7 @@ describe('<CheckboxTree />', () => {
171171
/>,
172172
);
173173

174-
await fireEvent.keyUp(container.querySelector('.rct-checkbox'), { key: 'Shift' });
174+
await fireEvent.keyUp(screen.getByRole('checkbox'), { key: 'Shift' });
175175

176176
assert.deepEqual(actual, ['jupiter']);
177177
});

test/TreeNode.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,7 @@ describe('<TreeNode />', () => {
378378
it('should trigger on key press', async () => {
379379
let actual = {};
380380

381-
const { container } = render(
381+
render(
382382
<TreeNode
383383
{...baseProps}
384384
checked={2}
@@ -389,7 +389,7 @@ describe('<TreeNode />', () => {
389389
/>,
390390
);
391391

392-
await fireEvent.keyUp(container.querySelector('.rct-checkbox'), { key: 'Enter' });
392+
await fireEvent.keyUp(screen.getByRole('checkbox'), { key: 'Enter' });
393393

394394
assert.isTrue(actual.checked);
395395
});

0 commit comments

Comments
 (0)