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