diff --git a/src/lib/__tests__/AnnotationControlsFSM-test.js b/src/lib/__tests__/AnnotationControlsFSM-test.js index 697ff2f922..38c2ae8b64 100644 --- a/src/lib/__tests__/AnnotationControlsFSM-test.js +++ b/src/lib/__tests__/AnnotationControlsFSM-test.js @@ -46,6 +46,7 @@ describe('lib/AnnotationControlsFSM', () => { const annotationControlsFSM = new AnnotationControlsFSM(); expect(annotationControlsFSM.transition(input, mode)).toBe(output); + expect(annotationControlsFSM.getMode()).toBe(output); expect(annotationControlsFSM.getState()).toBe(nextState); }); }); @@ -56,6 +57,7 @@ describe('lib/AnnotationControlsFSM', () => { const annotationControlsFSM = new AnnotationControlsFSM(); expect(annotationControlsFSM.transition(input)).toBe(AnnotationMode.NONE); + expect(annotationControlsFSM.getMode()).toBe(AnnotationMode.NONE); expect(annotationControlsFSM.getState()).toBe(AnnotationState.NONE); }); }); @@ -65,6 +67,7 @@ describe('lib/AnnotationControlsFSM', () => { const annotationControlsFSM = new AnnotationControlsFSM(); expect(annotationControlsFSM.transition(AnnotationInput.RESET)).toEqual(AnnotationMode.NONE); + expect(annotationControlsFSM.getMode()).toBe(AnnotationMode.NONE); expect(annotationControlsFSM.getState()).toEqual(AnnotationState.NONE); }); }); @@ -112,6 +115,7 @@ describe('lib/AnnotationControlsFSM', () => { const annotationControlsFSM = new AnnotationControlsFSM(AnnotationState.HIGHLIGHT); expect(annotationControlsFSM.transition(input, mode)).toEqual(output); + expect(annotationControlsFSM.getMode()).toBe(output); expect(annotationControlsFSM.getState()).toEqual(output); }); }); @@ -145,6 +149,7 @@ describe('lib/AnnotationControlsFSM', () => { const annotationControlsFSM = new AnnotationControlsFSM(AnnotationState.REGION); expect(annotationControlsFSM.transition(input, mode)).toEqual(output); + expect(annotationControlsFSM.getMode()).toBe(output); expect(annotationControlsFSM.getState()).toEqual(output); }); }); @@ -227,6 +232,7 @@ describe('lib/AnnotationControlsFSM', () => { const annotationControlsFSM = new AnnotationControlsFSM(AnnotationState.HIGHLIGHT_TEMP); expect(annotationControlsFSM.transition(input, mode)).toEqual(output); + expect(annotationControlsFSM.getMode()).toBe(output); expect(annotationControlsFSM.getState()).toEqual(output); }); }); @@ -260,6 +266,7 @@ describe('lib/AnnotationControlsFSM', () => { const annotationControlsFSM = new AnnotationControlsFSM(AnnotationState.REGION_TEMP); expect(annotationControlsFSM.transition(input, mode)).toEqual(output); + expect(annotationControlsFSM.getMode()).toBe(output); expect(annotationControlsFSM.getState()).toEqual(output); }); }); diff --git a/src/lib/viewers/controls/annotations/AnnotationsControls.tsx b/src/lib/viewers/controls/annotations/AnnotationsControls.tsx index 5bbf35c43c..55da65bfd1 100644 --- a/src/lib/viewers/controls/annotations/AnnotationsControls.tsx +++ b/src/lib/viewers/controls/annotations/AnnotationsControls.tsx @@ -9,29 +9,55 @@ import './AnnotationsControls.scss'; export type Props = { annotationMode?: AnnotationMode; - onAnnotationModeClick?: ({ mode }: { mode: AnnotationMode }) => void; hasHighlight?: boolean; hasRegion?: boolean; + onAnnotationModeClick?: ({ mode }: { mode: AnnotationMode }) => void; + onAnnotationModeEscape?: () => void; }; export default function AnnotationsControls({ annotationMode = AnnotationMode.NONE, + hasHighlight = false, + hasRegion = false, onAnnotationModeClick = noop, - hasHighlight = true, - hasRegion = true, + onAnnotationModeEscape = noop, }: Props): JSX.Element | null { const isFullscreen = useFullscreen(); const showHighlight = !isFullscreen && hasHighlight; const showRegion = !isFullscreen && hasRegion; - if (!showHighlight && !showRegion) { - return null; - } - + // Component event handlers const handleModeClick = (mode: AnnotationMode): void => { onAnnotationModeClick({ mode: annotationMode === mode ? AnnotationMode.NONE : mode }); }; + // Global event handlers + React.useEffect(() => { + const handleKeyDown = (event: KeyboardEvent): void => { + if (event.key !== 'Escape') { + return; + } + + event.preventDefault(); + event.stopPropagation(); + + onAnnotationModeEscape(); + }; + + if (annotationMode !== AnnotationMode.NONE) { + document.addEventListener('keydown', handleKeyDown); + } + + return (): void => { + document.removeEventListener('keydown', handleKeyDown); + }; + }, [annotationMode, onAnnotationModeEscape]); + + // Prevent empty group from being displayed + if (!showHighlight && !showRegion) { + return null; + } + return (
{ + const getWrapper = (props = {}): ShallowWrapper => + shallow( + + Test + , + ); + + describe('event handlers', () => { + test('should call the onClick callback with the given mode', () => { + const mode = AnnotationMode.HIGHLIGHT; + const onClick = jest.fn(); + const wrapper = getWrapper({ mode, onClick }); + + wrapper.simulate('click'); + + expect(onClick).toBeCalledWith(mode); + }); + }); + + describe('render', () => { + test('should return nothing if not enabled', () => { + const wrapper = getWrapper({ isEnabled: false }); + expect(wrapper.isEmptyRender()).toBe(true); + }); + + test('should return a valid wrapper', () => { + const wrapper = getWrapper(); + + expect(wrapper.hasClass('bp-AnnotationsButton')).toBe(true); + expect(wrapper.hasClass('bp-is-active')).toBe(false); // Default + expect(wrapper.text()).toBe('Test'); + }); + }); +}); diff --git a/src/lib/viewers/controls/annotations/__tests__/AnnotationsControls-test.tsx b/src/lib/viewers/controls/annotations/__tests__/AnnotationsControls-test.tsx index d16f941b26..d17fc66edc 100644 --- a/src/lib/viewers/controls/annotations/__tests__/AnnotationsControls-test.tsx +++ b/src/lib/viewers/controls/annotations/__tests__/AnnotationsControls-test.tsx @@ -1,14 +1,107 @@ import React from 'react'; -import { shallow } from 'enzyme'; +import { act } from 'react-dom/test-utils'; +import { ReactWrapper, mount } from 'enzyme'; import AnnotationsControls from '../AnnotationsControls'; +import { AnnotationMode } from '../types'; describe('AnnotationsControls', () => { + const getWrapper = (props = {}): ReactWrapper => mount(); + const getElement = (props = {}): ReactWrapper => getWrapper(props).childAt(0); + + beforeEach(() => { + jest.spyOn(document, 'addEventListener'); + jest.spyOn(document, 'removeEventListener'); + }); + + describe('lifecycle', () => { + test('should add and remove its event handlers on mount and unmount', () => { + const wrapper = getWrapper({ + annotationMode: AnnotationMode.REGION, + hasHighlight: true, + hasRegion: true, + }); + expect(document.addEventListener).toBeCalledWith('keydown', expect.any(Function)); + + wrapper.unmount(); + expect(document.removeEventListener).toBeCalledWith('keydown', expect.any(Function)); + }); + + test('should not add a handler if the annotation mode is set to none', () => { + const wrapper = getWrapper({ hasHighlight: true, hasRegion: true }); + expect(document.addEventListener).not.toBeCalledWith('keydown', expect.any(Function)); + + wrapper.unmount(); + expect(document.removeEventListener).toBeCalledWith('keydown', expect.any(Function)); + }); + }); + + describe('event handlers', () => { + test.each` + current | selector | result + ${AnnotationMode.NONE} | ${'bp-AnnotationsControls-regionBtn'} | ${AnnotationMode.REGION} + ${AnnotationMode.REGION} | ${'bp-AnnotationsControls-regionBtn'} | ${AnnotationMode.NONE} + ${AnnotationMode.REGION} | ${'bp-AnnotationsControls-highlightBtn'} | ${AnnotationMode.HIGHLIGHT} + ${AnnotationMode.NONE} | ${'bp-AnnotationsControls-highlightBtn'} | ${AnnotationMode.HIGHLIGHT} + `('in $current mode returns $result when $selector is clicked', ({ current, result, selector }) => { + const onClick = jest.fn(); + const element = getElement({ + annotationMode: current, + hasHighlight: true, + hasRegion: true, + onAnnotationModeClick: onClick, + }); + + element.find(`button[data-testid="${selector}"]`).simulate('click'); + + expect(onClick).toBeCalledWith({ mode: result }); + }); + + test('should invoke the escape callback if the escape key is pressed while in a mode', () => { + const onEscape = jest.fn(); + + getWrapper({ + annotationMode: AnnotationMode.REGION, + hasHighlight: true, + hasRegion: true, + onAnnotationModeEscape: onEscape, + }); + + act(() => { + document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' })); + }); + + expect(onEscape).toBeCalled(); + }); + + test('should not invoke the escape callback if any key other than escape is pressed', () => { + const onEscape = jest.fn(); + + getWrapper({ + annotationMode: AnnotationMode.REGION, + hasHighlight: true, + hasRegion: true, + onAnnotationModeEscape: onEscape, + }); + + act(() => { + document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' })); + }); + + expect(onEscape).not.toBeCalled(); + }); + }); + describe('render', () => { + test('should return nothing if no mode is enabled', () => { + const wrapper = getWrapper(); + + expect(wrapper.isEmptyRender()).toBe(true); + }); + test('should return a valid wrapper', () => { - const onClick = jest.fn(); - const wrapper = shallow(); + const element = getElement({ hasHighlight: true, hasRegion: true }); - expect(wrapper.hasClass('bp-AnnotationsControls')).toBe(true); + expect(element.hasClass('bp-AnnotationsControls')).toBe(true); }); }); }); diff --git a/src/lib/viewers/doc/DocBaseViewer.js b/src/lib/viewers/doc/DocBaseViewer.js index 62f453f896..5babd6e61f 100644 --- a/src/lib/viewers/doc/DocBaseViewer.js +++ b/src/lib/viewers/doc/DocBaseViewer.js @@ -1073,6 +1073,7 @@ class DocBaseViewer extends BaseViewer { maxScale={MAX_SCALE} minScale={MIN_SCALE} onAnnotationModeClick={this.handleAnnotationControlsClick} + onAnnotationModeEscape={this.handleAnnotationControlsEscape} onFindBarToggle={this.toggleFindBar} onFullscreenToggle={this.toggleFullscreen} onThumbnailsToggle={this.toggleThumbnails} diff --git a/src/lib/viewers/doc/DocControls.tsx b/src/lib/viewers/doc/DocControls.tsx index 16a3e859ab..19d9fd3f5b 100644 --- a/src/lib/viewers/doc/DocControls.tsx +++ b/src/lib/viewers/doc/DocControls.tsx @@ -19,6 +19,7 @@ export default function DocControls({ maxScale, minScale, onAnnotationModeClick, + onAnnotationModeEscape, onFindBarToggle, onFullscreenToggle, onThumbnailsToggle, @@ -44,6 +45,7 @@ export default function DocControls({ hasHighlight={hasHighlight} hasRegion={hasRegion} onAnnotationModeClick={onAnnotationModeClick} + onAnnotationModeEscape={onAnnotationModeEscape} /> ); diff --git a/src/lib/viewers/doc/__tests__/DocBaseViewer-test.js b/src/lib/viewers/doc/__tests__/DocBaseViewer-test.js index 8c652bd962..2a0a25be55 100644 --- a/src/lib/viewers/doc/__tests__/DocBaseViewer-test.js +++ b/src/lib/viewers/doc/__tests__/DocBaseViewer-test.js @@ -1704,6 +1704,7 @@ describe('src/lib/viewers/doc/DocBaseViewer', () => { maxScale={10} minScale={0.1} onAnnotationModeClick={docBase.handleAnnotationControlsClick} + onAnnotationModeEscape={docBase.handleAnnotationControlsEscape} onFindBarToggle={docBase.toggleFindBar} onFullscreenToggle={docBase.toggleFullscreen} onThumbnailsToggle={docBase.toggleThumbnails} diff --git a/src/lib/viewers/image/ImageControls.tsx b/src/lib/viewers/image/ImageControls.tsx index bb243649eb..87822c481a 100644 --- a/src/lib/viewers/image/ImageControls.tsx +++ b/src/lib/viewers/image/ImageControls.tsx @@ -9,10 +9,10 @@ export type Props = AnnotationsControlsProps & FullscreenToggleProps & RotateCon export default function ImageControls({ annotationMode, - fileId, hasHighlight, hasRegion, onAnnotationModeClick, + onAnnotationModeEscape, onFullscreenToggle, onRotateLeft, onZoomIn, @@ -26,10 +26,10 @@ export default function ImageControls({ ); diff --git a/src/lib/viewers/image/ImageViewer.js b/src/lib/viewers/image/ImageViewer.js index 3370bcb767..1382ded55d 100644 --- a/src/lib/viewers/image/ImageViewer.js +++ b/src/lib/viewers/image/ImageViewer.js @@ -403,6 +403,7 @@ class ImageViewer extends ImageBaseViewer { hasHighlight={false} hasRegion={canAnnotate} onAnnotationModeClick={this.handleAnnotationControlsClick} + onAnnotationModeEscape={this.handleAnnotationControlsEscape} onFullscreenToggle={this.toggleFullscreen} onRotateLeft={this.rotateLeft} onZoomIn={this.zoomIn} diff --git a/src/lib/viewers/image/__tests__/ImageViewer-test.js b/src/lib/viewers/image/__tests__/ImageViewer-test.js index c7f376ef45..9965cecc5c 100644 --- a/src/lib/viewers/image/__tests__/ImageViewer-test.js +++ b/src/lib/viewers/image/__tests__/ImageViewer-test.js @@ -427,6 +427,7 @@ describe('lib/viewers/image/ImageViewer', () => { hasHighlight={false} hasRegion={false} onAnnotationModeClick={image.handleAnnotationControlsClick} + onAnnotationModeEscape={image.handleAnnotationControlsEscape} onFullscreenToggle={image.toggleFullscreen} onRotateLeft={image.rotateLeft} onZoomIn={image.zoomIn}