-
Notifications
You must be signed in to change notification settings - Fork 1.2k
[Checkbox] properly support screen readers #4631
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
0372437 to
0b8ef91
Compare
size-limit report
|
900473b to
0a094f5
Compare
265c780 to
9ad0133
Compare
| it('is called when the CheckableButton pressed with spacebar', () => { | ||
| const spy = jest.fn(); | ||
| const element = mountWithApp( | ||
| <CheckableButton {...CheckableButtonProps} onToggleAll={spy} />, | ||
| ); | ||
|
|
||
| element.find(Checkbox)!.find('input')!.trigger('onKeyUp', { | ||
| keyCode: Key.Space, | ||
| }); | ||
|
|
||
| expect(spy).toHaveBeenCalled(); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we no longer manually handler the keyUp events, we no longer need to test this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here? Since KeyUp is back, does this still stand?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Although the onKeyUp handler is back on the Checkbox.Input element, we aren't managing it the same way this test intends. The onKeyUp only manages the focusable state and no longer triggers value change (we let the subsequent click event triggered by the browser do this).
| it('is not called from keyboard events when disabled', () => { | ||
| const spy = jest.fn(); | ||
| const checkbox = mountWithApp( | ||
| <Checkbox label="label" disabled onChange={spy} />, | ||
| ); | ||
| checkbox.find('input')!.trigger('onKeyUp', { | ||
| keyCode: Key.Enter, | ||
| }); | ||
| expect(spy).not.toHaveBeenCalled(); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we no longer manually handler the keyUp events, we no longer need to test this.
| it('is called when space is pressed', () => { | ||
| const spy = jest.fn(); | ||
| const element = mountWithApp( | ||
| <Checkbox id="MyCheckbox" label="Checkbox" onChange={spy} />, | ||
| ); | ||
|
|
||
| element.find('input')!.trigger('onKeyUp', { | ||
| keyCode: Key.Space, | ||
| }); | ||
|
|
||
| expect(spy).toHaveBeenCalledTimes(1); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we no longer manually handler the keyUp events, we no longer need to test this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you brought back the keyUp, does this still stand?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See #4631 (comment)
| it('can change values when disabled', () => { | ||
| const spy = jest.fn(); | ||
| const checkbox = mountWithApp( | ||
| <Checkbox label="label" disabled onChange={spy} />, | ||
| ); | ||
|
|
||
| checkbox.find('input')!.trigger('onKeyUp', { | ||
| keyCode: Key.Enter, | ||
| }); | ||
| checkbox.setProps({checked: true}); | ||
|
|
||
| expect(checkbox).toContainReactComponent('input', { | ||
| checked: true, | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we no longer manually handler the keyUp events, we no longer need to test this.
2cd2ffc to
05e4131
Compare
|
I'm down to make this sort of change but I think we ran into issues with handling focus on checkboxes natively back in the day. I think @dleroux has context on why |
NathanPJF
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've 🎩 'd on Mac+VoiceOver, JAWS, and NVDA; all works as expected
I think we ran into issues with handling focus on checkboxes natively back in the day
@kyledurand and @dleroux: I went looking through past GitHub issues about checkboxes and focus that involved Dan to see what more to check:
- ResourceList #792, I found through tophatting that focus is managed as expected for the ResourceList with multi-select.
- Styling in general #2661; and there isn't an issue with the changes in this PR as it still uses the
keyFocusedstate here.
Those were the ones that stood out to me to double-check; happy to hear if there's an issue I missed.
|
|
||
| const handleBlur = () => { | ||
| onBlur && onBlur(); | ||
| setKeyFocused(false); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason we have this keyFocused on checkboxes is 2 folds:
-
We won't want the styles (focus ring) to be applied on click. Only when tabbing. This may be fixed with
:focus-visiblehaving better support now? -
Also, since the focus right is on the backdrop, tabbing to the checkbox did not trigger the event to bubble and apply the styles. I believe this still an issue.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just had a look and yes does show the focus rectangle on click now. so the classname should only be applied when it's a keyboard click.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alright so I've added back the onKeyUp logic, but made some adjustments to it.
Instead of calling setKeyFocused(true) for every key up event, I only call it for the Space and Tab keyboard events. This will prevent an issue we had in the current implementation where if a user clicked on the checkbox and then type any key other then Tab or Space, it would still apply the focus style.
|
|
||
| function noop() {} | ||
|
|
||
| function stopPropagation<E>(event: React.MouseEvent<E>) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IIRC this was also part of the focus issue
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pretty sure this had to do with the bulk actions. I would remove this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please refer to other PR comments on the IndexTable component in which I have added a fix to properly support the bulk actions.
TLDR; the root cause of the issue were the same as for the checkbox hence managing events on other DOM nodes then the originating element.
|
This PR: Screen.Recording.2021-11-17.at.3.50.54.PM.movCore: Screen.Recording.2021-11-17.at.3.50.20.PM.mov |
| onClick={onInteraction} | ||
| onKeyUp={onInteraction} | ||
| onChange={stopPropagation} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We want to avoid doing this as this is event bubbling management and isn't required. Instead, we can simply subscribe to the onChange prop of the PolarisCheckbox
72970f9 to
4f88e86
Compare
src/components/Checkbox/Checkbox.tsx
Outdated
| onChange?( | ||
| newChecked: boolean, | ||
| id: string, | ||
| event?: React.KeyboardEvent | React.MouseEvent<Element, MouseEvent>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to pass the event here? There have been ongoing discussions about this and we have always opted to not pass the event. cc @kyledurand
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@kyledurand @BPScott there was discussion in this repo https://github.com/orgs/Shopify/teams/polaris/discussions/ but since the team name changed I can't find it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fore more context here is the reasons why I'm passing the event inside the onChange handler.
Inside the onInteraction method of the RowContext requires this event as arg. It needs to ensure that we stop the propagation of the event further up in the DOM tree and avoids unwanted side-effects. Although I'm not a fan of always passing event through javascript method args, I feel like in this case it's necessary to avoid the need of further refactoring in the IndexTable.
Also the way the code was written before my proposal, we had a <div> wrapper around the Checkbox component which would listen to the onClick and onKeyUp events that were bubbling up. With this in mind, it's safe to say that passing the event through the method args serves the same purpose.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was talk from back before the days that Github discussions existed: #521. (and #4111 is a recent-ish reitteration)
It's worth pointing out that exposing dom events as part of our public api plays badly with aspirations around Argo which has no way of moving these events around.
Passing dom events around internally is bearable, but them being part of the public API causes problems. The div wrapper to capture bubbling isn't great but it's better than creating APIs that are incompatible with Argo. That sad, if this is optional then it might not be the end of the world but it might lead to suprising behaviour. You'd need want somebody on the Argo team to give more context on this, as I'm mostly parroting old talking points as I've not been working with polaris-react on a day-to-day basis for a while.
#4111 (comment) - passing through an object containing exactly what is desired rather than tossing through the whole dom event might be an option
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @BPScott for the context.
After reading and gathering context, I went back to the code and tried to see how I could avoid adding the event in our API while still properly supporting the event bubbling.
The fix
-
On the wrapping div inside the
IndexTable/Checkbox, we only now only listen to the bubblingonClickevent.
<div className={wrapperClassName} onClick={onInteraction} onKeyUp={noop} -
Inside the
Polaris/Checkboxcomponent, we stop the propagation ofonClickandonKeyUpevents emerging from thespin.backdropDOM element
polaris-react/src/components/Checkbox/Checkbox.tsx
Lines 180 to 184 in dfb406c
<span className={backdropClassName} onClick={stopPropagation} onKeyUp={stopPropagation} />
The explanation
To be able to bind on the div.onClick on the wrapper we need to make sure that only the click events coming from the <Input /> control are bubbled up. Also since the activation (space for a checkbox) of a control will trigger the onClick event we need to ignore the onKeyUp events bubbled up to the wrapping div.
|
Code looks good, so does 🎩 but let hear from others before opening up to passing the event. |
7a8086d to
649051d
Compare
dfb406c to
123c986
Compare
dleroux
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code looks good, 🎩 too! Thanks @patrickracicot
WHY are these changes introduced?
Fixes #3973
The goal of this PR is too properly support screen readers on the
Checkboxcomponent. Currently, screen reader users must use a combination ofspace+enterkey to be able to check/uncheck the checkbox.WHAT is this pull request doing?
The current proposal modifies how the component handles the
clickandkeyboardevents. We now avoid hijacking the events on theChoicecomponent. Instead we listen to the events directly on the<input />element. The reason why we need to do this is due to the fact that screen readers are triggering the events directly on the<input />element and this triggered event is not capturable by theonClickhandler of the wrappingChoicecomponent.I also removed the
onKeyPresshandler of the<input />. According to the HTML spec, triggering thespacekeyboard event will activate (invoke the click event) of an interact-able element. To properly support the focus state, I added theonFocushandler on the<input />since this was managed inside theonKeyPresspreviously which is a side effect.For a deep dive please refer to the following document: Polaris Checkbox (in Draft Orders context)
How to 🎩
🖥 Local development instructions
🗒 General tophatting guidelines
📄 Changelog guidelines
To properly 🎩 these changes, one will need to build the repo using
yarn dev.The checkbox can be tested through the playbook and is actually used in the following examples
Step to 🎩
spaceto checkcontrol+option+spaceCopy-paste this code in
playground/Playground.tsx:🎩 checklist
README.mdwith documentation changes