From 8a7e630a80cf670cde81fdaf6eb1caefedb6d6c3 Mon Sep 17 00:00:00 2001 From: Jared Stoffan Date: Fri, 6 Nov 2020 13:40:13 -0800 Subject: [PATCH 1/6] feat(controls): Add react versions of annotations controls --- package.json | 2 + src/lib/AnnotationControlsFSM.ts | 2 + src/lib/viewers/BaseViewer.js | 16 ++++- src/lib/viewers/__tests__/BaseViewer-test.js | 5 +- .../annotations/AnnotationsButton.tsx | 44 +++++++++++++ .../annotations/AnnotationsControls.scss | 28 ++++++++ .../annotations/AnnotationsControls.tsx | 65 +++++++++++++++++++ .../__tests__/AnnotationsControls-test.tsx | 14 ++++ src/lib/viewers/controls/annotations/index.ts | 2 + src/lib/viewers/controls/annotations/types.ts | 6 ++ .../controls/fullscreen/FullscreenToggle.tsx | 17 +---- .../__tests__/FullscreenToggle-test.tsx | 35 ++++++---- .../hooks/__tests__/useFullscreen-test.tsx | 36 ++++++++++ src/lib/viewers/controls/hooks/index.ts | 2 + .../viewers/controls/hooks/useFullscreen.ts | 21 ++++++ .../controls/icons/IconHighlightText16.tsx | 4 +- .../viewers/controls/icons/IconRegion24.tsx | 4 +- src/lib/viewers/doc/DocBaseViewer.js | 10 +++ src/lib/viewers/doc/DocControls.tsx | 20 +++++- .../doc/__tests__/DocBaseViewer-test.js | 4 ++ src/lib/viewers/image/ImageControls.tsx | 16 ++++- src/lib/viewers/image/ImageViewer.js | 12 ++++ .../image/__tests__/ImageViewer-test.js | 5 ++ yarn.lock | 10 +++ 24 files changed, 339 insertions(+), 41 deletions(-) create mode 100644 src/lib/viewers/controls/annotations/AnnotationsButton.tsx create mode 100644 src/lib/viewers/controls/annotations/AnnotationsControls.scss create mode 100644 src/lib/viewers/controls/annotations/AnnotationsControls.tsx create mode 100644 src/lib/viewers/controls/annotations/__tests__/AnnotationsControls-test.tsx create mode 100644 src/lib/viewers/controls/annotations/index.ts create mode 100644 src/lib/viewers/controls/annotations/types.ts create mode 100644 src/lib/viewers/controls/hooks/__tests__/useFullscreen-test.tsx create mode 100644 src/lib/viewers/controls/hooks/index.ts create mode 100644 src/lib/viewers/controls/hooks/useFullscreen.ts diff --git a/package.json b/package.json index e3852409d..d10ad863e 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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", diff --git a/src/lib/AnnotationControlsFSM.ts b/src/lib/AnnotationControlsFSM.ts index 3ff6a1d86..b6ed824c9 100644 --- a/src/lib/AnnotationControlsFSM.ts +++ b/src/lib/AnnotationControlsFSM.ts @@ -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 => { diff --git a/src/lib/viewers/BaseViewer.js b/src/lib/viewers/BaseViewer.js index f2e1f375f..9533981b6 100644 --- a/src/lib/viewers/BaseViewer.js +++ b/src/lib/viewers/BaseViewer.js @@ -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)); } @@ -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 diff --git a/src/lib/viewers/__tests__/BaseViewer-test.js b/src/lib/viewers/__tests__/BaseViewer-test.js index f450e0b59..ec16bcf9f 100644 --- a/src/lib/viewers/__tests__/BaseViewer-test.js +++ b/src/lib/viewers/__tests__/BaseViewer-test.js @@ -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(); diff --git a/src/lib/viewers/controls/annotations/AnnotationsButton.tsx b/src/lib/viewers/controls/annotations/AnnotationsButton.tsx new file mode 100644 index 000000000..79102959f --- /dev/null +++ b/src/lib/viewers/controls/annotations/AnnotationsButton.tsx @@ -0,0 +1,44 @@ +import React, { ButtonHTMLAttributes } from 'react'; +import classNames from 'classnames'; +import noop from 'lodash/noop'; +import { AnnotationMode } from './types'; +import './AnnotationsControls.scss'; + +export type Props = Omit, 'onClick'> & { + children?: React.ReactNode; + className?: string; + isActive?: boolean; + isEnabled?: boolean; + fileId: string; + onClick?: (mode: AnnotationMode) => void; + mode: AnnotationMode; +}; + +export default function AnnotationsButton({ + children, + className, + isEnabled = true, + isActive = false, + fileId, + onClick = noop, + mode, + ...rest +}: Props): JSX.Element | null { + if (!isEnabled) { + return null; + } + + return ( + + ); +} diff --git a/src/lib/viewers/controls/annotations/AnnotationsControls.scss b/src/lib/viewers/controls/annotations/AnnotationsControls.scss new file mode 100644 index 000000000..d8c580bb4 --- /dev/null +++ b/src/lib/viewers/controls/annotations/AnnotationsControls.scss @@ -0,0 +1,28 @@ +@import '~box-ui-elements/es/styles/variables'; +@import '../styles'; + +.bp-AnnotationsControls { + @include bp-ControlGroup; + + padding-left: 4px; + border-left: 1px solid $twos; +} + +.bp-AnnotationsControls-button { + @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; + } + } +} diff --git a/src/lib/viewers/controls/annotations/AnnotationsControls.tsx b/src/lib/viewers/controls/annotations/AnnotationsControls.tsx new file mode 100644 index 000000000..2124e1d4d --- /dev/null +++ b/src/lib/viewers/controls/annotations/AnnotationsControls.tsx @@ -0,0 +1,65 @@ +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; + fileId: string; + onAnnotationModeClick?: ({ mode }: { mode: AnnotationMode }) => void; + hasHighlight?: boolean; + hasRegion?: boolean; +}; + +export default function AnnotationsControls({ + annotationMode = AnnotationMode.NONE, + fileId, + onAnnotationModeClick = noop, + hasHighlight = true, + hasRegion = true, +}: Props): JSX.Element | null { + const isFullscreen = useFullscreen(); + const showHighlight = !isFullscreen && hasHighlight; + const showRegion = !isFullscreen && hasRegion; + + if (!showHighlight && !showRegion) { + return null; + } + + const handleModeClick = (mode: AnnotationMode): void => { + onAnnotationModeClick({ mode: annotationMode === mode ? AnnotationMode.NONE : mode }); + }; + + return ( +
+ + + + + + +
+ ); +} diff --git a/src/lib/viewers/controls/annotations/__tests__/AnnotationsControls-test.tsx b/src/lib/viewers/controls/annotations/__tests__/AnnotationsControls-test.tsx new file mode 100644 index 000000000..d16f941b2 --- /dev/null +++ b/src/lib/viewers/controls/annotations/__tests__/AnnotationsControls-test.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import AnnotationsControls from '../AnnotationsControls'; + +describe('AnnotationsControls', () => { + describe('render', () => { + test('should return a valid wrapper', () => { + const onClick = jest.fn(); + const wrapper = shallow(); + + expect(wrapper.hasClass('bp-AnnotationsControls')).toBe(true); + }); + }); +}); diff --git a/src/lib/viewers/controls/annotations/index.ts b/src/lib/viewers/controls/annotations/index.ts new file mode 100644 index 000000000..609c00fbd --- /dev/null +++ b/src/lib/viewers/controls/annotations/index.ts @@ -0,0 +1,2 @@ +export * from './AnnotationsControls'; +export { default } from './AnnotationsControls'; diff --git a/src/lib/viewers/controls/annotations/types.ts b/src/lib/viewers/controls/annotations/types.ts new file mode 100644 index 000000000..50b299c67 --- /dev/null +++ b/src/lib/viewers/controls/annotations/types.ts @@ -0,0 +1,6 @@ +// eslint-disable-next-line import/prefer-default-export +export enum AnnotationMode { + HIGHLIGHT = 'highlight', + NONE = 'none', + REGION = 'region', +} diff --git a/src/lib/viewers/controls/fullscreen/FullscreenToggle.tsx b/src/lib/viewers/controls/fullscreen/FullscreenToggle.tsx index b6109093c..55704e822 100644 --- a/src/lib/viewers/controls/fullscreen/FullscreenToggle.tsx +++ b/src/lib/viewers/controls/fullscreen/FullscreenToggle.tsx @@ -1,7 +1,7 @@ import React from 'react'; -import fullscreen from '../../../Fullscreen'; import IconFullscreenIn24 from '../icons/IconFullscreenIn24'; import IconFullscreenOut24 from '../icons/IconFullscreenOut24'; +import useFullscreen from '../hooks/useFullscreen'; import './FullscreenToggle.scss'; export type Props = { @@ -9,7 +9,7 @@ export type Props = { }; export default function FullscreenToggle({ onFullscreenToggle }: Props): JSX.Element { - const [isFullscreen, setFullscreen] = React.useState(false); + const isFullscreen = useFullscreen(); const Icon = isFullscreen ? IconFullscreenOut24 : IconFullscreenIn24; const title = isFullscreen ? __('exit_fullscreen') : __('enter_fullscreen'); @@ -17,19 +17,6 @@ export default function FullscreenToggle({ onFullscreenToggle }: Props): JSX.Ele onFullscreenToggle(!isFullscreen); }; - React.useEffect(() => { - const handleFullscreenEnter = (): void => setFullscreen(true); - const handleFullscreenExit = (): void => setFullscreen(false); - - fullscreen.addListener('enter', handleFullscreenEnter); - fullscreen.addListener('exit', handleFullscreenExit); - - return (): void => { - fullscreen.removeListener('enter', handleFullscreenEnter); - fullscreen.removeListener('exit', handleFullscreenExit); - }; - }, []); - return (