Skip to content

Conversation

@patrickracicot
Copy link
Contributor

@patrickracicot patrickracicot commented Nov 11, 2021

WHY are these changes introduced?

Fixes #3973

The goal of this PR is too properly support screen readers on the Checkbox component. Currently, screen reader users must use a combination of space+enter key to be able to check/uncheck the checkbox.

WHAT is this pull request doing?

The current proposal modifies how the component handles the click and keyboard events. We now avoid hijacking the events on the Choice component. 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 the onClick handler of the wrapping Choice component.

I also removed the onKeyPress handler of the <input />. According to the HTML spec, triggering the space keyboard event will activate (invoke the click event) of an interact-able element. To properly support the focus state, I added the onFocus handler on the <input /> since this was managed inside the onKeyPress previously 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 🎩

  • With a storybook example open
  • Tab to focus on the checkbox
  • Hit space to check
  • With the mouse, left click the box to uncheck
  • With the mouse, left click the label to check
  • With a screen reader, use the screen readers shortcut to activate the checkbox
    • For mac VoiceOver, the combination is control+option+space
Copy-paste this code in playground/Playground.tsx:
import React from 'react';
import {Page} from '../src';

export function Playground() {
  return (
    <Page title="Playground">
      {/* Add the code you want to test in here */}
    </Page>
  );
}

🎩 checklist

@patrickracicot patrickracicot force-pushed the pracicot/checkbox_a11y branch 2 times, most recently from 0372437 to 0b8ef91 Compare November 11, 2021 20:55
@github-actions
Copy link
Contributor

github-actions bot commented Nov 11, 2021

size-limit report

Path Size
cjs 166.19 KB (+0.01% 🔺)
esm 96.75 KB (+0.01% 🔺)
esnext 143.3 KB (-0.01% 🔽)
css 34.29 KB (0%)

@patrickracicot patrickracicot force-pushed the pracicot/checkbox_a11y branch 3 times, most recently from 900473b to 0a094f5 Compare November 12, 2021 17:15
@patrickracicot patrickracicot self-assigned this Nov 12, 2021
@patrickracicot patrickracicot added the Bug Something is broken and not working as intended in the system. label Nov 12, 2021
@patrickracicot patrickracicot force-pushed the pracicot/checkbox_a11y branch 2 times, most recently from 265c780 to 9ad0133 Compare November 12, 2021 17:37
Comment on lines -77 to -88
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();
});
Copy link
Contributor Author

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.

Copy link
Contributor

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?

Copy link
Contributor Author

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).

Comment on lines -95 to -104
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();
});
Copy link
Contributor Author

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.

Comment on lines -60 to -71
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);
});
Copy link
Contributor Author

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.

Copy link
Contributor

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines -179 to -193
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,
});
});
Copy link
Contributor Author

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.

@patrickracicot patrickracicot force-pushed the pracicot/checkbox_a11y branch 2 times, most recently from 2cd2ffc to 05e4131 Compare November 12, 2021 17:49
@patrickracicot patrickracicot marked this pull request as ready for review November 12, 2021 18:00
@kyledurand
Copy link
Member

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

@kyledurand kyledurand requested a review from dleroux November 15, 2021 15:33
Copy link

@NathanPJF NathanPJF left a 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 keyFocused state 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);
Copy link
Contributor

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:

  1. We won't want the styles (focus ring) to be applied on click. Only when tabbing. This may be fixed with :focus-visible having better support now?

  2. 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.

Copy link
Contributor

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.

Copy link
Contributor Author

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>) {
Copy link
Contributor

@dleroux dleroux Nov 17, 2021

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

Copy link
Contributor

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.

Copy link
Contributor Author

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.

@dleroux
Copy link
Contributor

dleroux commented Nov 17, 2021

This PR:

Screen.Recording.2021-11-17.at.3.50.54.PM.mov

Core:

Screen.Recording.2021-11-17.at.3.50.20.PM.mov

Comment on lines 38 to 40
onClick={onInteraction}
onKeyUp={onInteraction}
onChange={stopPropagation}
Copy link
Contributor Author

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

onChange?(
newChecked: boolean,
id: string,
event?: React.KeyboardEvent | React.MouseEvent<Element, MouseEvent>,
Copy link
Contributor

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

Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Member

@BPScott BPScott Nov 22, 2021

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

Copy link
Contributor Author

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

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.

@dleroux
Copy link
Contributor

dleroux commented Nov 18, 2021

Code looks good, so does 🎩 but let hear from others before opening up to passing the event.

@dleroux dleroux self-requested a review November 23, 2021 16:36
Copy link
Contributor

@dleroux dleroux left a 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

@patrickracicot patrickracicot merged commit 5572d7f into main Nov 23, 2021
@patrickracicot patrickracicot deleted the pracicot/checkbox_a11y branch November 23, 2021 18:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Bug Something is broken and not working as intended in the system.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Checkbox][a11y] Unable to check checkbox with voiceover

5 participants