@khanacademy/perseus 0.4.1 → 0.4.3
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/.eslintrc.js +6 -0
- package/CHANGELOG.md +22 -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 +12 -9
- 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
|
@@ -1,529 +0,0 @@
|
|
|
1
|
-
// @flow
|
|
2
|
-
/**
|
|
3
|
-
* This is TooltipPositioner, part of NewTooltip.
|
|
4
|
-
*
|
|
5
|
-
* This component is responsible for positioning the tooltip UI.
|
|
6
|
-
*
|
|
7
|
-
* (For the sake of example, we'll just talk about tooltips with side="top".
|
|
8
|
-
* For other `side` values, swap out all the direction-y words in this
|
|
9
|
-
* docstring accordingly!)
|
|
10
|
-
*
|
|
11
|
-
* First, this component measures the position of the "target point", the
|
|
12
|
-
* top-center of the target element.
|
|
13
|
-
*
|
|
14
|
-
* Then, it absolute-positions (ish) a "tooltip bubble" (which holds
|
|
15
|
-
* the tooltip contents) to have its bottom-center point exactly align (ish)
|
|
16
|
-
* with the target point.
|
|
17
|
-
*
|
|
18
|
-
* Then, it absolute-positions (precisely) a "tooltip arrow" (a triangle that's
|
|
19
|
-
* visually connected to the bubble) to point from the bottom of the tooltip
|
|
20
|
-
* into the target element.
|
|
21
|
-
*
|
|
22
|
-
* Note that the tooltip bubble is positioned "ish", whereas the tooltip arrow
|
|
23
|
-
* is positioned "precisely". In most cases, the tooltip bubble and tooltip
|
|
24
|
-
* arrow will be centered relative to one another. However, if the tooltip
|
|
25
|
-
* bubble can't quite fit in the available space, then it will shift to the
|
|
26
|
-
* left or right until it fits.
|
|
27
|
-
*
|
|
28
|
-
* So, the tooltip bubble may not be centered on the target element, but the
|
|
29
|
-
* tooltip arrow will be - which produces the effect that the arrow won't
|
|
30
|
-
* always be centered on the tooltip bubble. And that's okay! It's better than
|
|
31
|
-
* the alternative, which is to keep the bubble centered and aggressively wrap
|
|
32
|
-
* it until it fits. (This looks verrry bad in some contexts :P)
|
|
33
|
-
*
|
|
34
|
-
* This flexible positioning strategy is encapsulated in the FlexiblePosition
|
|
35
|
-
* component. Look there for more details!
|
|
36
|
-
*
|
|
37
|
-
* Here's a set of three examples, demonstrating the positioning strategy.
|
|
38
|
-
* Pay special attention to the position of the arrow! (the "v" character)
|
|
39
|
-
*
|
|
40
|
-
* .---------------. .---------------. .---------------.
|
|
41
|
-
* | Hello, world! | | Hello, world! | | Hello, world! |
|
|
42
|
-
* '-v-------------' '-------v-------' '-------------v-'
|
|
43
|
-
* <hi!> <hi!> <hi!>
|
|
44
|
-
*
|
|
45
|
-
* In order to keep the tooltip aligned with the target element, we listen for
|
|
46
|
-
* events that might cause the target element to move, like window resizes, or
|
|
47
|
-
* scroll events. This behavior is liable to cause performance issues, so we'll
|
|
48
|
-
* need to keep an eye on this logic, and make clever compromises where
|
|
49
|
-
* necessary!
|
|
50
|
-
*
|
|
51
|
-
* This component has a public method `remeasure`, to handle `remeasure` calls
|
|
52
|
-
* passed down from NewTooltip. Calling this method will cause the bubble and
|
|
53
|
-
* arrow to update their position.
|
|
54
|
-
*
|
|
55
|
-
* NOTE(mdr): This component assumes that it's mounted in the root node
|
|
56
|
-
* provided via props. Do not attempt to mount this component in another
|
|
57
|
-
* context.
|
|
58
|
-
*/
|
|
59
|
-
import {StyleSheet, css} from "aphrodite";
|
|
60
|
-
import * as React from "react";
|
|
61
|
-
|
|
62
|
-
import {Errors} from "../../logging/log.js";
|
|
63
|
-
import {PerseusError} from "../../perseus-error.js";
|
|
64
|
-
import {
|
|
65
|
-
zindexTooltip,
|
|
66
|
-
zindexAboveModal,
|
|
67
|
-
} from "../../styles/global-constants.js";
|
|
68
|
-
|
|
69
|
-
import FlexiblePosition from "./flexible-position.jsx";
|
|
70
|
-
import {
|
|
71
|
-
arrowHeight,
|
|
72
|
-
arrowWidth,
|
|
73
|
-
bubbleBorderRadius,
|
|
74
|
-
} from "./style-constants.js";
|
|
75
|
-
import TooltipArrow from "./tooltip-arrow.jsx";
|
|
76
|
-
import TooltipBubble from "./tooltip-bubble.jsx";
|
|
77
|
-
|
|
78
|
-
import type {NewTooltipProps} from "./types.js";
|
|
79
|
-
|
|
80
|
-
type TooltipPositionerProps = {
|
|
81
|
-
tooltipProps: NewTooltipProps,
|
|
82
|
-
dismiss: () => void,
|
|
83
|
-
rootElement: HTMLElement,
|
|
84
|
-
targetElement: HTMLElement,
|
|
85
|
-
isAboveModal?: boolean,
|
|
86
|
-
...
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
type TooltipPositionerState = {
|
|
90
|
-
targetElementClientRect: ?ClientRect,
|
|
91
|
-
rootElementClientRect: ?ClientRect,
|
|
92
|
-
...
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
type TargetPointOffsets = {
|
|
96
|
-
pxFromTop: number,
|
|
97
|
-
pxFromBottom: number,
|
|
98
|
-
pxFromLeft: number,
|
|
99
|
-
pxFromRight: number,
|
|
100
|
-
...
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
export default class TooltipPositioner extends React.PureComponent<
|
|
104
|
-
TooltipPositionerProps,
|
|
105
|
-
TooltipPositionerState,
|
|
106
|
-
> {
|
|
107
|
-
_mountRemeasureTimeout: ?TimeoutID;
|
|
108
|
-
|
|
109
|
-
state: TooltipPositionerState = {
|
|
110
|
-
targetElementClientRect: null,
|
|
111
|
-
rootElementClientRect: null,
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
componentDidMount() {
|
|
115
|
-
// Wait a tick, then measure the elements.
|
|
116
|
-
//
|
|
117
|
-
// NOTE(mdr): This is necessary for all tooltips in IE and Edge, and
|
|
118
|
-
// some tooltips in other browsers, but I don't understand why.
|
|
119
|
-
// NewTooltip already waits a tick after mount before rendering
|
|
120
|
-
// this React subtree, so I would've expected styles to be present
|
|
121
|
-
// already when this element was initialized. But, without this,
|
|
122
|
-
// many tooltips are mis-positioned on IE and Edge, and the
|
|
123
|
-
// `CrossOutTooltip` is apparently always mispositioned, regardless
|
|
124
|
-
// of browser. So, shrug!
|
|
125
|
-
// TODO(jeff, WEB-1378): Use Wonder Blocks Timing API.
|
|
126
|
-
// eslint-disable-next-line no-restricted-syntax
|
|
127
|
-
this._mountRemeasureTimeout = setTimeout(this.remeasure, 0);
|
|
128
|
-
|
|
129
|
-
window.addEventListener("resize", this.remeasure);
|
|
130
|
-
window.addEventListener("scroll", this._handleScroll, true);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
componentWillUnmount() {
|
|
134
|
-
if (this._mountRemeasureTimeout) {
|
|
135
|
-
// TODO(jeff, WEB-1378): Use Wonder Blocks Timing API.
|
|
136
|
-
// eslint-disable-next-line no-restricted-syntax
|
|
137
|
-
clearTimeout(this._mountRemeasureTimeout);
|
|
138
|
-
}
|
|
139
|
-
window.removeEventListener("resize", this.remeasure);
|
|
140
|
-
window.removeEventListener("scroll", this._handleScroll, true);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* This is a global scroll handler, which detects all scroll events on the
|
|
145
|
-
* page. As a quick check before proceeding, we query whether the scroll
|
|
146
|
-
* container sits between the root and the target element. If so, this
|
|
147
|
-
* probably affected position, and we should `remeasure` ourselves. If not,
|
|
148
|
-
* do nothing.
|
|
149
|
-
*
|
|
150
|
-
* NOTE(mdr): This is roughly the same check as in VisibilityObserver.
|
|
151
|
-
*/
|
|
152
|
-
_handleScroll: (e: Event) => void = (e: Event) => {
|
|
153
|
-
const {rootElement, targetElement} = this.props;
|
|
154
|
-
const scrollContainer: HTMLElement = (e.target: any);
|
|
155
|
-
|
|
156
|
-
if (
|
|
157
|
-
rootElement.contains(scrollContainer) &&
|
|
158
|
-
scrollContainer.contains(targetElement)
|
|
159
|
-
) {
|
|
160
|
-
this.remeasure();
|
|
161
|
-
}
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Find the "target point" of the target element: the place where the
|
|
166
|
-
* tooltip arrow will point.
|
|
167
|
-
*
|
|
168
|
-
* When side="top", this is the top-center point.
|
|
169
|
-
* When side="bottom", this is the bottom-center point.
|
|
170
|
-
* When side="left", this is the left-center point.
|
|
171
|
-
* When side="right", this is the right-center point.
|
|
172
|
-
*
|
|
173
|
-
* Compute the target point's offsets from various sides of the screen,
|
|
174
|
-
* and return them. We'll use these offsets to position the tooltip bubble
|
|
175
|
-
* and arrow.
|
|
176
|
-
*/
|
|
177
|
-
_computeTargetPointOffsets(
|
|
178
|
-
targetElementClientRect: ClientRect,
|
|
179
|
-
rootElementClientRect: ClientRect,
|
|
180
|
-
): TargetPointOffsets {
|
|
181
|
-
const {offset, offsetFrom, side} = this.props.tooltipProps;
|
|
182
|
-
const t = targetElementClientRect;
|
|
183
|
-
const r = rootElementClientRect;
|
|
184
|
-
|
|
185
|
-
// When `offsetFrom` is "arrow", we need to perform offset from the
|
|
186
|
-
// arrow, rather than the bubble. To do this, we simply increase the
|
|
187
|
-
// offset amount by the arrow's height.
|
|
188
|
-
let totalOffset = offset;
|
|
189
|
-
if (offsetFrom === "arrow") {
|
|
190
|
-
totalOffset += arrowHeight;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// Compute the position of the target point, relative to the viewport.
|
|
194
|
-
// Our choice of target point depends on the `side` prop, and will be
|
|
195
|
-
// shifted according to the `offset` prop.
|
|
196
|
-
let targetPoint;
|
|
197
|
-
if (side === "top") {
|
|
198
|
-
targetPoint = {
|
|
199
|
-
left: t.left + t.width / 2,
|
|
200
|
-
top: t.top - totalOffset,
|
|
201
|
-
};
|
|
202
|
-
} else if (side === "bottom") {
|
|
203
|
-
targetPoint = {
|
|
204
|
-
left: t.left + t.width / 2,
|
|
205
|
-
top: t.bottom + totalOffset,
|
|
206
|
-
};
|
|
207
|
-
} else if (side === "left") {
|
|
208
|
-
targetPoint = {
|
|
209
|
-
left: t.left - totalOffset,
|
|
210
|
-
top: t.top + t.height / 2,
|
|
211
|
-
};
|
|
212
|
-
} else if (side === "right") {
|
|
213
|
-
targetPoint = {
|
|
214
|
-
left: t.right + totalOffset,
|
|
215
|
-
top: t.top + t.height / 2,
|
|
216
|
-
};
|
|
217
|
-
} else {
|
|
218
|
-
throw new PerseusError(
|
|
219
|
-
`unexpected side ${side}`,
|
|
220
|
-
Errors.InvalidInput,
|
|
221
|
-
);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// Take the target point, and compute its offset from each side of the
|
|
225
|
-
// root element.
|
|
226
|
-
//
|
|
227
|
-
// Note that the client rect's `right` coordinate is the distance
|
|
228
|
-
// between the element's right edge and the _left_ of the screen, not
|
|
229
|
-
// the right of the screen. The `bottom` coordinate is similarly
|
|
230
|
-
// unintuitive. Surprise!
|
|
231
|
-
//
|
|
232
|
-
// Thankfully, unlike client rects, our `pxFromRight` offset is more
|
|
233
|
-
// what you'd expect: it's the distance between the target point and
|
|
234
|
-
// the _right_ edge of the root element.
|
|
235
|
-
return {
|
|
236
|
-
pxFromTop: targetPoint.top - r.top,
|
|
237
|
-
pxFromBottom: r.bottom - targetPoint.top,
|
|
238
|
-
pxFromLeft: targetPoint.left - r.left,
|
|
239
|
-
pxFromRight: r.right - targetPoint.left,
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
_renderBubble(targetPoint: TargetPointOffsets): React.Element<"div"> {
|
|
244
|
-
const {isAboveModal} = this.props;
|
|
245
|
-
const {side, tooltipMargin} = this.props.tooltipProps;
|
|
246
|
-
|
|
247
|
-
// Compute the wrapper styles and FlexiblePosition props to position
|
|
248
|
-
// the bubble at the target point. We also apply "tooltip margin"
|
|
249
|
-
// styles, to prevent the tooltip from getting too close to the sides
|
|
250
|
-
// of the page. Also, in addition to the styles set here, we set one
|
|
251
|
-
// more position coordinate in the following block.
|
|
252
|
-
let bubblePositionerClass;
|
|
253
|
-
const bubblePositionerStyles = {};
|
|
254
|
-
const bubbleFlexiblePositionProps = {};
|
|
255
|
-
const bubbleWrapperStyles = {};
|
|
256
|
-
if (side === "top" || side === "bottom") {
|
|
257
|
-
bubblePositionerClass = styles.tooltipBubblePositionerTopBottom;
|
|
258
|
-
bubbleFlexiblePositionProps.direction = "horizontal";
|
|
259
|
-
bubbleFlexiblePositionProps.pxFromStart = targetPoint.pxFromLeft;
|
|
260
|
-
bubbleFlexiblePositionProps.pxFromEnd = targetPoint.pxFromRight;
|
|
261
|
-
bubbleWrapperStyles.marginLeft = tooltipMargin;
|
|
262
|
-
bubbleWrapperStyles.marginRight = tooltipMargin;
|
|
263
|
-
} else if (side === "left" || side === "right") {
|
|
264
|
-
bubblePositionerClass = styles.tooltipBubblePositionerLeftRight;
|
|
265
|
-
bubbleFlexiblePositionProps.direction = "vertical";
|
|
266
|
-
bubbleFlexiblePositionProps.pxFromStart = targetPoint.pxFromTop;
|
|
267
|
-
bubbleFlexiblePositionProps.pxFromEnd = targetPoint.pxFromBottom;
|
|
268
|
-
bubbleWrapperStyles.marginTop = tooltipMargin;
|
|
269
|
-
bubbleWrapperStyles.marginBottom = tooltipMargin;
|
|
270
|
-
} else {
|
|
271
|
-
throw new PerseusError(
|
|
272
|
-
`unexpected side ${side}`,
|
|
273
|
-
Errors.InvalidInput,
|
|
274
|
-
);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// When side="top", we position ourselves relative to the bottom of the
|
|
278
|
-
// root, so that the tooltip's height will grow upward from the target
|
|
279
|
-
// point.
|
|
280
|
-
//
|
|
281
|
-
// When side="bottom", we position ourselves relative to the top of the
|
|
282
|
-
// root, so that the tooltip's height will grow downward from the
|
|
283
|
-
// target point.
|
|
284
|
-
//
|
|
285
|
-
// When side="left", we position ourselves relative to the right of the
|
|
286
|
-
// root, so that the tooltip's width will grow leftward from the target
|
|
287
|
-
// point.
|
|
288
|
-
//
|
|
289
|
-
// When side="right", we position ourselves relative to the right of
|
|
290
|
-
// the root, so that the tooltip's width will grow rightward from the
|
|
291
|
-
// target point.
|
|
292
|
-
if (side === "top") {
|
|
293
|
-
bubblePositionerStyles.bottom = targetPoint.pxFromBottom;
|
|
294
|
-
} else if (side === "bottom") {
|
|
295
|
-
bubblePositionerStyles.top = targetPoint.pxFromTop;
|
|
296
|
-
} else if (side === "left") {
|
|
297
|
-
bubblePositionerStyles.right = targetPoint.pxFromRight;
|
|
298
|
-
} else if (side === "right") {
|
|
299
|
-
bubblePositionerStyles.left = targetPoint.pxFromLeft;
|
|
300
|
-
} else {
|
|
301
|
-
throw new PerseusError(
|
|
302
|
-
`unexpected side ${side}`,
|
|
303
|
-
Errors.InvalidInput,
|
|
304
|
-
);
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
return (
|
|
308
|
-
<div
|
|
309
|
-
className={css(
|
|
310
|
-
styles.tooltipBubblePositioner,
|
|
311
|
-
bubblePositionerClass,
|
|
312
|
-
isAboveModal && styles.tooltipAboveModal,
|
|
313
|
-
)}
|
|
314
|
-
style={bubblePositionerStyles}
|
|
315
|
-
>
|
|
316
|
-
<FlexiblePosition {...bubbleFlexiblePositionProps}>
|
|
317
|
-
<div
|
|
318
|
-
className={css(styles.tooltipBubbleWrapper)}
|
|
319
|
-
style={bubbleWrapperStyles}
|
|
320
|
-
>
|
|
321
|
-
<TooltipBubble
|
|
322
|
-
tooltipProps={this.props.tooltipProps}
|
|
323
|
-
dismiss={this.props.dismiss}
|
|
324
|
-
/>
|
|
325
|
-
</div>
|
|
326
|
-
</FlexiblePosition>
|
|
327
|
-
</div>
|
|
328
|
-
);
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
_renderArrow(
|
|
332
|
-
targetPoint: TargetPointOffsets,
|
|
333
|
-
rootElementClientRect: ClientRect,
|
|
334
|
-
): React.Element<"div"> {
|
|
335
|
-
const {isAboveModal} = this.props;
|
|
336
|
-
const {side, tooltipMargin} = this.props.tooltipProps;
|
|
337
|
-
const arrowStyles = {};
|
|
338
|
-
|
|
339
|
-
// Compute the vertical position of the arrow (if side is top/bottom),
|
|
340
|
-
// or horizontal position (if side is left/right). Include one extra
|
|
341
|
-
// pixel to get the overlapping just right!
|
|
342
|
-
//
|
|
343
|
-
// When side="top", we position ourselves relative to the bottom of the
|
|
344
|
-
// root element, etc., for consistency with the tooltip bubble. This
|
|
345
|
-
// ensures that, even in the face of extreme circumstances (e.g. the
|
|
346
|
-
// root element unexpectedly changing size), the arrow and bubble will
|
|
347
|
-
// never separate from each other.
|
|
348
|
-
if (side === "top") {
|
|
349
|
-
arrowStyles.bottom = targetPoint.pxFromBottom - arrowHeight + 1;
|
|
350
|
-
} else if (side === "bottom") {
|
|
351
|
-
arrowStyles.top = targetPoint.pxFromTop - arrowHeight + 1;
|
|
352
|
-
} else if (side === "left") {
|
|
353
|
-
arrowStyles.right = targetPoint.pxFromRight - arrowHeight + 1;
|
|
354
|
-
} else if (side === "right") {
|
|
355
|
-
arrowStyles.left = targetPoint.pxFromLeft - arrowHeight + 1;
|
|
356
|
-
} else {
|
|
357
|
-
throw new PerseusError(
|
|
358
|
-
`unexpected side ${side}`,
|
|
359
|
-
Errors.InvalidInput,
|
|
360
|
-
);
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
// Compute the horizontal position of the arrow (if side is top/bottom),
|
|
364
|
-
// or vertical position (if side is left/right).
|
|
365
|
-
//
|
|
366
|
-
// While we don't care about the arrow lining up with any *exact* point
|
|
367
|
-
// on the tooltip bubble, we *do* want to ensure that we don't align it
|
|
368
|
-
// with one of the curved corners.
|
|
369
|
-
//
|
|
370
|
-
// So, we determine the minimum and maximum position value that would
|
|
371
|
-
// be safe for the arrow to have while still leaving room for the
|
|
372
|
-
// border radius, and clamp the value to that range.
|
|
373
|
-
//
|
|
374
|
-
// If the page is very narrow, then it's possible that the arrow is too
|
|
375
|
-
// wide, and will have to exceed one bound or the other. In that case,
|
|
376
|
-
// well, that's what you get for using a 18px-wide viewport :P
|
|
377
|
-
const extraSpace = tooltipMargin + bubbleBorderRadius;
|
|
378
|
-
|
|
379
|
-
if (side === "top" || side === "bottom") {
|
|
380
|
-
const minLeft = extraSpace;
|
|
381
|
-
const maxLeft =
|
|
382
|
-
rootElementClientRect.width - arrowWidth - extraSpace;
|
|
383
|
-
arrowStyles.left = targetPoint.pxFromLeft - arrowWidth / 2;
|
|
384
|
-
arrowStyles.left = Math.min(arrowStyles.left, maxLeft);
|
|
385
|
-
arrowStyles.left = Math.max(arrowStyles.left, minLeft);
|
|
386
|
-
} else if (side === "left" || side === "right") {
|
|
387
|
-
const minTop = extraSpace;
|
|
388
|
-
const maxTop =
|
|
389
|
-
rootElementClientRect.height - arrowWidth - extraSpace;
|
|
390
|
-
arrowStyles.top = targetPoint.pxFromTop - arrowWidth / 2;
|
|
391
|
-
arrowStyles.top = Math.min(arrowStyles.top, maxTop);
|
|
392
|
-
arrowStyles.top = Math.max(arrowStyles.top, minTop);
|
|
393
|
-
} else {
|
|
394
|
-
throw new PerseusError(
|
|
395
|
-
`unexpected side ${side}`,
|
|
396
|
-
Errors.InvalidInput,
|
|
397
|
-
);
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
return (
|
|
401
|
-
<div
|
|
402
|
-
className={css(
|
|
403
|
-
styles.tooltipArrowPositioner,
|
|
404
|
-
isAboveModal && styles.tooltipAboveModal,
|
|
405
|
-
)}
|
|
406
|
-
style={arrowStyles}
|
|
407
|
-
>
|
|
408
|
-
<TooltipArrow tooltipProps={this.props.tooltipProps} />
|
|
409
|
-
</div>
|
|
410
|
-
);
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
/**
|
|
414
|
-
* Build an Aphrodite style that filters the tooltip's visibility by the
|
|
415
|
-
* `media` prop provided to NewTooltip.
|
|
416
|
-
*
|
|
417
|
-
* When the media query is satisfied, this will have `display: block`. When
|
|
418
|
-
* the media query is not satisfied, this will have `display: none`.
|
|
419
|
-
*
|
|
420
|
-
* If no `media` prop was provided, this returns null.
|
|
421
|
-
*/
|
|
422
|
-
_buildMediaFilterStyle(): $FlowFixMe {
|
|
423
|
-
const {media} = this.props.tooltipProps;
|
|
424
|
-
if (!media) {
|
|
425
|
-
return null;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
// NOTE(mdr): It's not, like, _good practice_ to create Aphrodite
|
|
429
|
-
// stylesheets inline like this, but there's no significant
|
|
430
|
-
// negative consequences! Thanks to Aphrodite's hashing strategy,
|
|
431
|
-
// this will always yield the same class name, even though we
|
|
432
|
-
// rebuild the stylesheet from scratch on each render.
|
|
433
|
-
const mediaFilterStyleSheet = StyleSheet.create({
|
|
434
|
-
mediaFilter: {
|
|
435
|
-
display: "none",
|
|
436
|
-
[media]: {
|
|
437
|
-
display: "block",
|
|
438
|
-
},
|
|
439
|
-
},
|
|
440
|
-
});
|
|
441
|
-
|
|
442
|
-
return mediaFilterStyleSheet.mediaFilter;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
remeasure: () => void = () => {
|
|
446
|
-
const {targetElement, rootElement} = this.props;
|
|
447
|
-
this.setState({
|
|
448
|
-
targetElementClientRect: targetElement.getBoundingClientRect(),
|
|
449
|
-
rootElementClientRect: rootElement.getBoundingClientRect(),
|
|
450
|
-
});
|
|
451
|
-
};
|
|
452
|
-
|
|
453
|
-
render(): null | React.Element<"div"> {
|
|
454
|
-
const {targetElementClientRect, rootElementClientRect} = this.state;
|
|
455
|
-
if (!targetElementClientRect || !rootElementClientRect) {
|
|
456
|
-
return null;
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
const targetPoint = this._computeTargetPointOffsets(
|
|
460
|
-
targetElementClientRect,
|
|
461
|
-
rootElementClientRect,
|
|
462
|
-
);
|
|
463
|
-
|
|
464
|
-
return (
|
|
465
|
-
<div className={css(this._buildMediaFilterStyle())}>
|
|
466
|
-
{this._renderBubble(targetPoint)}
|
|
467
|
-
{this._renderArrow(targetPoint, rootElementClientRect)}
|
|
468
|
-
</div>
|
|
469
|
-
);
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
const styles = StyleSheet.create({
|
|
474
|
-
tooltipBubblePositioner: {
|
|
475
|
-
// Absolute position.
|
|
476
|
-
// `top`/`bottom`/`left`/`right` provided dynamically, and also by the
|
|
477
|
-
// additional `tooltipBubblePositioner` classes.
|
|
478
|
-
position: "absolute",
|
|
479
|
-
zIndex: zindexTooltip,
|
|
480
|
-
|
|
481
|
-
// Disable pointer events, so that any extra empty space consumed by
|
|
482
|
-
// the FlexiblePosition element is not clickable. In
|
|
483
|
-
// `tooltipBubbleWrapper`, we reset this style, so that the user can
|
|
484
|
-
// interact with the tooltip content.
|
|
485
|
-
pointerEvents: "none",
|
|
486
|
-
},
|
|
487
|
-
|
|
488
|
-
tooltipAboveModal: {
|
|
489
|
-
zIndex: zindexAboveModal,
|
|
490
|
-
},
|
|
491
|
-
|
|
492
|
-
// More positioner styles for when side="top" or side="bottom".
|
|
493
|
-
tooltipBubblePositionerTopBottom: {
|
|
494
|
-
// The positioner consumes the full page width, which gives the
|
|
495
|
-
// FlexiblePosition element the freedom to position the tooltip
|
|
496
|
-
// anywhere horizontally on the page. (Though, of course, it'll aim for
|
|
497
|
-
// the target point!)
|
|
498
|
-
left: 0,
|
|
499
|
-
width: "100%",
|
|
500
|
-
},
|
|
501
|
-
|
|
502
|
-
// More positioner styles for when side="left" or side="right".
|
|
503
|
-
tooltipBubblePositionerLeftRight: {
|
|
504
|
-
// The positioner consumes the full page height, which gives the
|
|
505
|
-
// FlexiblePosition element the freedom to position the tooltip
|
|
506
|
-
// anywhere horizontally on the page. (Though, of course, it'll aim for
|
|
507
|
-
// the target point!)
|
|
508
|
-
top: 0,
|
|
509
|
-
height: "100%",
|
|
510
|
-
},
|
|
511
|
-
|
|
512
|
-
tooltipBubbleWrapper: {
|
|
513
|
-
// `marginLeft` and `marginRight` styles are provided dynamically, to
|
|
514
|
-
// implement the `tooltipMargin` provided by the caller.
|
|
515
|
-
|
|
516
|
-
// Re-enable pointer events, which were disabled by
|
|
517
|
-
// `tooltipBubblePositioner`, so that the user can interact with the
|
|
518
|
-
// tooltip content.
|
|
519
|
-
pointerEvents: "auto",
|
|
520
|
-
},
|
|
521
|
-
|
|
522
|
-
tooltipArrowPositioner: {
|
|
523
|
-
// `left` and `top`/`bottom` styles are provided dynamically.
|
|
524
|
-
position: "absolute",
|
|
525
|
-
zIndex: zindexTooltip,
|
|
526
|
-
|
|
527
|
-
pointerEvents: "none",
|
|
528
|
-
},
|
|
529
|
-
});
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
// @flow
|
|
2
|
-
/**
|
|
3
|
-
* Utility functions for the NewTooltip component.
|
|
4
|
-
*/
|
|
5
|
-
import Color from "@khanacademy/wonder-blocks-color";
|
|
6
|
-
|
|
7
|
-
import type {NewTooltipProps} from "./types.js";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Given a set of NewTooltipProps, return the `backgroundColor` and `textColor`
|
|
11
|
-
* of the tooltip. Used in TooltipArrow and TooltipBubble.
|
|
12
|
-
*/
|
|
13
|
-
export function getTooltipColors(tooltipProps: {
|
|
14
|
-
color: NewTooltipProps["color"],
|
|
15
|
-
inverted: NewTooltipProps["inverted"],
|
|
16
|
-
...
|
|
17
|
-
}): {|
|
|
18
|
-
backgroundColor: string,
|
|
19
|
-
textColor: string,
|
|
20
|
-
|} {
|
|
21
|
-
const {color, inverted} = tooltipProps;
|
|
22
|
-
|
|
23
|
-
if (inverted) {
|
|
24
|
-
return {backgroundColor: color, textColor: Color.white};
|
|
25
|
-
}
|
|
26
|
-
return {backgroundColor: Color.white, textColor: color};
|
|
27
|
-
}
|