From d030fe3fffc1b376ab72c88c0ae42cb91605b0f6 Mon Sep 17 00:00:00 2001 From: megansmith-box <75504287+megansmith-box@users.noreply.github.com> Date: Tue, 4 May 2021 11:44:43 -0700 Subject: [PATCH] feat(annotations): tooltip (#1364) * feat(annotations): tooltip * feat(annotations): tooltip * feat(annotations): tooltip * feat(annotations): tooltip * feat(annotations): tooltip * feat(annotations): tooltip * feat(annotations): tooltip --- package.json | 2 +- src/i18n/en-US.properties | 4 ++ src/lib/Preview.js | 13 ++++ src/lib/types/buie.ts | 2 + src/lib/types/index.ts | 1 + src/lib/types/targeting.ts | 10 +++ .../annotations/AnnotationsControls.tsx | 25 +++---- .../AnnotationsTargetedTooltip.scss | 14 ++++ .../AnnotationsTargetedTooltip.tsx | 65 +++++++++++++++++++ .../AnnotationsTargetedTooltip-test.tsx | 48 ++++++++++++++ .../controls/controls-layer/ControlsLayer.tsx | 27 +++++--- .../controls-layer/ControlsLayerContext.ts | 10 +++ .../viewers/controls/controls-layer/index.ts | 1 + .../experiences/ExperiencesContext.ts | 10 +++ .../experiences/ExperiencesProvider.tsx | 11 ++++ src/lib/viewers/controls/experiences/index.ts | 3 + .../tooltip/TargetedClickThroughTooltip.tsx | 11 ++++ src/lib/viewers/controls/tooltip/index.ts | 2 + src/lib/viewers/doc/DocBaseViewer.js | 15 +++++ src/lib/viewers/doc/DocControls.tsx | 7 +- src/lib/viewers/image/ImageControls.tsx | 7 +- src/lib/viewers/image/ImageViewer.js | 15 +++++ yarn.lock | 2 +- 23 files changed, 280 insertions(+), 25 deletions(-) create mode 100644 src/lib/types/targeting.ts create mode 100644 src/lib/viewers/controls/annotations/AnnotationsTargetedTooltip.scss create mode 100644 src/lib/viewers/controls/annotations/AnnotationsTargetedTooltip.tsx create mode 100644 src/lib/viewers/controls/annotations/__tests__/AnnotationsTargetedTooltip-test.tsx create mode 100644 src/lib/viewers/controls/controls-layer/ControlsLayerContext.ts create mode 100644 src/lib/viewers/controls/experiences/ExperiencesContext.ts create mode 100644 src/lib/viewers/controls/experiences/ExperiencesProvider.tsx create mode 100644 src/lib/viewers/controls/experiences/index.ts create mode 100644 src/lib/viewers/controls/tooltip/TargetedClickThroughTooltip.tsx create mode 100644 src/lib/viewers/controls/tooltip/index.ts diff --git a/package.json b/package.json index 2176dc35b..d293b6c6d 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,7 @@ "react": "^17.0.1", "react-dom": "^17.0.1", "react-intl": "^2.9.0", - "react-tether": "^1.0.0", + "react-tether": "^1.0.5", "react-virtualized": "^9.22.3", "sass-loader": "^7.1.0", "sinon": "^9.0.3", diff --git a/src/i18n/en-US.properties b/src/i18n/en-US.properties index 4651130bd..7ae81f135 100644 --- a/src/i18n/en-US.properties +++ b/src/i18n/en-US.properties @@ -212,6 +212,10 @@ annotations_create_error=We’re sorry, the annotation could not be created. annotations_delete_error=We’re sorry, the annotation could not be deleted. # Text for when the authorization token is invalid annotations_authorization_error=Your session has expired. Please refresh the page. +# Text for Tooltip content showing new users about annotations experience +annotations_tooltip_body=Make notes and comments to highlight key information when previewing content. +# Text for Tooltip title showing new users about annotations experience +annotations_tooltip_title=Add feedback on content # Notifications # Default text for notification button that dismisses notification diff --git a/src/lib/Preview.js b/src/lib/Preview.js index ee0c7ab11..ff3043cb1 100644 --- a/src/lib/Preview.js +++ b/src/lib/Preview.js @@ -1783,6 +1783,19 @@ class Preview extends EventEmitter { ); } + /** + * Updates experiences option after props have changed in parent app + * + * @public + * @param {Object} experiences - new experiences value + * @return {void} + */ + updateExperiences(experiences) { + if (this.viewer && this.viewer.updateExperiences) { + this.viewer.updateExperiences(experiences); + } + } + /** * Shows a preview of a file at the specified index in the current collection. * diff --git a/src/lib/types/buie.ts b/src/lib/types/buie.ts index 6e633ae2d..6adf12027 100644 --- a/src/lib/types/buie.ts +++ b/src/lib/types/buie.ts @@ -1,2 +1,4 @@ declare module 'box-ui-elements/es/components/preview'; +declare module 'box-ui-elements/es/components/tooltip/Tooltip'; +declare module 'box-ui-elements/es/features/targeting/hocs'; declare module 'box-ui-elements/es/styles/variables'; diff --git a/src/lib/types/index.ts b/src/lib/types/index.ts index 6e3360112..b46062b61 100644 --- a/src/lib/types/index.ts +++ b/src/lib/types/index.ts @@ -1 +1,2 @@ export * from './annotations'; +export * from './targeting'; diff --git a/src/lib/types/targeting.ts b/src/lib/types/targeting.ts new file mode 100644 index 000000000..4825c1d1e --- /dev/null +++ b/src/lib/types/targeting.ts @@ -0,0 +1,10 @@ +export type Experiences = { + [key: string]: TargetingApi; +}; + +export type TargetingApi = { + canShow: boolean; + onClose: () => void; + onComplete: () => void; + onShow: () => void; +}; diff --git a/src/lib/viewers/controls/annotations/AnnotationsControls.tsx b/src/lib/viewers/controls/annotations/AnnotationsControls.tsx index cae5e401b..b8d80d66e 100644 --- a/src/lib/viewers/controls/annotations/AnnotationsControls.tsx +++ b/src/lib/viewers/controls/annotations/AnnotationsControls.tsx @@ -2,6 +2,7 @@ import React from 'react'; import noop from 'lodash/noop'; import { bdlBoxBlue } from 'box-ui-elements/es/styles/variables'; import AnnotationsButton from './AnnotationsButton'; +import AnnotationsTargetedTooltip from './AnnotationsTargetedTooltip'; import IconDrawing24 from '../icons/IconDrawing24'; import IconHighlightText16 from '../icons/IconHighlightText16'; import IconRegion24 from '../icons/IconRegion24'; @@ -80,17 +81,19 @@ export default function AnnotationsControls({ > - - - + + + + + ; + +function AnnotationsTargetedTooltip({ children, isEnabled = false }: Props): JSX.Element | null { + const { experiences } = React.useContext(ExperiencesContext); + const { setIsForced } = React.useContext(ControlsLayerContext); + const [wasClosedByUser, setWasClosedByUser] = React.useState(false); + + const shouldTarget = !!( + isEnabled && + experiences && + experiences.tooltipFlowAnnotationsExperience && + experiences.tooltipFlowAnnotationsExperience.canShow + ); + + if (!shouldTarget) { + return <>{children}; + } + + return ( + +

{__('annotations_tooltip_title')}

+

{__('annotations_tooltip_body')}

+ + } + theme="callout" + useTargetingApi={(): TargetingApi => { + return { + ...experiences.tooltipFlowAnnotationsExperience, + onClose: (): void => { + experiences.tooltipFlowAnnotationsExperience.onClose(); + setIsForced(false); + }, + onShow: (): void => { + experiences.tooltipFlowAnnotationsExperience.onShow(); + + if (wasClosedByUser) { + return; + } + + setWasClosedByUser(true); + setIsForced(true); + }, + }; + }} + > + {children} +
+ ); +} + +export default AnnotationsTargetedTooltip; diff --git a/src/lib/viewers/controls/annotations/__tests__/AnnotationsTargetedTooltip-test.tsx b/src/lib/viewers/controls/annotations/__tests__/AnnotationsTargetedTooltip-test.tsx new file mode 100644 index 000000000..9a5fdcdc7 --- /dev/null +++ b/src/lib/viewers/controls/annotations/__tests__/AnnotationsTargetedTooltip-test.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { ReactWrapper, mount } from 'enzyme'; +import AnnotationsTargetedTooltip from '../AnnotationsTargetedTooltip'; + +describe('AnnotationsTargetedTooltip', () => { + const getWrapper = (props = {}): ReactWrapper => + mount( + +
Child
+
, + ); + + describe('render', () => { + beforeEach(() => { + jest.spyOn(React, 'useContext').mockImplementation(() => ({ + experiences: { + tooltipFlowAnnotationsExperience: { + canShow: true, + onClose: jest.fn(), + onComplete: jest.fn(), + onShow: jest.fn(), + }, + }, + setIsForced: jest.fn(), + })); + }); + + test('should return tooltip when is enabled', () => { + const wrapper = getWrapper({ + isEnabled: true, + }); + + expect(wrapper.children().text()).not.toBe('Child'); + expect(wrapper.children().prop('shouldTarget')).toBe(true); + expect(wrapper.children().prop('theme')).toBe('callout'); + expect(wrapper.children().prop('useTargetingApi')().canShow).toBe(true); + }); + + test('should return children when tooltip is disabled', () => { + const wrapper = getWrapper({ + isEnabled: false, + }); + + expect(wrapper.children().text()).toBe('Child'); + expect(wrapper.children().prop('shouldTarget')).toBe(undefined); + }); + }); +}); diff --git a/src/lib/viewers/controls/controls-layer/ControlsLayer.tsx b/src/lib/viewers/controls/controls-layer/ControlsLayer.tsx index 26ee0fb40..e04ad39f9 100644 --- a/src/lib/viewers/controls/controls-layer/ControlsLayer.tsx +++ b/src/lib/viewers/controls/controls-layer/ControlsLayer.tsx @@ -1,5 +1,6 @@ import React from 'react'; import noop from 'lodash/noop'; +import ControlsLayerContext from './ControlsLayerContext'; import './ControlsLayer.scss'; export type Helpers = { @@ -17,6 +18,7 @@ export const HIDE_DELAY_MS = 2000; export const SHOW_CLASSNAME = 'bp-is-visible'; export default function ControlsLayer({ children, onMount = noop }: Props): JSX.Element { + const [isForced, setIsForced] = React.useState(false); const [isShown, setIsShown] = React.useState(false); const hasFocusRef = React.useRef(false); const hasCursorRef = React.useRef(false); @@ -78,14 +80,23 @@ export default function ControlsLayer({ children, onMount = noop }: Props): JSX. React.useEffect(() => helpersRef.current.clean, []); return ( -
{ + helpersRef.current.reset(); + setIsForced(value); + }, + }} > - {children} -
+
+ {children} +
+ ); } diff --git a/src/lib/viewers/controls/controls-layer/ControlsLayerContext.ts b/src/lib/viewers/controls/controls-layer/ControlsLayerContext.ts new file mode 100644 index 000000000..e017c0a44 --- /dev/null +++ b/src/lib/viewers/controls/controls-layer/ControlsLayerContext.ts @@ -0,0 +1,10 @@ +import React from 'react'; +import noop from 'lodash/noop'; + +export type Context = { + setIsForced: (isForced: boolean) => void; +}; + +export default React.createContext({ + setIsForced: noop, +}); diff --git a/src/lib/viewers/controls/controls-layer/index.ts b/src/lib/viewers/controls/controls-layer/index.ts index 269a31a04..fbe052c23 100644 --- a/src/lib/viewers/controls/controls-layer/index.ts +++ b/src/lib/viewers/controls/controls-layer/index.ts @@ -1,2 +1,3 @@ export * from './ControlsLayer'; export { default } from './ControlsLayer'; +export { default as ControlsLayerContext } from './ControlsLayerContext'; diff --git a/src/lib/viewers/controls/experiences/ExperiencesContext.ts b/src/lib/viewers/controls/experiences/ExperiencesContext.ts new file mode 100644 index 000000000..e507290e5 --- /dev/null +++ b/src/lib/viewers/controls/experiences/ExperiencesContext.ts @@ -0,0 +1,10 @@ +import React from 'react'; +import { Experiences } from '../../../types'; + +export type Context = { + experiences: Experiences; +}; + +export default React.createContext({ + experiences: {}, +}); diff --git a/src/lib/viewers/controls/experiences/ExperiencesProvider.tsx b/src/lib/viewers/controls/experiences/ExperiencesProvider.tsx new file mode 100644 index 000000000..f02ee0093 --- /dev/null +++ b/src/lib/viewers/controls/experiences/ExperiencesProvider.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import ExperiencesContext from './ExperiencesContext'; +import { Experiences } from '../../../types'; + +export type Props = React.PropsWithChildren<{ + experiences?: Experiences; +}>; + +export default function ExperiencesProvider({ children, experiences = {} }: Props): JSX.Element { + return {children}; +} diff --git a/src/lib/viewers/controls/experiences/index.ts b/src/lib/viewers/controls/experiences/index.ts new file mode 100644 index 000000000..d16742e77 --- /dev/null +++ b/src/lib/viewers/controls/experiences/index.ts @@ -0,0 +1,3 @@ +export * from './ExperiencesProvider'; +export { default } from './ExperiencesProvider'; +export { default as ExperiencesContext } from './ExperiencesContext'; diff --git a/src/lib/viewers/controls/tooltip/TargetedClickThroughTooltip.tsx b/src/lib/viewers/controls/tooltip/TargetedClickThroughTooltip.tsx new file mode 100644 index 000000000..d7defb9e8 --- /dev/null +++ b/src/lib/viewers/controls/tooltip/TargetedClickThroughTooltip.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import Tooltip from 'box-ui-elements/es/components/tooltip/Tooltip'; +import { withTargetedClickThrough } from 'box-ui-elements/es/features/targeting/hocs'; + +const TargetedClickThroughTooltip = withTargetedClickThrough( + ({ children, ...props }: { children: React.ReactNode }): JSX.Element => { + return {children}; + }, +); + +export default TargetedClickThroughTooltip; diff --git a/src/lib/viewers/controls/tooltip/index.ts b/src/lib/viewers/controls/tooltip/index.ts new file mode 100644 index 000000000..122ef5493 --- /dev/null +++ b/src/lib/viewers/controls/tooltip/index.ts @@ -0,0 +1,2 @@ +export { default } from './TargetedClickThroughTooltip'; +export * from './TargetedClickThroughTooltip'; diff --git a/src/lib/viewers/doc/DocBaseViewer.js b/src/lib/viewers/doc/DocBaseViewer.js index d2100e4fe..13f099a88 100644 --- a/src/lib/viewers/doc/DocBaseViewer.js +++ b/src/lib/viewers/doc/DocBaseViewer.js @@ -119,6 +119,7 @@ class DocBaseViewer extends BaseViewer { this.throttledScrollHandler = this.getScrollHandler().bind(this); this.toggleFindBar = this.toggleFindBar.bind(this); this.toggleThumbnails = this.toggleThumbnails.bind(this); + this.updateExperiences = this.updateExperiences.bind(this); this.updateDiscoverabilityResinTag = this.updateDiscoverabilityResinTag.bind(this); this.zoomIn = this.zoomIn.bind(this); this.zoomOut = this.zoomOut.bind(this); @@ -1045,6 +1046,19 @@ class DocBaseViewer extends BaseViewer { this.renderUI(); } + /** + * Updates experiences option after props have changed in parent app + * + * @protected + * @param {Object} experiences - new experiences prop + * @return {void} + */ + updateExperiences(experiences) { + this.experiences = experiences; + + this.renderUI(); + } + /** * Render controls * @@ -1064,6 +1078,7 @@ class DocBaseViewer extends BaseViewer { + @@ -82,6 +85,6 @@ export default function DocControls({ onAnnotationColorChange={onAnnotationColorChange} /> - + ); } diff --git a/src/lib/viewers/image/ImageControls.tsx b/src/lib/viewers/image/ImageControls.tsx index 11d0bb4d2..902dd759d 100644 --- a/src/lib/viewers/image/ImageControls.tsx +++ b/src/lib/viewers/image/ImageControls.tsx @@ -2,12 +2,14 @@ import React from 'react'; import AnnotationsControls, { Props as AnnotationsControlsProps } from '../controls/annotations'; import ControlsBar, { ControlsBarGroup } from '../controls/controls-bar'; import DrawingControls, { Props as DrawingControlsProps } from '../controls/annotations/DrawingControls'; +import ExperiencesProvider, { Props as ExperiencesProviderProps } from '../controls/experiences'; import FullscreenToggle, { Props as FullscreenToggleProps } from '../controls/fullscreen'; import RotateControl, { Props as RotateControlProps } from '../controls/rotate'; import ZoomControls, { Props as ZoomControlsProps } from '../controls/zoom'; export type Props = AnnotationsControlsProps & DrawingControlsProps & + ExperiencesProviderProps & FullscreenToggleProps & RotateControlProps & ZoomControlsProps; @@ -15,6 +17,7 @@ export type Props = AnnotationsControlsProps & export default function ImageControls({ annotationColor, annotationMode, + experiences, hasDrawing, hasHighlight, hasRegion, @@ -28,7 +31,7 @@ export default function ImageControls({ scale, }: Props): JSX.Element { return ( - <> + @@ -54,6 +57,6 @@ export default function ImageControls({ onAnnotationColorChange={onAnnotationColorChange} /> - + ); } diff --git a/src/lib/viewers/image/ImageViewer.js b/src/lib/viewers/image/ImageViewer.js index 0a2dbe43c..92b145a3a 100644 --- a/src/lib/viewers/image/ImageViewer.js +++ b/src/lib/viewers/image/ImageViewer.js @@ -33,6 +33,7 @@ class ImageViewer extends ImageBaseViewer { this.handleZoomEvent = this.handleZoomEvent.bind(this); this.rotateLeft = this.rotateLeft.bind(this); this.updateDiscoverabilityResinTag = this.updateDiscoverabilityResinTag.bind(this); + this.updateExperiences = this.updateExperiences.bind(this); this.updatePannability = this.updatePannability.bind(this); this.annotationControlsFSM = new AnnotationControlsFSM( @@ -366,6 +367,19 @@ class ImageViewer extends ImageBaseViewer { this.renderUI(); } + /** + * Updates experiences option after props have changed in parent app + * + * @protected + * @param {Object} experiences - new experiences prop + * @return {void} + */ + updateExperiences(experiences) { + this.experiences = experiences; + + this.renderUI(); + } + renderUI() { if (!this.controls) { return; @@ -379,6 +393,7 @@ class ImageViewer extends ImageBaseViewer {