@khanacademy/perseus 0.4.1 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/dist/es/index.js +613 -5189
- package/dist/es/index.js.map +1 -1
- package/dist/index.js +607 -5183
- package/dist/index.js.map +1 -1
- package/dist/strings.js +0 -2
- package/package.json +6 -3
- package/src/components/highlighting/ui/highlight-tooltip.jsx +0 -16
- package/src/components/new-tooltip/README.md +1 -108
- package/src/components/new-tooltip/new-tooltip.jsx +47 -291
- package/src/components/new-tooltip/__stories__/flexible-position.stories.jsx +0 -437
- package/src/components/new-tooltip/__stories__/tooltip-arrow.stories.jsx +0 -70
- package/src/components/new-tooltip/__stories__/tooltip-bubble.stories.jsx +0 -98
- package/src/components/new-tooltip/__tests__/util_test.js +0 -47
- package/src/components/new-tooltip/flexible-position.jsx +0 -182
- package/src/components/new-tooltip/tooltip-arrow.jsx +0 -184
- package/src/components/new-tooltip/tooltip-bubble.jsx +0 -223
- package/src/components/new-tooltip/tooltip-portal.jsx +0 -101
- package/src/components/new-tooltip/tooltip-positioner.jsx +0 -529
- package/src/components/new-tooltip/util.js +0 -27
package/dist/strings.js
CHANGED
|
@@ -50,8 +50,6 @@ module.exports = [
|
|
|
50
50
|
i18n._("Click on the tiles to change the lights.", {}),
|
|
51
51
|
i18n._("Click to add points", {}),
|
|
52
52
|
i18n._("Click to add vertices", {}),
|
|
53
|
-
i18n._("Close", {}),
|
|
54
|
-
i18n._("Closes the tooltip: ", {}),
|
|
55
53
|
i18n._("Correct", {}),
|
|
56
54
|
i18n._("Correct (but you crossed it out)", {}),
|
|
57
55
|
i18n._("Correct (selected)", {}),
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"description": "Core Perseus API (includes renderers and widgets)",
|
|
4
4
|
"author": "Khan Academy",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"version": "0.4.
|
|
6
|
+
"version": "0.4.2",
|
|
7
7
|
"publishConfig": {
|
|
8
8
|
"access": "public"
|
|
9
9
|
},
|
|
@@ -23,19 +23,20 @@
|
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"@khanacademy/kas": "^0.2.5",
|
|
25
25
|
"@khanacademy/kmath": "^0.0.6",
|
|
26
|
-
"@khanacademy/math-input": "^0.6.
|
|
26
|
+
"@khanacademy/math-input": "^0.6.1",
|
|
27
27
|
"@khanacademy/perseus-linter": "^0.2.1",
|
|
28
28
|
"@khanacademy/pure-markdown": "^0.1.1",
|
|
29
29
|
"@khanacademy/simple-markdown": "^0.8.4",
|
|
30
|
-
"@khanacademy/wonder-blocks-form": "^2.4.5",
|
|
31
30
|
"mathquill": "git+https://git@github.com/Khan/mathquill.git#a9ae54e057c5c1acc8244a5627acbff29901d992"
|
|
32
31
|
},
|
|
33
32
|
"devDependencies": {
|
|
34
33
|
"@khanacademy/wonder-blocks-button": "^2.11.5",
|
|
34
|
+
"@khanacademy/wonder-blocks-clickable": "^2.2.6",
|
|
35
35
|
"@khanacademy/wonder-blocks-color": "^1.1.20",
|
|
36
36
|
"@khanacademy/wonder-blocks-core": "^4.3.0",
|
|
37
37
|
"@khanacademy/wonder-blocks-data": "^7.0.0",
|
|
38
38
|
"@khanacademy/wonder-blocks-dropdown": "^2.6.7",
|
|
39
|
+
"@khanacademy/wonder-blocks-form": "^2.4.5",
|
|
39
40
|
"@khanacademy/wonder-blocks-i18n": "^1.2.3",
|
|
40
41
|
"@khanacademy/wonder-blocks-layout": "^1.4.9",
|
|
41
42
|
"@khanacademy/wonder-blocks-link": "^3.8.5",
|
|
@@ -62,10 +63,12 @@
|
|
|
62
63
|
},
|
|
63
64
|
"peerDependencies": {
|
|
64
65
|
"@khanacademy/wonder-blocks-button": "^2.11.5",
|
|
66
|
+
"@khanacademy/wonder-blocks-clickable": "^2.2.6",
|
|
65
67
|
"@khanacademy/wonder-blocks-color": "^1.1.20",
|
|
66
68
|
"@khanacademy/wonder-blocks-core": "^4.3.0",
|
|
67
69
|
"@khanacademy/wonder-blocks-data": "^7.0.0",
|
|
68
70
|
"@khanacademy/wonder-blocks-dropdown": "^2.6.7",
|
|
71
|
+
"@khanacademy/wonder-blocks-form": "^2.4.5",
|
|
69
72
|
"@khanacademy/wonder-blocks-i18n": "^1.2.3",
|
|
70
73
|
"@khanacademy/wonder-blocks-layout": "^1.4.9",
|
|
71
74
|
"@khanacademy/wonder-blocks-link": "^3.8.5",
|
|
@@ -38,22 +38,6 @@ type HighlightTooltipProps = {|
|
|
|
38
38
|
class HighlightTooltip extends React.PureComponent<HighlightTooltipProps> {
|
|
39
39
|
_tooltip: ?React.ElementRef<typeof NewTooltip>;
|
|
40
40
|
|
|
41
|
-
componentDidUpdate(oldProps: HighlightTooltipProps) {
|
|
42
|
-
// If the highlight focus might've changed, then tell the tooltip to
|
|
43
|
-
// remeasure itself. For "Add highlight", this happens when an existing
|
|
44
|
-
// selection is updated, e.g., by using the Shift+RightArrow shortcut.
|
|
45
|
-
// For "Remove highlight", this can happen when we quickly switch from
|
|
46
|
-
// targeting one highlight to another: we reuse the same
|
|
47
|
-
// HighlightTooltip instance, for improved performance.
|
|
48
|
-
if (
|
|
49
|
-
this.props.focusNode !== oldProps.focusNode ||
|
|
50
|
-
this.props.focusOffset !== oldProps.focusOffset ||
|
|
51
|
-
this.props.offsetParent !== oldProps.offsetParent
|
|
52
|
-
) {
|
|
53
|
-
this._tooltip?.remeasure();
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
41
|
_getFocusRect(): ?Rect {
|
|
58
42
|
const {focusNode, focusOffset, offsetParent} = this.props;
|
|
59
43
|
|
|
@@ -1,110 +1,3 @@
|
|
|
1
1
|
# `NewTooltip`: KA tooltip component
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
**Warning: This component is WIP!** We're in the process of rolling out this tooltip to eventually replace the old `Tooltip` component. `NewTooltip` is currently is only used at a select few call sites. [Scroll down for details.](#warning-this-component-is-wip)
|
|
6
|
-
|
|
7
|
-
## Usage
|
|
8
|
-
|
|
9
|
-
To point a tooltip at an element, wrap it in a `NewTooltip` component:
|
|
10
|
-
|
|
11
|
-
```jsx
|
|
12
|
-
<NewTooltip content={i18n._("You should click on this!")}>
|
|
13
|
-
<button><$_>Assign to students</$_></button>
|
|
14
|
-
</NewTooltip>
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
This will produce a tooltip that says "You should click on this!" when you hover over the wrapped button.
|
|
18
|
-
|
|
19
|
-
You can also provide more advanced content:
|
|
20
|
-
|
|
21
|
-
```jsx
|
|
22
|
-
const content = <div className={css(styles.tooltipContent)}>
|
|
23
|
-
<p><$_>You should click on this!</$_></p>
|
|
24
|
-
<p><$_>It will help you!</$_></p>
|
|
25
|
-
</div>;
|
|
26
|
-
|
|
27
|
-
<NewTooltip content={content}>
|
|
28
|
-
<button>Assign to students</button>
|
|
29
|
-
</NewTooltip>
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
There are also many styling, positioning, and behavioral options for the tooltip! See `types.js` for the list of options, and the [React Sandbox][fixtures] to see them in action.
|
|
33
|
-
|
|
34
|
-
## Accessibility
|
|
35
|
-
|
|
36
|
-
Unlike some other tooltip components on the web, this tooltip is a _visual_ element rather than a semantic element. Its use cases are relatively diverse:
|
|
37
|
-
|
|
38
|
-
* Explaining a UI control
|
|
39
|
-
* Illustrating a relationship between two permanent UI elements
|
|
40
|
-
* Showing a transient UI element without disrupting the flow of the document
|
|
41
|
-
* Prompting the user to take a secondary or blocking action (tooltip) before taking the main action (target)
|
|
42
|
-
|
|
43
|
-
The key commonality is that the tooltip and its target are somehow _related_… but that's about it.
|
|
44
|
-
|
|
45
|
-
**So, be sure to add your own ARIA attributes to tooltip targets and content!** Only you know the semantic relationship between your tooltip and target, so it's up to you to express it.
|
|
46
|
-
|
|
47
|
-
## Package structure
|
|
48
|
-
|
|
49
|
-
The two most important files in this package are:
|
|
50
|
-
|
|
51
|
-
* `new-tooltip.jsx`: The entry point to our tooltip code. Manages high-level visibility state. Renders the tooltip's target element inline, and renders a `TooltipPortal` to get the tooltip content outside of any `overflow: hidden` containers.
|
|
52
|
-
* `types.js`: Defines the props for `NewTooltip`.
|
|
53
|
-
|
|
54
|
-
We also have a number of supporting files, as well:
|
|
55
|
-
|
|
56
|
-
* React components
|
|
57
|
-
* `tooltip-portal.jsx`: Simulates a [React 16 portal][react-portal]. Creates a node near the root of the document, and renders a `TooltipPositioner` there, instead of inline with the target element.
|
|
58
|
-
* `tooltip-positioner.jsx`: Manages the position of the tooltip UI. Computes the position of the target element, and watches for changes. Renders a `TooltipBubble` and `TooltipArrow`, positioned to point at the target element.
|
|
59
|
-
* `tooltip-bubble.jsx`: The main tooltip "bubble" element. Renders the tooltip content.
|
|
60
|
-
* `tooltip-arrow.jsx`: The little triangle that points from the tooltip bubble to the target element.
|
|
61
|
-
* Library files:
|
|
62
|
-
* `util.js`: Miscellaneous utility functions.
|
|
63
|
-
|
|
64
|
-
Note that these components are relatively tightly coupled—for example, rather than specifying their individual data requirements, each tooltip component just forwards the entire set of `tooltipProps` to its child tooltip components. This enables faster iteration, but means that the child components aren't especially reusable.
|
|
65
|
-
|
|
66
|
-
## Warning: This component is WIP!
|
|
67
|
-
|
|
68
|
-
This is `NewTooltip`, a fork of the old `Tooltip`, in which we've built a brand new layout algorithm to address some key UX issues. Someday, `NewTooltip` will replace `Tooltip`, hooray! [See ADR #33 for details.][adr-33]
|
|
69
|
-
|
|
70
|
-
`NewTooltip` is probably _feature-ready_ for most use cases on Khan Academy, but **general usage is not yet advised**. We're still developing our plan for the "Wonder Blocks" shared component rollout, and we want to keep call sites well-constrained until we know more. But, if you _need_ the features of `NewTooltip` today, maybe we can figure something out! Hit up the [#reusable-components][reusable-components] Slack channel.
|
|
71
|
-
|
|
72
|
-
But, if you need a feature that hasn't been ported yet, maybe you'll want to continue to use `Tooltip`. (Or port the feature into `NewTooltip`? 😍)
|
|
73
|
-
|
|
74
|
-
Here's the overall behavioral comparison right now. Especially valuable features are marked with a star (⭐️).
|
|
75
|
-
|
|
76
|
-
| Feature | `NewTooltip` | `Tooltip` |
|
|
77
|
-
|----------------------------------------------|--------------|-----------|
|
|
78
|
-
| ⭐️ **Basic tooltip functionality** | ✅ | ✅ |
|
|
79
|
-
| ⭐️ **Escape `overflow: hidden` containers** | ✅ | ✅ |
|
|
80
|
-
| ⭐️ **Attach to any side of the target** | ✅ | ✅ |
|
|
81
|
-
| ⭐️ **Configurable colors** | ✅ | ✅ |
|
|
82
|
-
| ⭐️ **Dismiss on click** | ✅ | ✅ |
|
|
83
|
-
| ⭐️ **Only show once per user lifetime** | ✅ | ✅ |
|
|
84
|
-
| Tweak position via the `offset` prop | ✅ | ✅ |
|
|
85
|
-
| Arrow center-aligned with bubble | ✅ | ✅ |
|
|
86
|
-
| ⭐️ **Auto-shift position to avoid wrapping** | ✅ | ❌ |
|
|
87
|
-
| ⭐️ **Auto-reposition during scrolling** | ✅ | ❌ |
|
|
88
|
-
| ⭐️ **Update content without remounting** | ✅ | ❌ |
|
|
89
|
-
| ⭐️ **Media queries to show/hide tooltip** | ✅ | ❌ |
|
|
90
|
-
| Can specify offset from the _arrow_ | ✅ | ❌ |
|
|
91
|
-
| Can specify margin around tooltip | ✅ | ❌ |
|
|
92
|
-
| ⭐️ **Can be used inside a modal** | ✅ | ✅ |
|
|
93
|
-
| Can target `position: fixed` elements | ❌ | ✅ |
|
|
94
|
-
| Auto-choose top or bottom anchoring | ❌ | ✅ |
|
|
95
|
-
| Try to left/right align tooltip with target | ❌ | ✅ |
|
|
96
|
-
| Manual positioning (left/top) | ❌ | ✅ |
|
|
97
|
-
| Dismiss on scroll/interaction/timer | ❌ | ✅ |
|
|
98
|
-
| Show tooltip when target gains focus | ❌ | ✅ |
|
|
99
|
-
|
|
100
|
-
> NOTE(mdr): It's possible that some of the old `Tooltip` features are also "especially valuable", and I just don't know about it yet! So far we've only used `NewTooltip` at GTP call sites, and they haven't needed many of these features, so it's not clear to me how broadly they're actually used. (We'll probably have to support most of the missing options eventually, but maybe not all, in case some of their use cases were addressed by the general positioning improvements?)
|
|
101
|
-
|
|
102
|
-
Eventually, we'll roll out `NewTooltip` to all KA call sites, and upgrade it to support everything that the old `Tooltip` call sites need. So, no worries if you end up adding new `Tooltip` call sites; we'll help upgrade them soon! [For more details, see ADR #33.][adr-33]
|
|
103
|
-
|
|
104
|
-
[adr-33]: https://docs.google.com/document/d/1B4pMGWp344e96RmIbnsmUBiTbrizdbOvipe-9I_1iZQ/edit
|
|
105
|
-
[fixtures]: http://localhost:8081/react-sandbox/components/new-tooltip-package/new-tooltip.jsx.fixture.js
|
|
106
|
-
[react-portal]: https://reactjs.org/docs/portals.html
|
|
107
|
-
[reusable-components]: https://khanacademy.slack.com/messages/C4PE1QM5Y/
|
|
108
|
-
|
|
109
|
-
Code-Owner: Frontend Infrastructure
|
|
110
|
-
Crowdin-Category: _other_
|
|
3
|
+
This is a wrapper around [Wonderblock's popover component](https://github.com/Khan/wonder-blocks). It is API compatiable with Perseus and remains for compatiability and legacy reasons. If you are anyone but the Perseus group at Khan Academy please use the wonderblocks component directly.
|
|
@@ -1,35 +1,15 @@
|
|
|
1
1
|
/* eslint-disable react/no-unsafe */
|
|
2
2
|
// @flow
|
|
3
3
|
/**
|
|
4
|
-
* This component
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* This top-level component primarily manages high-level visibility state. It
|
|
8
|
-
* renders the tooltip's target element inline, and renders a TooltipPortal
|
|
9
|
-
* to get the tooltip content outside of any `overflow: hidden` containers.
|
|
10
|
-
*
|
|
11
|
-
* For usage information and help navigating this package, see `README.md`.
|
|
12
|
-
* For prop type information, see `types.js`.
|
|
13
|
-
*
|
|
14
|
-
* Public methods:
|
|
15
|
-
* - `remeasure()`: Re-measure the target element's position, and update the
|
|
16
|
-
* tooltip's position. This is helpful to call when the parent is aware of
|
|
17
|
-
* a change in the target element's position, which might not be clear from
|
|
18
|
-
* the events that the tooltip is watching (e.g., scrolls).
|
|
4
|
+
* This component is a wrapper around Wonderblock's tooltip with the API
|
|
5
|
+
* that perseus expects
|
|
19
6
|
*/
|
|
20
|
-
|
|
21
|
-
import
|
|
7
|
+
|
|
8
|
+
import {Popover, PopoverContentCore} from "@khanacademy/wonder-blocks-popover";
|
|
22
9
|
import * as React from "react";
|
|
23
10
|
|
|
24
|
-
import {getDependencies} from "../../dependencies.js";
|
|
25
|
-
import {Errors, Log} from "../../logging/log.js";
|
|
26
|
-
import {PerseusError} from "../../perseus-error.js";
|
|
27
11
|
import {colors} from "../../styles/global-styles.js";
|
|
28
|
-
import {createVisibilityObserver} from "../visibility-observer/visibility-observer.js";
|
|
29
|
-
|
|
30
|
-
import TooltipPortal from "./tooltip-portal.jsx";
|
|
31
12
|
|
|
32
|
-
import type {VisibilityObserver} from "../visibility-observer/visibility-observer.js";
|
|
33
13
|
import type {NewTooltipProps} from "./types.js";
|
|
34
14
|
|
|
35
15
|
type NewTooltipState = {|
|
|
@@ -37,28 +17,11 @@ type NewTooltipState = {|
|
|
|
37
17
|
dismissed: boolean,
|
|
38
18
|
// Whether the target element is currently being hovered.
|
|
39
19
|
hovered: boolean,
|
|
40
|
-
// Whether the target element is currently visible, relative to the "root
|
|
41
|
-
// element" where we're mounting the tooltip.
|
|
42
|
-
//
|
|
43
|
-
// NOTE(mdr): This can be true (element is visible), false (element is not
|
|
44
|
-
// visible), or null (unknown).
|
|
45
|
-
targetElementIsVisible: ?boolean,
|
|
46
20
|
|};
|
|
47
21
|
|
|
48
22
|
type Props = {|
|
|
49
23
|
...NewTooltipProps,
|
|
50
24
|
|
|
51
|
-
// The wrapped content that serves as the target of the tooltip.
|
|
52
|
-
//
|
|
53
|
-
// This must be a single React element, with block-like styles. (e.g.,
|
|
54
|
-
// `display: block`, `display: inline-block`, `display: flex`, etc.)
|
|
55
|
-
//
|
|
56
|
-
// Unfortunately, we can't reliably track the position of inline elements,
|
|
57
|
-
// and we can't wrap your children in a block-like element without
|
|
58
|
-
// potentially disrupting your call site's layout.
|
|
59
|
-
//
|
|
60
|
-
// So, your element will need to be block-like. Sorry for the extra
|
|
61
|
-
// constraint! (For details, see the `render` method of `NewTooltip`).
|
|
62
25
|
children: React$Element<any>,
|
|
63
26
|
|};
|
|
64
27
|
|
|
@@ -88,218 +51,27 @@ export const DefaultPropValues: DefaultProps = Object.freeze({
|
|
|
88
51
|
showOnMount: false,
|
|
89
52
|
});
|
|
90
53
|
|
|
91
|
-
class NewTooltip extends React.
|
|
92
|
-
// The root element, where we'll mount tooltips.
|
|
93
|
-
_rootElement: HTMLElement;
|
|
94
|
-
|
|
95
|
-
// The wrapper element that we created, to listen to mouse events.
|
|
96
|
-
_wrapper: ?HTMLElement;
|
|
97
|
-
|
|
98
|
-
// The target element itself: the child of the wrapper, provided by the
|
|
99
|
-
// caller.
|
|
100
|
-
_targetElement: ?HTMLElement;
|
|
101
|
-
|
|
102
|
-
// Our child TooltipPortal instance. Used to pass down `remeasure` calls.
|
|
103
|
-
_tooltipPortal: ?TooltipPortal;
|
|
104
|
-
|
|
105
|
-
// The VisibilityObserver, notifying us when the element enters or exits
|
|
106
|
-
// total invisibility. Will be null iff the browser doesn't support the
|
|
107
|
-
// necessary APIs.
|
|
108
|
-
_visibilityObserver: VisibilityObserver;
|
|
109
|
-
|
|
110
|
-
// Whether this component is mounted.
|
|
111
|
-
_isMounted: boolean;
|
|
112
|
-
|
|
113
|
-
// A timeout used in `componentDidMount`.
|
|
114
|
-
_targetElementMountTimeout: ?TimeoutID;
|
|
115
|
-
|
|
54
|
+
class NewTooltip extends React.Component<Props, NewTooltipState> {
|
|
116
55
|
static defaultProps: DefaultProps = DefaultPropValues;
|
|
117
56
|
|
|
118
57
|
constructor(props: Props) {
|
|
119
58
|
super(props);
|
|
120
59
|
|
|
121
|
-
this._isMounted = false;
|
|
122
|
-
|
|
123
60
|
this.state = {
|
|
124
61
|
dismissed: props.dismissed != null ? props.dismissed : false,
|
|
125
62
|
hovered: false,
|
|
126
|
-
targetElementIsVisible: false,
|
|
127
63
|
};
|
|
128
64
|
}
|
|
129
65
|
|
|
130
|
-
componentDidMount() {
|
|
131
|
-
this._isMounted = true;
|
|
132
|
-
|
|
133
|
-
this._registerVisibilityObserver();
|
|
134
|
-
}
|
|
135
|
-
|
|
136
66
|
UNSAFE_componentWillReceiveProps(nextProps: Props) {
|
|
137
|
-
if (nextProps.dismissed
|
|
138
|
-
// We're serving as a "controlled component": our dismissed state
|
|
139
|
-
// always matches the incoming props.
|
|
67
|
+
if (Object.keys(nextProps).includes("dismissed")) {
|
|
140
68
|
this.setState({dismissed: nextProps.dismissed});
|
|
141
69
|
}
|
|
142
70
|
}
|
|
143
71
|
|
|
144
|
-
UNSAFE_componentWillUpdate() {
|
|
145
|
-
// The component may receive a new root element via the context.
|
|
146
|
-
// For example, we may get a modal root element that has become
|
|
147
|
-
// available.
|
|
148
|
-
|
|
149
|
-
this._registerVisibilityObserver();
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
componentWillUnmount() {
|
|
153
|
-
this._isMounted = false;
|
|
154
|
-
if (this._targetElementMountTimeout) {
|
|
155
|
-
// TODO(jeff, WEB-1378): Use Wonder Blocks Timing API.
|
|
156
|
-
// eslint-disable-next-line no-restricted-syntax
|
|
157
|
-
clearTimeout(this._targetElementMountTimeout);
|
|
158
|
-
}
|
|
159
|
-
this._visibilityObserver.disconnect();
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
_findRootElement(): any {
|
|
163
|
-
// In the Khan Academy webapp, the best place to mount is the
|
|
164
|
-
// #outer-wrapper element, because it's our outermost scroll container.
|
|
165
|
-
// But, if we're for some reason in a page without an #outer-wrapper,
|
|
166
|
-
// then mounting at the body element is probably a good guess.
|
|
167
|
-
// Also, if there is a modal anywhere in the context, it should be the
|
|
168
|
-
// root because it obscures the visibility of the higher containers.
|
|
169
|
-
//
|
|
170
|
-
// HACK(davidflanagan):
|
|
171
|
-
// Missions use a modal-like ../../tasks-package/task-container.jsx
|
|
172
|
-
// component that does not work like our standard modal, so we handle
|
|
173
|
-
// that with a special hardcoded case element id. When this component
|
|
174
|
-
// is converted to a wonder block, we should upgrade Modal to use
|
|
175
|
-
// the new context mechanism instead of the legacy mechanism and
|
|
176
|
-
// replace this mission-task-container id with context.
|
|
177
|
-
return (
|
|
178
|
-
this.context.modalContainerElement ||
|
|
179
|
-
document.getElementById("mission-task-container") ||
|
|
180
|
-
document.getElementById("outer-wrapper") ||
|
|
181
|
-
document.body
|
|
182
|
-
);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// When this component mounts inside a modal, we don't have the modal
|
|
186
|
-
// container in the context yet. That comes during the next cycle. If the
|
|
187
|
-
// tooltip is in the modal, it will be observing with respect to the app
|
|
188
|
-
// root for a moment (and won't display because it is obscured) and then
|
|
189
|
-
// it will get called again on the next update because the context changes.
|
|
190
|
-
// When it sees that the root is different, it will unregister the first
|
|
191
|
-
// and register the new one.
|
|
192
|
-
_registerVisibilityObserver() {
|
|
193
|
-
const rootElement = this._findRootElement();
|
|
194
|
-
|
|
195
|
-
if (!rootElement) {
|
|
196
|
-
throw new PerseusError(
|
|
197
|
-
"Failed to mount NewTooltip: " +
|
|
198
|
-
"we need a #outer-wrapper or document.body element.",
|
|
199
|
-
Errors.Internal,
|
|
200
|
-
);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
if (rootElement === this._rootElement) {
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
this._rootElement = rootElement;
|
|
208
|
-
|
|
209
|
-
if (this._visibilityObserver) {
|
|
210
|
-
this._visibilityObserver.disconnect();
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Create a VisibilityObserver, and start by assuming the target
|
|
214
|
-
// element is invisible. On mount, we'll start observing the
|
|
215
|
-
// target, and check whether it's visible.
|
|
216
|
-
this._visibilityObserver = createVisibilityObserver(
|
|
217
|
-
rootElement,
|
|
218
|
-
this._handleVisibilityChange,
|
|
219
|
-
);
|
|
220
|
-
|
|
221
|
-
// Find our target element. We do this by walking down the DOM tree,
|
|
222
|
-
// until we find a node that doesn't have `data-tooltip-wrapper` on it.
|
|
223
|
-
//
|
|
224
|
-
// (This also enables us to compose tooltips: if you wrap an element
|
|
225
|
-
// in multiple tooltips, we'll skip *all* the wrapper elements, and
|
|
226
|
-
// find the correct element.)
|
|
227
|
-
//
|
|
228
|
-
// HACK(mdr): I'm not entirely satisfied with this :/ The whole tooltip
|
|
229
|
-
// composition "API" is a bit of a hack to support different
|
|
230
|
-
// tooltip props based on media query, and a *robust* way of
|
|
231
|
-
// specifying that would probably be preferable... but, in the
|
|
232
|
-
// meantime, I'd rather not invent new styling patterns (there's
|
|
233
|
-
// already enough fragmentation!), and instead write some
|
|
234
|
-
// well-encapsulated hacks that keep the public-facing API simple
|
|
235
|
-
// and low on new concepts.
|
|
236
|
-
const wrapper = this._wrapper;
|
|
237
|
-
if (!wrapper) {
|
|
238
|
-
throw new PerseusError(
|
|
239
|
-
"NewTooltip expected a wrapper element after mount",
|
|
240
|
-
Errors.Internal,
|
|
241
|
-
);
|
|
242
|
-
}
|
|
243
|
-
let targetElement: HTMLElement = wrapper;
|
|
244
|
-
while (targetElement.hasAttribute("data-tooltip-wrapper")) {
|
|
245
|
-
targetElement = (targetElement.childNodes[0]: any);
|
|
246
|
-
if (!targetElement) {
|
|
247
|
-
throw new PerseusError(
|
|
248
|
-
"NewTooltip expected wrapper element to have a child",
|
|
249
|
-
Errors.Internal,
|
|
250
|
-
);
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
this._targetElement = targetElement;
|
|
254
|
-
|
|
255
|
-
// Now that we've mounted, update our TooltipPortal (by re-rendering)
|
|
256
|
-
// and our VisibilityObserver, so they can be aware of our new target
|
|
257
|
-
// element.
|
|
258
|
-
// TODO(jeff, WEB-1378): Use Wonder Blocks Timing API.
|
|
259
|
-
// eslint-disable-next-line no-restricted-syntax
|
|
260
|
-
this._targetElementMountTimeout = setTimeout(() => {
|
|
261
|
-
const targetElement = this._targetElement;
|
|
262
|
-
if (!targetElement) {
|
|
263
|
-
throw new PerseusError(
|
|
264
|
-
"NewTooltip should have a target element after mounting.",
|
|
265
|
-
Errors.Internal,
|
|
266
|
-
);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// While we're here, let's also check whether the element is
|
|
270
|
-
// block-like, as is required for position tracking to work. If
|
|
271
|
-
// not, warn the developer - but in prod, skip this, because
|
|
272
|
-
// it's slow.
|
|
273
|
-
if (getDependencies().isDevServer) {
|
|
274
|
-
if (getComputedStyle(targetElement).display === "inline") {
|
|
275
|
-
Log.error(
|
|
276
|
-
"Warning: The children of a NewTooltip element " +
|
|
277
|
-
"must be block-like, but this element seems " +
|
|
278
|
-
"to have `display: inline`. This could break " +
|
|
279
|
-
"position tracking.",
|
|
280
|
-
Errors.Internal,
|
|
281
|
-
);
|
|
282
|
-
Log.log("NewTooltip Target Element:", {
|
|
283
|
-
targetElement: targetElement.outerHTML,
|
|
284
|
-
});
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
this._visibilityObserver.setTargetElement(targetElement);
|
|
289
|
-
|
|
290
|
-
this.forceUpdate();
|
|
291
|
-
}, 0);
|
|
292
|
-
}
|
|
293
|
-
|
|
294
72
|
_shouldShowTooltip(): boolean {
|
|
295
73
|
const {toggleOnHover, showOnMount} = this.props;
|
|
296
|
-
const {dismissed
|
|
297
|
-
|
|
298
|
-
// If the target element isn't visible, then we shouldn't show the
|
|
299
|
-
// tooltip, regardless of configuration.
|
|
300
|
-
if (targetElementIsVisible === false) {
|
|
301
|
-
return false;
|
|
302
|
-
}
|
|
74
|
+
const {dismissed} = this.state;
|
|
303
75
|
|
|
304
76
|
// If the tooltip was dismissed, or we're not sure yet, don't show it.
|
|
305
77
|
if (dismissed) {
|
|
@@ -322,78 +94,62 @@ class NewTooltip extends React.PureComponent<Props, NewTooltipState> {
|
|
|
322
94
|
return false;
|
|
323
95
|
}
|
|
324
96
|
|
|
325
|
-
_handleVisibilityChange: (targetElementIsVisible: boolean) => void = (
|
|
326
|
-
targetElementIsVisible: boolean,
|
|
327
|
-
) => {
|
|
328
|
-
this.setState({targetElementIsVisible});
|
|
329
|
-
};
|
|
330
|
-
|
|
331
|
-
_handleMouseEnter: () => void = () => {
|
|
332
|
-
this.setState({hovered: true});
|
|
333
|
-
};
|
|
334
|
-
|
|
335
|
-
_handleMouseLeave: () => void = () => {
|
|
336
|
-
this.setState({hovered: false});
|
|
337
|
-
};
|
|
338
|
-
|
|
339
97
|
_handleDismiss: () => void = () => {
|
|
340
98
|
if (this.props.dismissed == null) {
|
|
341
99
|
// We're serving as an "uncontrolled component": our dismissed
|
|
342
100
|
// state is managed by us, not by incoming props.
|
|
343
101
|
this.setState({dismissed: true});
|
|
344
102
|
}
|
|
103
|
+
|
|
345
104
|
this.props.onDismiss && this.props.onDismiss();
|
|
346
105
|
};
|
|
347
106
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
107
|
+
_popoverContentColor: () => "white" | "blue" | "darkBlue" = () => {
|
|
108
|
+
const {inverted, color} = this.props;
|
|
109
|
+
if (inverted) {
|
|
110
|
+
return "darkBlue";
|
|
111
|
+
} else if (color === "blue") {
|
|
112
|
+
return "blue";
|
|
113
|
+
}
|
|
351
114
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
const {children: _, ...portalProps} = this.props;
|
|
115
|
+
return "white";
|
|
116
|
+
};
|
|
355
117
|
|
|
118
|
+
render(): React.Element<any> {
|
|
119
|
+
const {side, children, content, dismissOnClickClose} = this.props;
|
|
120
|
+
const {onMouseEnter, onMouseLeave} = this.props;
|
|
356
121
|
return (
|
|
357
|
-
<
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
122
|
+
<Popover
|
|
123
|
+
content={
|
|
124
|
+
<PopoverContentCore
|
|
125
|
+
color={this._popoverContentColor()}
|
|
126
|
+
closeButtonVisible={dismissOnClickClose}
|
|
127
|
+
style={{}}
|
|
128
|
+
>
|
|
129
|
+
{content}
|
|
130
|
+
</PopoverContentCore>
|
|
364
131
|
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
132
|
+
placement={side}
|
|
133
|
+
opened={this._shouldShowTooltip()}
|
|
134
|
+
onClose={this._handleDismiss}
|
|
135
|
+
dismissEnabled={false}
|
|
136
|
+
testId="question-id"
|
|
368
137
|
>
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
138
|
+
<div
|
|
139
|
+
onMouseEnter={() => {
|
|
140
|
+
onMouseEnter && onMouseEnter();
|
|
141
|
+
this.setState({hovered: true});
|
|
142
|
+
}}
|
|
143
|
+
onMouseLeave={() => {
|
|
144
|
+
onMouseLeave && onMouseLeave();
|
|
145
|
+
this.setState({hovered: false});
|
|
146
|
+
}}
|
|
147
|
+
>
|
|
148
|
+
{children}
|
|
149
|
+
</div>
|
|
150
|
+
</Popover>
|
|
381
151
|
);
|
|
382
152
|
}
|
|
383
153
|
}
|
|
384
154
|
|
|
385
|
-
NewTooltip.contextTypes = {
|
|
386
|
-
modalContainerElement: PropTypes.instanceOf(HTMLElement),
|
|
387
|
-
};
|
|
388
|
-
|
|
389
|
-
const styles = StyleSheet.create({
|
|
390
|
-
targetElement: {
|
|
391
|
-
// NOTE(mdr): By setting this container to `display: inline`, we avoid
|
|
392
|
-
// disrupting the call site's layout! Yay! Unfortunately, this
|
|
393
|
-
// requires other hacks in order to do position tracking; see the
|
|
394
|
-
// `cloneElement` call in `render`.
|
|
395
|
-
display: "inline",
|
|
396
|
-
},
|
|
397
|
-
});
|
|
398
|
-
|
|
399
155
|
export default NewTooltip;
|