Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(annotations): tooltip #1364

Merged
merged 7 commits into from May 4, 2021
Merged

feat(annotations): tooltip #1364

merged 7 commits into from May 4, 2021

Conversation

ghost
Copy link

@ghost ghost commented Apr 26, 2021

Screen Shot 2021-04-25 at 11 07 10 AM

  • I see that AnnotationsControls Component is imported for Images & Docs.
  • this.preview.updateExperiences will be called whenever the experiences props passed in updates feat(content-preview): update props in content preview box-ui-elements#2549
    • this will updateExperiences in the viewer depending on the file
      • this will renderUI again & update ControlsRoot
        • If the tooltip is showing we want the controls bar to show therefore I added forceShow prop to ControlsLayer Component
        • When the user closes the tooltip, we don't want the control bar to persist, so I added the callback setWasClosedByUser

@ghost ghost self-requested a review as a code owner April 26, 2021 20:49

beforeEach(() => {
jest.spyOn(React, 'useEffect').mockImplementation(cb => {
unmount = cb() as () => void; // Enzyme unmount helper does not currently invoke useEffect cleanup
if (!found) {
Copy link
Author

@ghost ghost Apr 26, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After adding TargetedClickThroughGuideTooltip there is now more than one useEffect spied on. The imported useEffect from BUIE that is being spied on is https://github.com/box/box-ui-elements/blob/master/src/features/targeting/utils/useOnClickBody.js. Therefore I modified this to select the first one

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we mock AnnotationsTargetedTooltip, instead? It doesn't look like it's under test at the moment.

{...rest}
<TargetedClickThroughGuideTooltip
body={__('annotations_tooltip_body')}
className="preview-annotations-tooltip"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We've been using bp- as the prefix for preview classnames

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also use SUIT CSS naming conventions, so bp-AnnotationsTooltip would work here.

Comment on lines 86 to 94
if (this.wasClosedByUser.tooltipFlowAnnotationsExperience) {
return false;
}

if (!this.experiences.tooltipFlowAnnotationsExperience) {
return false;
}

return this.experiences.tooltipFlowAnnotationsExperience.canShow;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way this logic could be enhanced to scale to support more experiences?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me know what you think of my solution that can be used by more experiences once I push my changes!

// Destroy timeouts on unmount
React.useEffect(() => helpersRef.current.clean, []);

return (
<div
className={`bp-ControlsLayer ${isShown ? SHOW_CLASSNAME : ''}`}
className={`bp-ControlsLayer ${isShown || forceShow ? SHOW_CLASSNAME : ''}`}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does forceShow need to be maintained separately from isShown state? Or can it be rolled into the state value?

@@ -76,7 +82,9 @@ export default function AnnotationsControls({
isEnabled={showDrawing}
mode={AnnotationMode.DRAWING}
onClick={handleModeClick}
setWasClosedByUser={setWasClosedByUser}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does setWasClosedByUser need to be passed down or can the tooltipFlowAnnotationsExperience be modified here to include setWasClosedByUser so that it can be passed transparently in the child component

@@ -67,7 +82,32 @@ export default class ControlsRoot {
this.controlsEl.classList.remove('bp-is-hidden');
}

shouldForceShow(): boolean {
if (this.wasClosedByUser.tooltipFlowAnnotationsExperience) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we move this logic to another component? ControlsRoot is currently pretty generic and shouldn't know about what controls/experiments it's rendering. I'm thinking something like:

<ControlsLayer onMount={this.handleMount}>
    <ControlsExperiments onClosed={this.helpers.reset()}>
        {controls}
    </ControlsExperiments>
</ControlsLayer>

@@ -10,13 +10,14 @@ export type Helpers = {

export type Props = {
children: React.ReactNode;
forceShow?: boolean;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we avoid having a separate prop for showing/hiding? It could get difficult to keep things synced up.

};

export default function AnnotationsControls({
annotationColor = bdlBoxBlue,
annotationMode = AnnotationMode.NONE,
experiences,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we avoid prop drilling by using context?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think that's a good idea!

{...rest}
<TargetedClickThroughGuideTooltip
body={__('annotations_tooltip_body')}
className="preview-annotations-tooltip"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also use SUIT CSS naming conventions, so bp-AnnotationsTooltip would work here.

onClick={(): void => onClick(mode)}
type="button"
{...rest}
<TargetedClickThroughGuideTooltip
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this live in a separate component?

* @return {void}
*/
updateExperiences(experiences) {
this.options.experiences = experiences;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to keep a copy of this on the Preview instance, viewer instance, and controls instance? Keeping them all in sync seems to be adding some overhead.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool! It looks like we don't need it in Preview instance 👍 We do need it in viewer instance to pass it to the context. I think we need it in controls but will continue to look

@@ -11,6 +13,8 @@ export type Props = Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'onClick
isEnabled?: boolean;
mode: AnnotationMode;
onClick?: (mode: AnnotationMode) => void;
setWasClosedByUser?: (experienceName: string | undefined) => void;
tooltipFlowAnnotationsExperience?: TargetingApi;
};

export default function AnnotationsButton({
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should every annotation button be wrapped in this tooltip? Will only one show?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My first way of doing it was every AnnotationsButton was wrapped in this tooltip, but it was only ever shown for one due to the shouldTarget prop. I've moved the tooltip out to the Annotations Control component. Now it only wraps one button. Let me know what you think. I wasn't sure if creating another component was a good idea. Thank you!

@@ -20,22 +24,46 @@ export default function AnnotationsButton({
isEnabled = true,
mode,
onClick = noop,
setWasClosedByUser = noop,
tooltipFlowAnnotationsExperience = {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we check for this value being defined and not render the tooltip if it's missing?

@ghost
Copy link
Author

ghost commented Apr 29, 2021

Still WIP, I'm trying to figure out how to hide/show control bar when tooltip is showing & closed

@ghost
Copy link
Author

ghost commented May 3, 2021

Thank you @jstoffan for your help! I referenced your example of using 2 different contexts. The only thing I had to add was a wasClosedByUser state in the AnnotationsTaretedTooltip component. This makes sure that we don't call setIsForced(true); after the tooltip is closed. There is similar logic in BUIE to make sure onShow is only called once https://github.com/box/box-ui-elements/blob/master/src/features/targeting/useShowOne.js#L14

@@ -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 annotations tooltip body
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add that this is a first-time user experience tooltip to the description? Our localization service uses this string to help translate the content properly.

import IconDrawing24 from '../icons/IconDrawing24';
import IconHighlightText16 from '../icons/IconHighlightText16';
import IconRegion24 from '../icons/IconRegion24';
import useFullscreen from '../hooks/useFullscreen';
import { AnnotationMode } from '../../../types';

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We typically group all imports together without whitespace.

>
<IconDrawing24 fill={isDrawingActive ? annotationColor : '#fff'} />
</AnnotationsButton>
<AnnotationsTargetedTooltip isEnabled={showDrawing}>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have a preference for which annotation type we want to draw the most attention to? It seems like the choice would be between drawing and regions, the latter of which may be a bit simpler of a user experience for a first-time user. It may be worth checking with the product team on this question.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will confirm with product 👍

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, it looks like they prefer the comment region button!

@@ -0,0 +1,57 @@
import React from 'react';
import TargetedClickThroughGuideTooltip from 'box-ui-elements/es/features/targeting/TargetedClickThroughGuideTooltip';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this relies on react-tether. Can we ensure that we're using the same version BUIE expects? Can we also do a comparison of the production bundle size before and after this change?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated react-tether to 1.0.5. I will compare the bundle sizes now...

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like Preview.js went up about 100 Kib and Preview.css 10 Kib. Do you think that sounds reasonable?
Screen Shot 2021-05-03 at 2 17 54 PM

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, that seems like a big jump given the nature of the change. Can we use the bundle analyzer to make sure we're not pulling in code we shouldn't be, like another version of React or something?

We should also check the built CSS file to make sure we're not bringing in extra global styles from the Button components in GuideTooltip. I think we've run into issues around that before.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screen Shot 2021-05-04 at 8 59 06 AM

preview.js is now at 755 KiB. I ran the analyzer and see that the Tooltip Component in BUIE is about 15.8 KiB, therefore I think the Tooltip component + React Tether make up most of the 54 additional KiB. I'm not sure what's included in the 10 KiB extra for preview.css. Do you know how to see what styles are included in the webpack analyzer plugin?

);

if (!shouldTarget) {
return <div>{children}</div>;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we omit the wrapping div here?


beforeEach(() => {
jest.spyOn(React, 'useEffect').mockImplementation(cb => {
unmount = cb() as () => void; // Enzyme unmount helper does not currently invoke useEffect cleanup
if (!found) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we mock AnnotationsTargetedTooltip, instead? It doesn't look like it's under test at the moment.

import { TargetingApi } from '../../../types';

export type Props = React.PropsWithChildren<{
experiences?: {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use the shared Experiences type here?


expect(wrapper.children().text()).not.toBe('Child');
expect(wrapper.children().prop('shouldTarget')).toBe(true);
expect(wrapper.children().prop('body')).toBeTruthy();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use more explicit assertions here?

@@ -1,5 +1,7 @@
import React from 'react';
import noop from 'lodash/noop';
import ControlsLayerContext from './ControlsLayerContext';

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We typically group our imports without extra whitespace.

@@ -0,0 +1,57 @@
import React from 'react';
import TargetedClickThroughGuideTooltip from 'box-ui-elements/es/features/targeting/TargetedClickThroughGuideTooltip';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, that seems like a big jump given the nature of the change. Can we use the bundle analyzer to make sure we're not pulling in code we shouldn't be, like another version of React or something?

We should also check the built CSS file to make sure we're not bringing in extra global styles from the Button components in GuideTooltip. I think we've run into issues around that before.

Copy link
Collaborator

@jstoffan jstoffan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking great!

Copy link
Collaborator

@jstoffan jstoffan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀

@ghost ghost added the ready-to-merge label May 4, 2021
@mergify mergify bot merged commit d030fe3 into box:master May 4, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants