-
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,7 +13,7 @@ import {useUniqueId} from '../../utilities/unique-id'; | |
| import {Choice, helpTextID} from '../Choice'; | ||
| import {errorTextID} from '../InlineError'; | ||
| import {Icon} from '../Icon'; | ||
| import {Error, Key, CheckboxHandles} from '../../types'; | ||
| import {Error, CheckboxHandles, Key} from '../../types'; | ||
| import {WithinListboxContext} from '../../utilities/listbox/context'; | ||
|
|
||
| import styles from './Checkbox.scss'; | ||
|
|
@@ -92,20 +92,21 @@ export const Checkbox = forwardRef<CheckboxHandles, CheckboxProps>( | |
| setKeyFocused(false); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The reason we have this keyFocused on checkboxes is 2 folds:
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Alright so I've added back the Instead of calling |
||
| }; | ||
|
|
||
| const handleInput = () => { | ||
| if (onChange == null || inputNode.current == null || disabled) { | ||
| return; | ||
| const handleKeyUp = (event: React.KeyboardEvent) => { | ||
| const {keyCode} = event; | ||
|
|
||
| if (keyCode === Key.Space || keyCode === Key.Tab) { | ||
| !keyFocused && setKeyFocused(true); | ||
| } | ||
| onChange(!inputNode.current.checked, id); | ||
| inputNode.current.focus(); | ||
| }; | ||
|
|
||
| const handleKeyUp = (event: React.KeyboardEvent) => { | ||
| const {keyCode} = event; | ||
| !keyFocused && setKeyFocused(true); | ||
| if (keyCode === Key.Space) { | ||
| handleInput(); | ||
| const handleOnClick = () => { | ||
| if (onChange == null || inputNode.current == null || disabled) { | ||
| return; | ||
| } | ||
|
|
||
| onChange(inputNode.current.checked, id); | ||
| inputNode.current.focus(); | ||
| }; | ||
|
|
||
| const describedBy: string[] = []; | ||
|
|
@@ -152,13 +153,11 @@ export const Checkbox = forwardRef<CheckboxHandles, CheckboxProps>( | |
| helpText={helpText} | ||
| error={error} | ||
| disabled={disabled} | ||
| onClick={handleInput} | ||
| onMouseOver={handleMouseOver} | ||
| onMouseOut={handleMouseOut} | ||
| > | ||
| <span className={wrapperClassName}> | ||
| <input | ||
| onKeyUp={handleKeyUp} | ||
| ref={inputNode} | ||
| id={id} | ||
| name={name} | ||
|
|
@@ -167,17 +166,22 @@ export const Checkbox = forwardRef<CheckboxHandles, CheckboxProps>( | |
| checked={isChecked} | ||
| disabled={disabled} | ||
| className={inputClassName} | ||
| onFocus={onFocus} | ||
| onBlur={handleBlur} | ||
| onClick={stopPropagation} | ||
| onChange={noop} | ||
| onClick={handleOnClick} | ||
| onFocus={onFocus} | ||
| onKeyUp={handleKeyUp} | ||
| aria-invalid={error != null} | ||
| aria-controls={ariaControls} | ||
| aria-describedby={ariaDescribedBy} | ||
| role={isWithinListbox ? 'presentation' : 'checkbox'} | ||
| {...indeterminateAttributes} | ||
| /> | ||
| <span className={backdropClassName} /> | ||
| <span | ||
| className={backdropClassName} | ||
| onClick={stopPropagation} | ||
| onKeyUp={stopPropagation} | ||
| /> | ||
| <span className={styles.Icon}> | ||
| <Icon source={iconSource} /> | ||
| </span> | ||
|
|
@@ -189,6 +193,8 @@ export const Checkbox = forwardRef<CheckboxHandles, CheckboxProps>( | |
|
|
||
| function noop() {} | ||
|
|
||
| function stopPropagation<E>(event: React.MouseEvent<E>) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IIRC this was also part of the focus issue
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please refer to other PR comments on the 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. |
||
| function stopPropagation( | ||
| event: React.MouseEvent | React.KeyboardEvent | React.FormEvent, | ||
| ) { | ||
| event.stopPropagation(); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,7 +2,6 @@ import React, {AllHTMLAttributes} from 'react'; | |
| import {mountWithApp} from 'tests/utilities'; | ||
|
|
||
| import {Key} from '../../../types'; | ||
| import {Choice} from '../../Choice'; | ||
| import {Checkbox} from '../Checkbox'; | ||
|
|
||
| describe('<Checkbox />', () => { | ||
|
|
@@ -32,18 +31,6 @@ describe('<Checkbox />', () => { | |
| }); | ||
| }); | ||
|
|
||
| it('does not propagate click events from input element', () => { | ||
| const spy = jest.fn(); | ||
| const element = mountWithApp( | ||
| <Checkbox id="MyCheckbox" label="Checkbox" onChange={spy} />, | ||
| ); | ||
|
|
||
| element.find('input')!.trigger('onClick', { | ||
| stopPropagation: () => {}, | ||
| }); | ||
| expect(spy).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| describe('onChange()', () => { | ||
| it('is called with the updated checked value of the input on click', () => { | ||
| const spy = jest.fn(); | ||
|
|
@@ -52,57 +39,25 @@ describe('<Checkbox />', () => { | |
| ); | ||
|
|
||
| (element.find('input')!.domNode as HTMLInputElement).checked = true; | ||
| element.find(Choice)?.trigger('onClick'); | ||
|
|
||
| expect(spy).toHaveBeenCalledWith(false, 'MyCheckbox'); | ||
| }); | ||
|
|
||
| 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); | ||
| }); | ||
|
Comment on lines
-60
to
-71
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you brought back the keyUp, does this still stand?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See #4631 (comment) |
||
|
|
||
| it('is not from keys other than space', () => { | ||
| const spy = jest.fn(); | ||
| const element = mountWithApp( | ||
| <Checkbox id="MyCheckbox" label="Checkbox" onChange={spy} />, | ||
| ); | ||
|
|
||
| element.find('input')!.trigger('onKeyUp', { | ||
| keyCode: Key.Enter, | ||
| const event = new MouseEvent('click', { | ||
| view: window, | ||
| bubbles: true, | ||
| cancelable: true, | ||
| }); | ||
| element.find('input')!.domNode?.dispatchEvent(event); | ||
|
|
||
| expect(spy).not.toHaveBeenCalled(); | ||
| expect(spy).toHaveBeenCalledWith(false, 'MyCheckbox'); | ||
| }); | ||
|
|
||
| it('sets focus on the input when checkbox is toggled off', () => { | ||
| const checkbox = mountWithApp( | ||
| <Checkbox checked id="checkboxId" label="Checkbox" onChange={noop} />, | ||
| ); | ||
| checkbox.find(Choice)!.trigger('onClick'); | ||
| checkbox.find('input')!.trigger('onClick'); | ||
|
|
||
| expect(document.activeElement).toBe(checkbox.find('input')!.domNode); | ||
| }); | ||
|
|
||
| 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(); | ||
| }); | ||
|
Comment on lines
-95
to
-104
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 not called from click events when disabled', () => { | ||
| const spy = jest.fn(); | ||
| const checkbox = mountWithApp( | ||
|
|
@@ -175,22 +130,6 @@ describe('<Checkbox />', () => { | |
| disabled: false, | ||
| }); | ||
| }); | ||
|
|
||
| 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, | ||
| }); | ||
| }); | ||
|
Comment on lines
-179
to
-193
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
| }); | ||
|
|
||
| describe('helpText', () => { | ||
|
|
||
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
keyUpevents, 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
onKeyUphandler is back on theCheckbox.Inputelement, we aren't managing it the same way this test intends. TheonKeyUponly manages the focusable state and no longer triggers value change (we let the subsequent click event triggered by the browser do this).