[fix] Prevent onClick being called when certain roles are disabled

Fix #1779
Close #1780
This commit is contained in:
Charlie Croom
2020-10-22 10:31:33 -04:00
committed by Nicolas Gallagher
parent a2d72ee89c
commit cea4172efb
3 changed files with 101 additions and 33 deletions
@@ -23,37 +23,5 @@ describe('exports/createElement', () => {
const { container } = render(createElement(Custom, { accessibilityRole: 'link' }));
expect(container.firstChild.nodeName).toBe('DIV');
});
const testRole = ({ accessibilityRole, disabled }) => {
[{ key: 'Enter' }, { key: ' ' }].forEach(({ key }) => {
test(`"onClick" is ${disabled ? 'not ' : ''}called when "${key}" key is pressed`, () => {
const onClick = jest.fn();
const { container } = render(
createElement('span', { accessibilityRole, disabled, onClick })
);
const event = document.createEvent('CustomEvent');
event.initCustomEvent('keydown', true, true);
event.key = key;
container.firstChild.dispatchEvent(event);
expect(onClick).toHaveBeenCalledTimes(disabled ? 0 : 1);
});
});
};
describe('value is "button" and disabled is "true"', () => {
testRole({ accessibilityRole: 'button', disabled: true });
});
describe('value is "button" and disabled is "false"', () => {
testRole({ accessibilityRole: 'button', disabled: false });
});
describe('value is "menuitem" and disabled is "true"', () => {
testRole({ accessibilityRole: 'menuitem', disabled: true });
});
describe('value is "menuitem" and disabled is "false"', () => {
testRole({ accessibilityRole: 'menuitem', disabled: false });
});
});
});
@@ -140,6 +140,106 @@ describe('modules/createDOMProps', () => {
});
});
describe('prop "onClick"', () => {
const callsOnClick = (component, accessibilityRole, disabled = false) => {
const onClick = jest.fn();
const event = { stopPropagation: jest.fn() };
const finalProps = createDOMProps(component, { accessibilityRole, disabled, onClick });
finalProps.onClick(event);
return onClick.mock.calls.length === 1;
};
test('is called for various roles', () => {
expect(callsOnClick('div', 'link')).toBe(true);
expect(callsOnClick('div', 'button')).toBe(true);
expect(callsOnClick('div', 'textbox')).toBe(true);
expect(callsOnClick('div', 'menuitem')).toBe(true);
expect(callsOnClick('div', 'bogus')).toBe(true);
expect(callsOnClick('a')).toBe(true);
expect(callsOnClick('button')).toBe(true);
expect(callsOnClick('input')).toBe(true);
expect(callsOnClick('select')).toBe(true);
expect(callsOnClick('textarea')).toBe(true);
expect(callsOnClick('h1')).toBe(true);
});
test('is not called when disabled is true', () => {
expect(callsOnClick('div', 'link', true)).toBe(false);
expect(callsOnClick('div', 'button', true)).toBe(false);
expect(callsOnClick('div', 'menuitem', true)).toBe(false);
expect(callsOnClick('a', undefined, true)).toBe(false);
expect(callsOnClick('button', undefined, true)).toBe(false);
expect(callsOnClick('input', undefined, true)).toBe(false);
expect(callsOnClick('select', undefined, true)).toBe(false);
expect(callsOnClick('textarea', undefined, true)).toBe(false);
expect(callsOnClick('div', 'textbox', true)).toBe(true);
expect(callsOnClick('div', 'bogus', true)).toBe(true);
expect(callsOnClick('h1', undefined, true)).toBe(true);
});
});
describe('prop "onKeyDown"', () => {
const callsOnClick = key => (component, accessibilityRole, disabled = false) => {
const onClick = jest.fn();
const onKeyDown = jest.fn();
const event = { key, preventDefault: jest.fn() };
const finalProps = createDOMProps(component, {
accessibilityRole,
disabled,
onClick,
onKeyDown
});
finalProps.onKeyDown(event);
// The original onKeyDown should always be called
expect(onKeyDown).toHaveBeenCalled();
return onClick.mock.calls.length === 1;
};
const respondsToEnter = callsOnClick('Enter');
const respondsToSpace = callsOnClick(' ');
test('does not emulate "onClick" when disabled', () => {
expect(respondsToEnter('div', 'link', true)).toBe(false);
expect(respondsToEnter('div', 'button', true)).toBe(false);
expect(respondsToEnter('div', 'textbox', true)).toBe(false);
expect(respondsToEnter('div', 'menuitem', true)).toBe(false);
expect(respondsToEnter('div', 'bogus', true)).toBe(false);
});
test('does not emulate "onClick" for native elements', () => {
expect(respondsToEnter('a')).toBe(false);
expect(respondsToEnter('button')).toBe(false);
expect(respondsToEnter('input')).toBe(false);
expect(respondsToEnter('select')).toBe(false);
expect(respondsToEnter('textarea')).toBe(false);
expect(respondsToEnter('h1')).toBe(false);
expect(respondsToEnter('div', 'link')).toBe(false);
expect(respondsToSpace('a')).toBe(false);
expect(respondsToSpace('button')).toBe(false);
expect(respondsToSpace('input')).toBe(false);
expect(respondsToSpace('select')).toBe(false);
expect(respondsToSpace('textarea')).toBe(false);
expect(respondsToSpace('h1')).toBe(false);
expect(respondsToSpace('div', 'link')).toBe(false);
});
test('emulates "onClick" for "Enter" for certain roles', () => {
expect(respondsToEnter('div', 'button')).toBe(true);
expect(respondsToEnter('div', 'menuitem')).toBe(true);
expect(respondsToEnter('div', 'textbox')).toBe(false);
expect(respondsToEnter('div', 'bogus')).toBe(false);
});
test('emulates "onClick" for "Space" for certain roles', () => {
expect(respondsToSpace('div', 'button')).toBe(true);
expect(respondsToSpace('div', 'menuitem')).toBe(true);
expect(respondsToSpace('div', 'textbox')).toBe(false);
expect(respondsToSpace('div', 'bogus')).toBe(false);
});
});
test('prop "accessibilityLabel" becomes "aria-label"', () => {
const accessibilityLabel = 'accessibilityLabel';
const props = createProps({ accessibilityLabel });
@@ -225,7 +225,7 @@ const createDOMProps = (component, props) => {
// Keyboard accessibility
// Button-like roles should trigger 'onClick' if SPACE key is pressed.
// Button-like roles should not trigger 'onClick' if they are disabled.
if (domProps['data-focusable']) {
if (isNativeInteractiveElement || role === 'button' || role === 'menuitem') {
const onClick = domProps.onClick;
if (onClick != null) {
if (disabled) {