Skip to content

Commit

Permalink
feat(annotations): tooltip (#1364)
Browse files Browse the repository at this point in the history
* feat(annotations): tooltip

* feat(annotations): tooltip

* feat(annotations): tooltip

* feat(annotations): tooltip

* feat(annotations): tooltip

* feat(annotations): tooltip

* feat(annotations): tooltip
  • Loading branch information
megansmith-box authored May 4, 2021
1 parent 528b065 commit d030fe3
Show file tree
Hide file tree
Showing 23 changed files with 280 additions and 25 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 4 additions & 0 deletions src/i18n/en-US.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions src/lib/Preview.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
2 changes: 2 additions & 0 deletions src/lib/types/buie.ts
Original file line number Diff line number Diff line change
@@ -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';
1 change: 1 addition & 0 deletions src/lib/types/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './annotations';
export * from './targeting';
10 changes: 10 additions & 0 deletions src/lib/types/targeting.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export type Experiences = {
[key: string]: TargetingApi;
};

export type TargetingApi = {
canShow: boolean;
onClose: () => void;
onComplete: () => void;
onShow: () => void;
};
25 changes: 14 additions & 11 deletions src/lib/viewers/controls/annotations/AnnotationsControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -80,17 +81,19 @@ export default function AnnotationsControls({
>
<IconDrawing24 fill={isDrawingActive ? annotationColor : '#fff'} />
</AnnotationsButton>
<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>
<AnnotationsTargetedTooltip isEnabled={showRegion}>
<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>
</AnnotationsTargetedTooltip>
<AnnotationsButton
data-resin-target="highlightText"
data-testid="bp-AnnotationsControls-highlightBtn"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
.bp-AnnotationsTooltip {
max-width: 408px;
padding: 18px 0 12px 18px;
}

.bp-AnnotationsTooltip-title {
font-weight: bold;
font-size: 18px;
}

.bp-AnnotationsTooltip-body {
font-size: 14px;
line-height: 20px;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import React from 'react';
import TargetedClickThroughTooltip from '../tooltip';
import { ControlsLayerContext } from '../controls-layer';
import { ExperiencesContext } from '../experiences';
import { TargetingApi } from '../../../types';
import './AnnotationsTargetedTooltip.scss';

export type Props = React.PropsWithChildren<{
isEnabled?: boolean;
}>;

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 (
<TargetedClickThroughTooltip
className="bp-AnnotationsTooltip"
shouldTarget
showCloseButton
text={
<div>
<h3 className="bp-AnnotationsTooltip-title">{__('annotations_tooltip_title')}</h3>
<p className="bp-AnnotationsTooltip-body">{__('annotations_tooltip_body')}</p>
</div>
}
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}
</TargetedClickThroughTooltip>
);
}

export default AnnotationsTargetedTooltip;
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React from 'react';
import { ReactWrapper, mount } from 'enzyme';
import AnnotationsTargetedTooltip from '../AnnotationsTargetedTooltip';

describe('AnnotationsTargetedTooltip', () => {
const getWrapper = (props = {}): ReactWrapper =>
mount(
<AnnotationsTargetedTooltip {...props}>
<div>Child</div>
</AnnotationsTargetedTooltip>,
);

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);
});
});
});
27 changes: 19 additions & 8 deletions src/lib/viewers/controls/controls-layer/ControlsLayer.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react';
import noop from 'lodash/noop';
import ControlsLayerContext from './ControlsLayerContext';
import './ControlsLayer.scss';

export type Helpers = {
Expand All @@ -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);
Expand Down Expand Up @@ -78,14 +80,23 @@ export default function ControlsLayer({ children, onMount = noop }: Props): JSX.
React.useEffect(() => helpersRef.current.clean, []);

return (
<div
className={`bp-ControlsLayer ${isShown ? SHOW_CLASSNAME : ''}`}
onBlur={handleFocusOut}
onFocus={handleFocusIn}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
<ControlsLayerContext.Provider
value={{
setIsForced: (value): void => {
helpersRef.current.reset();
setIsForced(value);
},
}}
>
{children}
</div>
<div
className={`bp-ControlsLayer ${isShown || isForced ? SHOW_CLASSNAME : ''}`}
onBlur={handleFocusOut}
onFocus={handleFocusIn}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
{children}
</div>
</ControlsLayerContext.Provider>
);
}
10 changes: 10 additions & 0 deletions src/lib/viewers/controls/controls-layer/ControlsLayerContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import React from 'react';
import noop from 'lodash/noop';

export type Context = {
setIsForced: (isForced: boolean) => void;
};

export default React.createContext<Context>({
setIsForced: noop,
});
1 change: 1 addition & 0 deletions src/lib/viewers/controls/controls-layer/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './ControlsLayer';
export { default } from './ControlsLayer';
export { default as ControlsLayerContext } from './ControlsLayerContext';
10 changes: 10 additions & 0 deletions src/lib/viewers/controls/experiences/ExperiencesContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import React from 'react';
import { Experiences } from '../../../types';

export type Context = {
experiences: Experiences;
};

export default React.createContext<Context>({
experiences: {},
});
11 changes: 11 additions & 0 deletions src/lib/viewers/controls/experiences/ExperiencesProvider.tsx
Original file line number Diff line number Diff line change
@@ -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 <ExperiencesContext.Provider value={{ experiences }}>{children}</ExperiencesContext.Provider>;
}
3 changes: 3 additions & 0 deletions src/lib/viewers/controls/experiences/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './ExperiencesProvider';
export { default } from './ExperiencesProvider';
export { default as ExperiencesContext } from './ExperiencesContext';
11 changes: 11 additions & 0 deletions src/lib/viewers/controls/tooltip/TargetedClickThroughTooltip.tsx
Original file line number Diff line number Diff line change
@@ -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 <Tooltip {...props}>{children}</Tooltip>;
},
);

export default TargetedClickThroughTooltip;
2 changes: 2 additions & 0 deletions src/lib/viewers/controls/tooltip/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default } from './TargetedClickThroughTooltip';
export * from './TargetedClickThroughTooltip';
15 changes: 15 additions & 0 deletions src/lib/viewers/doc/DocBaseViewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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
*
Expand All @@ -1064,6 +1078,7 @@ class DocBaseViewer extends BaseViewer {
<DocControls
annotationColor={this.annotationModule.getColor()}
annotationMode={this.annotationControlsFSM.getMode()}
experiences={this.experiences}
hasDrawing={canAnnotate && showAnnotationsDrawingCreate}
hasHighlight={canAnnotate && canDownload}
hasRegion={canAnnotate}
Expand Down
Loading

0 comments on commit d030fe3

Please sign in to comment.