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}