Skip to content

Commit

Permalink
feat(controls): Add react version of annotations controls (#1291)
Browse files Browse the repository at this point in the history
  • Loading branch information
jstoffan authored Nov 10, 2020
1 parent 9426404 commit 06c9084
Show file tree
Hide file tree
Showing 31 changed files with 517 additions and 65 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"@commitlint/config-conventional": "^8.2.0",
"@commitlint/travis-cli": "^8.2.0",
"@testing-library/jest-dom": "^5.11.4",
"@types/classnames": "^2.2.11",
"@types/enzyme": "^3.10.8",
"@types/lodash": "^4.14.149",
"@types/react": "^16.9.0",
Expand All @@ -35,6 +36,7 @@
"box-annotations": "^2.3.0",
"box-ui-elements": "^12.0.0-beta.10",
"chai": "^4.2.0",
"classnames": "^2.2.6",
"conventional-changelog-cli": "^2.0.28",
"conventional-github-releaser": "^3.1.3",
"create-react-class": "^15.6.2",
Expand Down
2 changes: 2 additions & 0 deletions src/lib/AnnotationControlsFSM.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ export default class AnnotationControlsFSM {
this.currentState = initialState;
}

public getMode = (): AnnotationMode => stateModeMap[this.currentState];

public getState = (): AnnotationState => this.currentState;

public subscribe = (callback: Subscription): void => {
Expand Down
7 changes: 7 additions & 0 deletions src/lib/__tests__/AnnotationControlsFSM-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
Expand All @@ -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);
});
});
Expand All @@ -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);
});
});
Expand Down Expand Up @@ -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);
});
});
Expand Down Expand Up @@ -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);
});
});
Expand Down Expand Up @@ -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);
});
});
Expand Down Expand Up @@ -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);
});
});
Expand Down
16 changes: 13 additions & 3 deletions src/lib/viewers/BaseViewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -916,10 +916,18 @@ class BaseViewer extends EventEmitter {
//--------------------------------------------------------------------------

disableAnnotationControls() {
if (this.annotator && this.annotationControls && this.areNewAnnotationsEnabled()) {
if (!this.areNewAnnotationsEnabled()) {
return;
}

if (this.annotator) {
this.annotator.toggleAnnotationMode(AnnotationMode.NONE);
}

if (this.annotationControls) {
this.annotationControls.toggle(false);
}

this.processAnnotationModeChange(this.annotationControlsFSM.transition(AnnotationInput.RESET));
}

Expand Down Expand Up @@ -1090,11 +1098,13 @@ class BaseViewer extends EventEmitter {
* @param {AnnotationMode} mode Next annotation mode
*/
processAnnotationModeChange = mode => {
if (!this.annotationControls) {
if (!this.areNewAnnotationsEnabled()) {
return;
}

this.annotationControls.setMode(mode);
if (this.annotationControls) {
this.annotationControls.setMode(mode);
}

if (this.containerEl) {
// Remove all annotations create related classes
Expand Down
5 changes: 3 additions & 2 deletions src/lib/viewers/__tests__/BaseViewer-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1776,13 +1776,14 @@ describe('lib/viewers/BaseViewer', () => {
setMode: jest.fn(),
};
base.containerEl = document.createElement('div');
base.areNewAnnotationsEnabled = jest.fn().mockReturnValue(true);
});

test('should do nothing if no annotationControls', () => {
test('should do nothing if new annotations are not enabled', () => {
jest.spyOn(base.containerEl.classList, 'add');
jest.spyOn(base.containerEl.classList, 'remove');

base.annotationControls = undefined;
base.areNewAnnotationsEnabled.mockReturnValue(false);
base.processAnnotationModeChange(AnnotationMode.REGION);

expect(base.containerEl.classList.add).not.toBeCalled();
Expand Down
21 changes: 21 additions & 0 deletions src/lib/viewers/controls/annotations/AnnotationsButton.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
@import '~box-ui-elements/es/styles/variables';
@import '../styles';

.bp-AnnotationsButton {
@include bp-ControlButton;

svg {
width: 34px;
height: 32px;
padding: 4px 5px;
border-radius: 4px;
fill: $white;
}

&.bp-is-active {
svg {
background-color: $white;
fill: $black;
}
}
}
41 changes: 41 additions & 0 deletions src/lib/viewers/controls/annotations/AnnotationsButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React, { ButtonHTMLAttributes } from 'react';
import classNames from 'classnames';
import noop from 'lodash/noop';
import { AnnotationMode } from './types';
import './AnnotationsButton.scss';

export type Props = Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'onClick'> & {
children?: React.ReactNode;
className?: string;
isActive?: boolean;
isEnabled?: boolean;
mode: AnnotationMode;
onClick?: (mode: AnnotationMode) => void;
};

export default function AnnotationsButton({
children,
className,
isActive = false,
isEnabled = true,
mode,
onClick = noop,
...rest
}: Props): JSX.Element | null {
if (!isEnabled) {
return null;
}

return (
<button
className={classNames('bp-AnnotationsButton', className, {
'bp-is-active': isActive,
})}
onClick={(): void => onClick(mode)}
type="button"
{...rest}
>
{children}
</button>
);
}
9 changes: 9 additions & 0 deletions src/lib/viewers/controls/annotations/AnnotationsControls.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
@import '~box-ui-elements/es/styles/variables';
@import '../styles';

.bp-AnnotationsControls {
@include bp-ControlGroup;

padding-left: 4px;
border-left: 1px solid $twos;
}
87 changes: 87 additions & 0 deletions src/lib/viewers/controls/annotations/AnnotationsControls.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import React from 'react';
import noop from 'lodash/noop';
import AnnotationsButton from './AnnotationsButton';
import IconHighlightText16 from '../icons/IconHighlightText16';
import IconRegion24 from '../icons/IconRegion24';
import useFullscreen from '../hooks/useFullscreen';
import { AnnotationMode } from './types';
import './AnnotationsControls.scss';

export type Props = {
annotationMode?: AnnotationMode;
hasHighlight?: boolean;
hasRegion?: boolean;
onAnnotationModeClick?: ({ mode }: { mode: AnnotationMode }) => void;
onAnnotationModeEscape?: () => void;
};

export default function AnnotationsControls({
annotationMode = AnnotationMode.NONE,
hasHighlight = false,
hasRegion = false,
onAnnotationModeClick = noop,
onAnnotationModeEscape = noop,
}: Props): JSX.Element | null {
const isFullscreen = useFullscreen();
const showHighlight = !isFullscreen && hasHighlight;
const showRegion = !isFullscreen && hasRegion;

// 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 (
<div className="bp-AnnotationsControls">
<AnnotationsButton
data-resin-target="highlightRegion"
data-testid="bp-AnnotationsControls-regionBtn"
isActive={annotationMode === AnnotationMode.REGION}
isEnabled={showRegion}
mode={AnnotationMode.REGION}
onClick={handleModeClick}
title={__('region_comment')}
>
<IconRegion24 />
</AnnotationsButton>
<AnnotationsButton
data-resin-target="highlightText"
data-testid="bp-AnnotationsControls-highlightBtn"
isActive={annotationMode === AnnotationMode.HIGHLIGHT}
isEnabled={showHighlight}
mode={AnnotationMode.HIGHLIGHT}
onClick={handleModeClick}
title={__('highlight_text')}
>
<IconHighlightText16 />
</AnnotationsButton>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React from 'react';
import { shallow, ShallowWrapper } from 'enzyme';
import AnnotationsButton from '../AnnotationsButton';
import { AnnotationMode } from '../types';

describe('AnnotationsButton', () => {
const getWrapper = (props = {}): ShallowWrapper =>
shallow(
<AnnotationsButton mode={AnnotationMode.REGION} onClick={jest.fn()} {...props}>
Test
</AnnotationsButton>,
);

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');
});
});
});
Loading

0 comments on commit 06c9084

Please sign in to comment.