@khanacademy/wonder-blocks-tooltip 1.3.0
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/LICENSE +21 -0
- package/dist/es/index.js +1133 -0
- package/dist/index.js +1389 -0
- package/dist/index.js.flow +2 -0
- package/docs.md +11 -0
- package/package.json +37 -0
- package/src/__tests__/__snapshots__/generated-snapshot.test.js.snap +2674 -0
- package/src/__tests__/generated-snapshot.test.js +475 -0
- package/src/components/__tests__/__snapshots__/tooltip-tail.test.js.snap +9 -0
- package/src/components/__tests__/__snapshots__/tooltip.test.js.snap +47 -0
- package/src/components/__tests__/tooltip-anchor.test.js +987 -0
- package/src/components/__tests__/tooltip-bubble.test.js +80 -0
- package/src/components/__tests__/tooltip-popper.test.js +71 -0
- package/src/components/__tests__/tooltip-tail.test.js +117 -0
- package/src/components/__tests__/tooltip.integration.test.js +79 -0
- package/src/components/__tests__/tooltip.test.js +401 -0
- package/src/components/tooltip-anchor.js +330 -0
- package/src/components/tooltip-bubble.js +150 -0
- package/src/components/tooltip-bubble.md +92 -0
- package/src/components/tooltip-content.js +76 -0
- package/src/components/tooltip-content.md +34 -0
- package/src/components/tooltip-popper.js +101 -0
- package/src/components/tooltip-tail.js +462 -0
- package/src/components/tooltip-tail.md +143 -0
- package/src/components/tooltip.js +235 -0
- package/src/components/tooltip.md +194 -0
- package/src/components/tooltip.stories.js +76 -0
- package/src/index.js +12 -0
- package/src/util/__tests__/__snapshots__/active-tracker.test.js.snap +3 -0
- package/src/util/__tests__/__snapshots__/ref-tracker.test.js.snap +3 -0
- package/src/util/__tests__/active-tracker.test.js +142 -0
- package/src/util/__tests__/ref-tracker.test.js +153 -0
- package/src/util/active-tracker.js +94 -0
- package/src/util/constants.js +7 -0
- package/src/util/ref-tracker.js +46 -0
- package/src/util/types.js +29 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
/**
|
|
3
|
+
* This component turns the given content into an accessible anchor for
|
|
4
|
+
* positioning and displaying tooltips.
|
|
5
|
+
*/
|
|
6
|
+
import * as React from "react";
|
|
7
|
+
import * as ReactDOM from "react-dom";
|
|
8
|
+
|
|
9
|
+
import {Text as WBText} from "@khanacademy/wonder-blocks-core";
|
|
10
|
+
import type {IIdentifierFactory} from "@khanacademy/wonder-blocks-core";
|
|
11
|
+
|
|
12
|
+
import ActiveTracker from "../util/active-tracker.js";
|
|
13
|
+
import {
|
|
14
|
+
TooltipAppearanceDelay,
|
|
15
|
+
TooltipDisappearanceDelay,
|
|
16
|
+
} from "../util/constants.js";
|
|
17
|
+
|
|
18
|
+
import type {IActiveTrackerSubscriber} from "../util/active-tracker.js";
|
|
19
|
+
|
|
20
|
+
type Props = {|
|
|
21
|
+
/**
|
|
22
|
+
* The content for anchoring the tooltip.
|
|
23
|
+
* This element will be used to position the tooltip.
|
|
24
|
+
* If a string is passed as children we wrap it in a Text element.
|
|
25
|
+
* We allow children to be a string so that we can add tooltips to
|
|
26
|
+
* words within a large block of text easily.
|
|
27
|
+
*/
|
|
28
|
+
children: React.Element<any> | string,
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Callback to be invoked when the anchored content is mounted.
|
|
32
|
+
* This provides a reference to the anchored content, which can then be
|
|
33
|
+
* used for calculating tooltip bubble positioning.
|
|
34
|
+
*/
|
|
35
|
+
anchorRef: (?Element) => mixed,
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* When true, if a tabindex attribute is not already present on the element
|
|
39
|
+
* wrapped by the anchor, the element will be given tabindex=0 to make it
|
|
40
|
+
* keyboard focusable; otherwise, does not attempt to change the ability to
|
|
41
|
+
* focus the anchor element.
|
|
42
|
+
*
|
|
43
|
+
* Defaults to true.
|
|
44
|
+
*
|
|
45
|
+
* One might set this to false in circumstances where the wrapped component
|
|
46
|
+
* already can receive focus or contains an element that can.
|
|
47
|
+
* Use good judgement when overriding this value, the tooltip content should
|
|
48
|
+
* be accessible via keyboard in all circumstances where the tooltip would
|
|
49
|
+
* appear using the mouse, so verify those use-cases.
|
|
50
|
+
*/
|
|
51
|
+
forceAnchorFocusivity?: boolean,
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Callback to pass active state back to Tooltip.
|
|
55
|
+
*
|
|
56
|
+
* `active` will be true whenever the anchor is hovered or focused and false
|
|
57
|
+
* otherwise.
|
|
58
|
+
*/
|
|
59
|
+
onActiveChanged: (active: boolean) => mixed,
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Optional unique id factory.
|
|
63
|
+
*/
|
|
64
|
+
ids?: IIdentifierFactory,
|
|
65
|
+
|};
|
|
66
|
+
|
|
67
|
+
type DefaultProps = {|
|
|
68
|
+
forceAnchorFocusivity: $PropertyType<Props, "forceAnchorFocusivity">,
|
|
69
|
+
|};
|
|
70
|
+
|
|
71
|
+
type State = {|
|
|
72
|
+
/** Is the anchor active or not? */
|
|
73
|
+
active: boolean,
|
|
74
|
+
|};
|
|
75
|
+
|
|
76
|
+
const TRACKER = new ActiveTracker();
|
|
77
|
+
|
|
78
|
+
export default class TooltipAnchor
|
|
79
|
+
extends React.Component<Props, State>
|
|
80
|
+
implements IActiveTrackerSubscriber {
|
|
81
|
+
_weSetFocusivity: ?boolean;
|
|
82
|
+
_anchorNode: ?Element;
|
|
83
|
+
_focused: boolean;
|
|
84
|
+
_hovered: boolean;
|
|
85
|
+
_stolenFromUs: boolean;
|
|
86
|
+
_unsubscribeFromTracker: ?() => void;
|
|
87
|
+
_timeoutID: ?TimeoutID;
|
|
88
|
+
|
|
89
|
+
static defaultProps: DefaultProps = {
|
|
90
|
+
forceAnchorFocusivity: true,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
constructor(props: Props) {
|
|
94
|
+
super(props);
|
|
95
|
+
|
|
96
|
+
this._focused = false;
|
|
97
|
+
this._hovered = false;
|
|
98
|
+
this.state = {
|
|
99
|
+
active: false,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
componentDidMount() {
|
|
104
|
+
const anchorNode = ReactDOM.findDOMNode(this);
|
|
105
|
+
|
|
106
|
+
// This should never happen, but we have this check here to make flow
|
|
107
|
+
// happy and ensure that if this does happen, we'll know about it.
|
|
108
|
+
if (anchorNode instanceof Text) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
"TooltipAnchor must be applied to an Element. Text content is not supported.",
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
this._unsubscribeFromTracker = TRACKER.subscribe(this);
|
|
115
|
+
this._anchorNode = anchorNode;
|
|
116
|
+
this._updateFocusivity();
|
|
117
|
+
if (anchorNode) {
|
|
118
|
+
/**
|
|
119
|
+
* TODO(somewhatabstract): Work out how to allow pointer to go over
|
|
120
|
+
* the tooltip content to keep it active. This likely requires
|
|
121
|
+
* pointer events but that would break the obscurement checks we do.
|
|
122
|
+
* So, careful consideration required. See WB-302.
|
|
123
|
+
*/
|
|
124
|
+
anchorNode.addEventListener("focusin", this._handleFocusIn);
|
|
125
|
+
anchorNode.addEventListener("focusout", this._handleFocusOut);
|
|
126
|
+
anchorNode.addEventListener("mouseenter", this._handleMouseEnter);
|
|
127
|
+
anchorNode.addEventListener("mouseleave", this._handleMouseLeave);
|
|
128
|
+
|
|
129
|
+
this.props.anchorRef(this._anchorNode);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
componentDidUpdate(prevProps: Props) {
|
|
134
|
+
if (
|
|
135
|
+
prevProps.forceAnchorFocusivity !==
|
|
136
|
+
this.props.forceAnchorFocusivity ||
|
|
137
|
+
prevProps.children !== this.props.children
|
|
138
|
+
) {
|
|
139
|
+
this._updateFocusivity();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
componentWillUnmount() {
|
|
144
|
+
if (this._unsubscribeFromTracker) {
|
|
145
|
+
this._unsubscribeFromTracker();
|
|
146
|
+
}
|
|
147
|
+
this._clearPendingAction();
|
|
148
|
+
|
|
149
|
+
const anchorNode = this._anchorNode;
|
|
150
|
+
if (anchorNode) {
|
|
151
|
+
anchorNode.removeEventListener("focusin", this._handleFocusIn);
|
|
152
|
+
anchorNode.removeEventListener("focusout", this._handleFocusOut);
|
|
153
|
+
anchorNode.removeEventListener(
|
|
154
|
+
"mouseenter",
|
|
155
|
+
this._handleMouseEnter,
|
|
156
|
+
);
|
|
157
|
+
anchorNode.removeEventListener(
|
|
158
|
+
"mouseleave",
|
|
159
|
+
this._handleMouseLeave,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
if (this.state.active) {
|
|
163
|
+
document.removeEventListener("keyup", this._handleKeyUp);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
static ariaContentId: string = "aria-content";
|
|
168
|
+
|
|
169
|
+
activeStateStolen: () => void = () => {
|
|
170
|
+
// Something wants the active state.
|
|
171
|
+
// Do we have it? If so, let's remember that.
|
|
172
|
+
// If we are already active, or we're inactive but have a timeoutID,
|
|
173
|
+
// then it was stolen from us.
|
|
174
|
+
this._stolenFromUs = this.state.active || !!this._timeoutID;
|
|
175
|
+
// Let's first tell ourselves we're not focused (otherwise the tooltip
|
|
176
|
+
// will be sticky on the next hover of this anchor and that just looks
|
|
177
|
+
// weird).
|
|
178
|
+
this._focused = false;
|
|
179
|
+
// Now update our actual state.
|
|
180
|
+
this._setActiveState(false, true);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
_updateFocusivity() {
|
|
184
|
+
const anchorNode = this._anchorNode;
|
|
185
|
+
if (!anchorNode) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const {forceAnchorFocusivity} = this.props;
|
|
189
|
+
const currentTabIndex = anchorNode.getAttribute("tabindex");
|
|
190
|
+
|
|
191
|
+
if (forceAnchorFocusivity && !currentTabIndex) {
|
|
192
|
+
// Ensure that the anchor point is keyboard focusable so that
|
|
193
|
+
// we can show the tooltip for visually impaired users that don't
|
|
194
|
+
// use pointer devices nor assistive technology like screen readers.
|
|
195
|
+
anchorNode.setAttribute("tabindex", "0");
|
|
196
|
+
this._weSetFocusivity = true;
|
|
197
|
+
} else if (!forceAnchorFocusivity && currentTabIndex) {
|
|
198
|
+
// We may not be forcing it, but we also want to ensure that if we
|
|
199
|
+
// did before, we remove it.
|
|
200
|
+
if (this._weSetFocusivity) {
|
|
201
|
+
anchorNode.removeAttribute("tabindex");
|
|
202
|
+
this._weSetFocusivity = false;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
_updateActiveState(hovered: boolean, focused: boolean) {
|
|
208
|
+
// Update our stored values.
|
|
209
|
+
this._hovered = hovered;
|
|
210
|
+
this._focused = focused;
|
|
211
|
+
|
|
212
|
+
this._setActiveState(hovered || focused);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
_clearPendingAction() {
|
|
216
|
+
if (this._timeoutID) {
|
|
217
|
+
clearTimeout(this._timeoutID);
|
|
218
|
+
this._timeoutID = null;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
_setActiveState(active: boolean, instant?: boolean) {
|
|
223
|
+
if (
|
|
224
|
+
this._stolenFromUs ||
|
|
225
|
+
active !== this.state.active ||
|
|
226
|
+
(!this.state.active && this._timeoutID)
|
|
227
|
+
) {
|
|
228
|
+
// If we are about to lose active state or change it, we need to
|
|
229
|
+
// cancel any pending action to show ourselves.
|
|
230
|
+
// So, if active is stolen from us, we are changing active state,
|
|
231
|
+
// or we are inactive and have a timer, clear the action.
|
|
232
|
+
this._clearPendingAction();
|
|
233
|
+
} else if (active === this.state.active && !this._timeoutID) {
|
|
234
|
+
// Nothing to do if we're already active.
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Determine if we are doing things immediately or not.
|
|
239
|
+
instant = instant || (active && TRACKER.steal(this));
|
|
240
|
+
|
|
241
|
+
if (instant) {
|
|
242
|
+
if (active) {
|
|
243
|
+
document.addEventListener("keyup", this._handleKeyUp);
|
|
244
|
+
} else {
|
|
245
|
+
document.removeEventListener("keyup", this._handleKeyUp);
|
|
246
|
+
}
|
|
247
|
+
this.setState({active});
|
|
248
|
+
this.props.onActiveChanged(active);
|
|
249
|
+
if (!this._stolenFromUs && !active) {
|
|
250
|
+
// Only the very last thing going inactive will giveup
|
|
251
|
+
// the stolen active state.
|
|
252
|
+
TRACKER.giveup();
|
|
253
|
+
}
|
|
254
|
+
this._stolenFromUs = false;
|
|
255
|
+
} else {
|
|
256
|
+
const delay = active
|
|
257
|
+
? TooltipAppearanceDelay
|
|
258
|
+
: TooltipDisappearanceDelay;
|
|
259
|
+
this._timeoutID = setTimeout(() => {
|
|
260
|
+
this._timeoutID = null;
|
|
261
|
+
this._setActiveState(active, true);
|
|
262
|
+
}, delay);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
_handleFocusIn: () => void = () => {
|
|
267
|
+
this._updateActiveState(this._hovered, true);
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
_handleFocusOut: () => void = () => {
|
|
271
|
+
this._updateActiveState(this._hovered, false);
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
_handleMouseEnter: () => void = () => {
|
|
275
|
+
this._updateActiveState(true, this._focused);
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
_handleMouseLeave: () => void = () => {
|
|
279
|
+
this._updateActiveState(false, this._focused);
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
_handleKeyUp: (e: KeyboardEvent) => void = (e) => {
|
|
283
|
+
// We check the key as that's keyboard layout agnostic and also avoids
|
|
284
|
+
// the minefield of deprecated number type properties like keyCode and
|
|
285
|
+
// which, with the replacement code, which uses a string instead.
|
|
286
|
+
if (e.key === "Escape" && this.state.active) {
|
|
287
|
+
// Stop the event going any further.
|
|
288
|
+
// For cancellation events, like the Escape key, we generally should
|
|
289
|
+
// air on the side of caution and only allow it to cancel one thing.
|
|
290
|
+
// So, it's polite for us to stop propagation of the event.
|
|
291
|
+
// Otherwise, we end up with UX where one Escape key press
|
|
292
|
+
// unexpectedly cancels multiple things.
|
|
293
|
+
//
|
|
294
|
+
// For example, using Escape to close a tooltip or a dropdown while
|
|
295
|
+
// displaying a modal and having the modal close as well. This would
|
|
296
|
+
// be annoyingly bad UX.
|
|
297
|
+
e.preventDefault();
|
|
298
|
+
e.stopPropagation();
|
|
299
|
+
this._updateActiveState(false, false);
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
_renderAnchorableChildren(): React.Element<any> {
|
|
304
|
+
const {children} = this.props;
|
|
305
|
+
return typeof children === "string" ? (
|
|
306
|
+
<WBText>{children}</WBText>
|
|
307
|
+
) : (
|
|
308
|
+
children
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
_renderAccessibleChildren(ids: IIdentifierFactory): React.Node {
|
|
313
|
+
const anchorableChildren = this._renderAnchorableChildren();
|
|
314
|
+
|
|
315
|
+
return React.cloneElement(anchorableChildren, {
|
|
316
|
+
"aria-describedby": ids.get(TooltipAnchor.ariaContentId),
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
render(): React.Node {
|
|
321
|
+
// We need to make sure we can anchor on our content.
|
|
322
|
+
// If the content is just a string, we wrap it in a Text element
|
|
323
|
+
// so as not to affect styling or layout but still have an element
|
|
324
|
+
// to anchor to.
|
|
325
|
+
if (this.props.ids) {
|
|
326
|
+
return this._renderAccessibleChildren(this.props.ids);
|
|
327
|
+
}
|
|
328
|
+
return this._renderAnchorableChildren();
|
|
329
|
+
}
|
|
330
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import {StyleSheet} from "aphrodite";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import Colors from "@khanacademy/wonder-blocks-color";
|
|
5
|
+
import {View} from "@khanacademy/wonder-blocks-core";
|
|
6
|
+
import Spacing from "@khanacademy/wonder-blocks-spacing";
|
|
7
|
+
|
|
8
|
+
import type {StyleType} from "@khanacademy/wonder-blocks-core";
|
|
9
|
+
import typeof TooltipContent from "./tooltip-content.js";
|
|
10
|
+
import TooltipTail from "./tooltip-tail.js";
|
|
11
|
+
|
|
12
|
+
import type {getRefFn, Offset, Placement} from "../util/types.js";
|
|
13
|
+
|
|
14
|
+
export type PopperElementProps = {|
|
|
15
|
+
/** The placement of the bubble with respect to the anchor. */
|
|
16
|
+
placement: Placement,
|
|
17
|
+
|
|
18
|
+
/** Whether the bubble is out of bounds or not. */
|
|
19
|
+
isReferenceHidden?: ?boolean,
|
|
20
|
+
|
|
21
|
+
/** A callback for updating the ref of the bubble itself. */
|
|
22
|
+
updateBubbleRef?: getRefFn,
|
|
23
|
+
|
|
24
|
+
/** A callback for updating the ref of the bubble's tail. */
|
|
25
|
+
updateTailRef?: getRefFn,
|
|
26
|
+
|
|
27
|
+
/** Where the tail is to be rendered. */
|
|
28
|
+
tailOffset?: Offset,
|
|
29
|
+
|
|
30
|
+
/** Additional styles to be applied by the bubble. */
|
|
31
|
+
style?: StyleType,
|
|
32
|
+
|};
|
|
33
|
+
|
|
34
|
+
export type Props = {|
|
|
35
|
+
/** The unique identifier for this component. */
|
|
36
|
+
id: string,
|
|
37
|
+
|
|
38
|
+
/** The `TooltipContent` element that will be rendered in the bubble. */
|
|
39
|
+
children: React.Element<TooltipContent>,
|
|
40
|
+
|
|
41
|
+
onActiveChanged: (active: boolean) => mixed,
|
|
42
|
+
|
|
43
|
+
// TODO(somewhatabstract): Update react-docgen to support spread operators
|
|
44
|
+
// (v3 beta introduces this)
|
|
45
|
+
...PopperElementProps,
|
|
46
|
+
|};
|
|
47
|
+
|
|
48
|
+
type State = {|
|
|
49
|
+
active: boolean,
|
|
50
|
+
|};
|
|
51
|
+
|
|
52
|
+
export default class TooltipBubble extends React.Component<Props, State> {
|
|
53
|
+
state: State = {
|
|
54
|
+
active: false,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
_setActiveState(active: boolean) {
|
|
58
|
+
this.setState({active});
|
|
59
|
+
this.props.onActiveChanged(active);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
handleMouseEnter: () => void = () => {
|
|
63
|
+
this._setActiveState(true);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
handleMouseLeave: () => void = () => {
|
|
67
|
+
this.props.onActiveChanged(false);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
render(): React.Node {
|
|
71
|
+
const {
|
|
72
|
+
id,
|
|
73
|
+
children,
|
|
74
|
+
updateBubbleRef,
|
|
75
|
+
placement,
|
|
76
|
+
isReferenceHidden,
|
|
77
|
+
style,
|
|
78
|
+
updateTailRef,
|
|
79
|
+
tailOffset,
|
|
80
|
+
} = this.props;
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<View
|
|
84
|
+
id={id}
|
|
85
|
+
role="tooltip"
|
|
86
|
+
data-placement={placement}
|
|
87
|
+
onMouseEnter={this.handleMouseEnter}
|
|
88
|
+
onMouseLeave={this.handleMouseLeave}
|
|
89
|
+
ref={updateBubbleRef}
|
|
90
|
+
style={[
|
|
91
|
+
isReferenceHidden && styles.hide,
|
|
92
|
+
styles.bubble,
|
|
93
|
+
styles[`content-${placement}`],
|
|
94
|
+
style,
|
|
95
|
+
]}
|
|
96
|
+
>
|
|
97
|
+
<View style={styles.content}>{children}</View>
|
|
98
|
+
<TooltipTail
|
|
99
|
+
updateRef={updateTailRef}
|
|
100
|
+
placement={placement}
|
|
101
|
+
offset={tailOffset}
|
|
102
|
+
/>
|
|
103
|
+
</View>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const styles = StyleSheet.create({
|
|
109
|
+
bubble: {
|
|
110
|
+
position: "absolute",
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The hide style ensures that the bounds of the bubble stay unchanged.
|
|
115
|
+
* This is because popper.js calculates the bubble position based off its
|
|
116
|
+
* bounds and if we stopped rendering it entirely, it wouldn't know where to
|
|
117
|
+
* place it when it reappeared.
|
|
118
|
+
*/
|
|
119
|
+
hide: {
|
|
120
|
+
pointerEvents: "none",
|
|
121
|
+
opacity: 0,
|
|
122
|
+
backgroundColor: "transparent",
|
|
123
|
+
color: "transparent",
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Ensure the content and tail are properly arranged.
|
|
128
|
+
*/
|
|
129
|
+
"content-top": {
|
|
130
|
+
flexDirection: "column",
|
|
131
|
+
},
|
|
132
|
+
"content-right": {
|
|
133
|
+
flexDirection: "row-reverse",
|
|
134
|
+
},
|
|
135
|
+
"content-bottom": {
|
|
136
|
+
flexDirection: "column-reverse",
|
|
137
|
+
},
|
|
138
|
+
"content-left": {
|
|
139
|
+
flexDirection: "row",
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
content: {
|
|
143
|
+
maxWidth: 472,
|
|
144
|
+
borderRadius: Spacing.xxxSmall_4,
|
|
145
|
+
border: `solid 1px ${Colors.offBlack16}`,
|
|
146
|
+
backgroundColor: Colors.white,
|
|
147
|
+
boxShadow: `0 ${Spacing.xSmall_8}px ${Spacing.xSmall_8}px 0 ${Colors.offBlack8}`,
|
|
148
|
+
justifyContent: "center",
|
|
149
|
+
},
|
|
150
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
This is an internal component that we use to render the stuff that appears when a tooltip shows.
|
|
2
|
+
|
|
3
|
+
Note that without explicit positioning, the tail will not be centered.
|
|
4
|
+
|
|
5
|
+
### Placement top
|
|
6
|
+
|
|
7
|
+
```jsx
|
|
8
|
+
import {View} from "@khanacademy/wonder-blocks-core";
|
|
9
|
+
import {TooltipContent} from "@khanacademy/wonder-blocks-tooltip";
|
|
10
|
+
|
|
11
|
+
<View>
|
|
12
|
+
<TooltipBubble placement="top" style={{position: "relative"}}>
|
|
13
|
+
<TooltipContent>I'm on the top!</TooltipContent>
|
|
14
|
+
</TooltipBubble>
|
|
15
|
+
</View>
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
### Placement right
|
|
19
|
+
|
|
20
|
+
```jsx
|
|
21
|
+
import {View} from "@khanacademy/wonder-blocks-core";
|
|
22
|
+
import {TooltipContent} from "@khanacademy/wonder-blocks-tooltip";
|
|
23
|
+
|
|
24
|
+
<View style={{alignItems: "flex-start"}}>
|
|
25
|
+
<TooltipBubble placement="right" style={{position: "relative"}}>
|
|
26
|
+
<TooltipContent>I'm on the right!</TooltipContent>
|
|
27
|
+
</TooltipBubble>
|
|
28
|
+
</View>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Placement bottom
|
|
32
|
+
|
|
33
|
+
```jsx
|
|
34
|
+
import {View} from "@khanacademy/wonder-blocks-core";
|
|
35
|
+
import {TooltipContent} from "@khanacademy/wonder-blocks-tooltip";
|
|
36
|
+
|
|
37
|
+
<View>
|
|
38
|
+
<TooltipBubble placement="bottom" style={{position: "relative"}}>
|
|
39
|
+
<TooltipContent>I'm on the bottom!</TooltipContent>
|
|
40
|
+
</TooltipBubble>
|
|
41
|
+
</View>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Placement left
|
|
45
|
+
|
|
46
|
+
```jsx
|
|
47
|
+
import {View} from "@khanacademy/wonder-blocks-core";
|
|
48
|
+
import {TooltipContent} from "@khanacademy/wonder-blocks-tooltip";
|
|
49
|
+
|
|
50
|
+
<View>
|
|
51
|
+
<TooltipBubble placement="left" style={{position: "relative"}}>
|
|
52
|
+
<TooltipContent>I'm on the left!</TooltipContent>
|
|
53
|
+
</TooltipBubble>
|
|
54
|
+
</View>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Positioning the tail
|
|
58
|
+
Here we tell the tail that it's lefthand side is at 50px.
|
|
59
|
+
|
|
60
|
+
```jsx
|
|
61
|
+
import {View} from "@khanacademy/wonder-blocks-core";
|
|
62
|
+
import Spacing from "@khanacademy/wonder-blocks-spacing";
|
|
63
|
+
import {TooltipContent} from "@khanacademy/wonder-blocks-tooltip";
|
|
64
|
+
|
|
65
|
+
<View>
|
|
66
|
+
<TooltipBubble
|
|
67
|
+
placement="bottom"
|
|
68
|
+
tailOffset={{left: 50, top: 0}}
|
|
69
|
+
style={{position: "relative"}}
|
|
70
|
+
>
|
|
71
|
+
<TooltipContent>I'm on the bottom with a tail 50px in!</TooltipContent>
|
|
72
|
+
</TooltipBubble>
|
|
73
|
+
</View>
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Hidden because isReferenceHidden is true
|
|
77
|
+
|
|
78
|
+
```jsx
|
|
79
|
+
import {View} from "@khanacademy/wonder-blocks-core";
|
|
80
|
+
import Spacing from "@khanacademy/wonder-blocks-spacing";
|
|
81
|
+
import {TooltipContent} from "@khanacademy/wonder-blocks-tooltip";
|
|
82
|
+
|
|
83
|
+
<View>
|
|
84
|
+
<TooltipBubble
|
|
85
|
+
placement="top"
|
|
86
|
+
isReferenceHidden={true}
|
|
87
|
+
style={{position: "relative"}}
|
|
88
|
+
>
|
|
89
|
+
<TooltipContent>I'm hidden. So hidden. Shhhhh!</TooltipContent>
|
|
90
|
+
</TooltipBubble>
|
|
91
|
+
</View>
|
|
92
|
+
```
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import {StyleSheet} from "aphrodite";
|
|
4
|
+
|
|
5
|
+
import {View} from "@khanacademy/wonder-blocks-core";
|
|
6
|
+
import {Strut} from "@khanacademy/wonder-blocks-layout";
|
|
7
|
+
import Spacing from "@khanacademy/wonder-blocks-spacing";
|
|
8
|
+
import {HeadingSmall, LabelMedium} from "@khanacademy/wonder-blocks-typography";
|
|
9
|
+
|
|
10
|
+
import type {Typography} from "@khanacademy/wonder-blocks-typography";
|
|
11
|
+
|
|
12
|
+
type Props = {|
|
|
13
|
+
/**
|
|
14
|
+
* The title for the tooltip content.
|
|
15
|
+
* Optional.
|
|
16
|
+
*/
|
|
17
|
+
title?: string | React.Element<Typography>,
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The main content for a tooltip.
|
|
21
|
+
*/
|
|
22
|
+
children:
|
|
23
|
+
| string
|
|
24
|
+
| React.Element<Typography>
|
|
25
|
+
| Array<React.Element<Typography>>,
|
|
26
|
+
|};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* This component is used to provide the content that is to be rendered in the
|
|
30
|
+
* tooltip bubble.
|
|
31
|
+
*/
|
|
32
|
+
export default class TooltipContent extends React.Component<Props> {
|
|
33
|
+
_renderTitle(): React.Node {
|
|
34
|
+
const {title} = this.props;
|
|
35
|
+
if (title) {
|
|
36
|
+
if (typeof title === "string") {
|
|
37
|
+
return <HeadingSmall>{title}</HeadingSmall>;
|
|
38
|
+
} else {
|
|
39
|
+
return title;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
_renderChildren(): React.Node {
|
|
46
|
+
const {children} = this.props;
|
|
47
|
+
if (typeof children === "string") {
|
|
48
|
+
return <LabelMedium>{children}</LabelMedium>;
|
|
49
|
+
} else {
|
|
50
|
+
return children;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
render(): React.Node {
|
|
55
|
+
const title = this._renderTitle();
|
|
56
|
+
const children = this._renderChildren();
|
|
57
|
+
const containerStyle = title ? styles.withTitle : styles.withoutTitle;
|
|
58
|
+
return (
|
|
59
|
+
<View style={containerStyle}>
|
|
60
|
+
{title}
|
|
61
|
+
{title && children && <Strut size={Spacing.xxxSmall_4} />}
|
|
62
|
+
{children}
|
|
63
|
+
</View>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const styles = StyleSheet.create({
|
|
69
|
+
withoutTitle: {
|
|
70
|
+
padding: `10px ${Spacing.medium_16}px`,
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
withTitle: {
|
|
74
|
+
padding: Spacing.medium_16,
|
|
75
|
+
},
|
|
76
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
The `TooltipContent` component is provided for situations where the `Tooltip` needs to be customized beyond the default stylings. `TooltipContent` supports all `wonder-blocks-typography` components.
|
|
2
|
+
|
|
3
|
+
### Only text content
|
|
4
|
+
|
|
5
|
+
This shows the default which is text rendered using `LabelMedium`.
|
|
6
|
+
|
|
7
|
+
```jsx
|
|
8
|
+
<TooltipContent>
|
|
9
|
+
Only the content
|
|
10
|
+
</TooltipContent>
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### Titled content
|
|
14
|
+
|
|
15
|
+
This shows the default with a title; the title is rendered using `HeadingSmall`.
|
|
16
|
+
|
|
17
|
+
```jsx
|
|
18
|
+
<TooltipContent title="Title text!">
|
|
19
|
+
Some content in my content
|
|
20
|
+
</TooltipContent>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Custom title and custom content
|
|
24
|
+
|
|
25
|
+
This shows how we can customize both the title and the content.
|
|
26
|
+
|
|
27
|
+
```jsx
|
|
28
|
+
const {Body, LabelSmall} = require("@khanacademy/wonder-blocks-typography");
|
|
29
|
+
|
|
30
|
+
<TooltipContent title={<Body>Body text title!</Body>}>
|
|
31
|
+
<Body>Body text content!</Body>
|
|
32
|
+
<LabelSmall>And LabelSmall!</LabelSmall>
|
|
33
|
+
</TooltipContent>
|
|
34
|
+
```
|