;
+
+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 {