Skip to content

Commit

Permalink
fix(a11y): Add keyboard support to highlight comment flow (#690)
Browse files Browse the repository at this point in the history
  • Loading branch information
jstoffan authored Aug 31, 2021
1 parent 8141c7d commit 46a64ef
Show file tree
Hide file tree
Showing 5 changed files with 75 additions and 39 deletions.
6 changes: 3 additions & 3 deletions src/common/useOutsideEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ import * as React from 'react';
export default function useOutsideEvent(
type: string,
ref?: React.RefObject<Element> | Element | null,
callback?: () => void,
callback?: EventListener,
): void {
React.useEffect(() => {
function handleEvent(event: Event): void {
function handleEvent<T extends Event>(event: T): void {
const element = ref instanceof Element ? ref : ref?.current;
const containsEventTarget = !!element?.contains(event.target as Node);

if (callback && !containsEventTarget) {
callback();
callback(event);
}
}

Expand Down
26 changes: 20 additions & 6 deletions src/components/Popups/PopupHighlight.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import './PopupHighlight.scss';

export type Props = {
onCancel?: () => void;
onClick?: (event: React.MouseEvent) => void;
onSubmit?: () => void;
shape: Shape;
};

Expand Down Expand Up @@ -45,7 +45,7 @@ const options: Partial<Options> = {
placement: 'bottom',
};

export default function PopupHighlight({ onCancel = noop, onClick = noop, shape }: Props): JSX.Element {
export default function PopupHighlight({ onCancel = noop, onSubmit = noop, shape }: Props): JSX.Element {
const buttonRef = React.useRef<HTMLButtonElement>(null);
const { height, width, x, y } = shape;

Expand All @@ -60,19 +60,33 @@ export default function PopupHighlight({ onCancel = noop, onClick = noop, shape
}),
};

const handleClick = (event: React.MouseEvent): void => {
onClick(event);
const handleKeydownOutside = (event: Event): void => {
if (!(event instanceof KeyboardEvent) || event.key !== 'Enter' || event.ctrlKey || event.metaKey) {
return;
}

event.preventDefault(); // Consume any enter keydown event if the promoter is open
event.stopPropagation(); // Prevent the enter keydown event from adding a new line to the textarea

onSubmit();
};

useOutsideEvent('keydown', buttonRef, handleKeydownOutside);
useOutsideEvent('mousedown', buttonRef, onCancel);

return (
<PopupBase className="ba-PopupHighlight" options={options} reference={reference}>
<PopupBase
aria-live="polite"
className="ba-PopupHighlight"
options={options}
reference={reference}
role="dialog"
>
<button
ref={buttonRef}
className="ba-PopupHighlight-button"
data-testid="ba-PopupHighlight-button"
onClick={handleClick}
onClick={onSubmit}
type="button"
>
<IconHighlightTextAnnotation className="ba-PopupHighlight-icon" />
Expand Down
69 changes: 46 additions & 23 deletions src/components/Popups/__tests__/PopupHighlight-test.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
import React from 'react';
import { shallow, ShallowWrapper } from 'enzyme';
import { act } from 'react-dom/test-utils';
import { mount, ReactWrapper } from 'enzyme';
import PopupBase from '../PopupBase';
import PopupHighlight from '../PopupHighlight';
import useOutsideEvent from '../../../common/useOutsideEvent';

jest.mock('../../../common/useOutsideEvent');
jest.mock('../PopupBase');

describe('PopupHighlight', () => {
const defaults = {
onCancel: jest.fn(),
onClick: jest.fn(),
shape: {
height: 10,
width: 100,
x: 200,
y: 200,
},
};

const getWrapper = (props = {}): ShallowWrapper => shallow(<PopupHighlight {...defaults} {...props} />);
const getWrapper = (props = {}): ReactWrapper =>
mount(
<PopupHighlight
onCancel={jest.fn()}
onSubmit={jest.fn()}
shape={{
height: 10,
width: 100,
x: 200,
y: 200,
}}
{...props}
/>,
);

const buttonRef = React.createRef();

Expand All @@ -27,22 +29,43 @@ describe('PopupHighlight', () => {
});

describe('event handlers', () => {
test('should call onCancel', () => {
getWrapper();
test('should handle outside mousedown events', () => {
const onCancel = jest.fn();
const wrapper = getWrapper({ onCancel });

expect(defaults.onCancel).toHaveBeenCalled();
expect(useOutsideEvent).toHaveBeenCalledWith('mousedown', buttonRef, defaults.onCancel);
act(() => {
document.dispatchEvent(new MouseEvent('mousedown'));
});
wrapper.update();

expect(onCancel).toBeCalled();
});

test('should handle outside keydown events', () => {
const event = new KeyboardEvent('keydown', { key: 'Enter' });
const wrapper = getWrapper();

jest.spyOn(event, 'preventDefault');
jest.spyOn(event, 'stopPropagation');

act(() => {
document.dispatchEvent(event);
});
wrapper.update();

expect(event.preventDefault).toBeCalled();
expect(event.stopPropagation).toBeCalled();
});
});

describe('mouse events', () => {
test('should call onClick', () => {
const wrapper = getWrapper();
const onSubmit = jest.fn();
const wrapper = getWrapper({ onSubmit });

const mouseEvent = new MouseEvent('click');
wrapper.find('[data-testid="ba-PopupHighlight-button"]').simulate('click', mouseEvent);
wrapper.find('[data-testid="ba-PopupHighlight-button"]').simulate('click');

expect(defaults.onClick).toHaveBeenCalledWith(mouseEvent);
expect(onSubmit).toBeCalled();
});
});

Expand Down
11 changes: 5 additions & 6 deletions src/highlight/HighlightAnnotations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,16 +70,15 @@ const HighlightAnnotations = (props: Props): JSX.Element => {
setStatus(CreatorStatus.staged);
}, [selection, setStaged, setStatus]);

const handleCancel = (): void => {
setSelection(null);
};

const handlePromote = (): void => {
stageSelection();

setIsPromoting(true);
};

const handleCancel = (): void => {
setSelection(null);
};

const handleStagedMount = (uuid: string): void => {
setReferenceId(uuid);
};
Expand Down Expand Up @@ -126,7 +125,7 @@ const HighlightAnnotations = (props: Props): JSX.Element => {
<div className="ba-HighlightAnnotations-popup">
<PopupHighlight
onCancel={handleCancel}
onClick={handlePromote}
onSubmit={handlePromote}
shape={getBoundingRect(selection.rects)}
/>
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/highlight/__tests__/HighlightAnnotations-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ describe('HighlightAnnotations', () => {
describe('handlePromote()', () => {
test('should clear selection and set isPromoting', () => {
const wrapper = getWrapper({ selection: selectionMock });
wrapper.find(PopupHighlight).prop('onClick')!({} as React.MouseEvent<HTMLButtonElement>);
wrapper.find(PopupHighlight).prop('onSubmit')!();

expect(defaults.setStaged).toHaveBeenCalledWith({
location: 1,
Expand Down

0 comments on commit 46a64ef

Please sign in to comment.