@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,101 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
/**
|
|
3
|
+
* This component is a light wrapper for react-popper, allowing us to position
|
|
4
|
+
* and control the tooltip bubble location and visibility as we need.
|
|
5
|
+
*/
|
|
6
|
+
import * as React from "react";
|
|
7
|
+
import {Popper} from "react-popper";
|
|
8
|
+
import type {PopperChildrenProps} from "react-popper";
|
|
9
|
+
|
|
10
|
+
import RefTracker from "../util/ref-tracker.js";
|
|
11
|
+
import type {Placement} from "../util/types.js";
|
|
12
|
+
import type {PopperElementProps} from "./tooltip-bubble.js";
|
|
13
|
+
|
|
14
|
+
type Props = {|
|
|
15
|
+
/**
|
|
16
|
+
* This uses the children-as-a-function approach, mirroring react-popper's
|
|
17
|
+
* implementation.
|
|
18
|
+
*
|
|
19
|
+
* TODO(WB-624): figure out to only allow TooltipBubble and PopoverDialog
|
|
20
|
+
*/
|
|
21
|
+
children: (PopperElementProps) => React.Element<any>,
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The element that anchors the tooltip bubble.
|
|
25
|
+
* This is used to position the bubble.
|
|
26
|
+
*/
|
|
27
|
+
anchorElement: ?HTMLElement,
|
|
28
|
+
|
|
29
|
+
/** Where should the bubble try to go with respect to its anchor. */
|
|
30
|
+
placement: Placement,
|
|
31
|
+
|};
|
|
32
|
+
|
|
33
|
+
export default class TooltipPopper extends React.Component<Props> {
|
|
34
|
+
_bubbleRefTracker: RefTracker = new RefTracker();
|
|
35
|
+
_tailRefTracker: RefTracker = new RefTracker();
|
|
36
|
+
|
|
37
|
+
_renderPositionedContent(popperProps: PopperChildrenProps): React.Node {
|
|
38
|
+
const {children} = this.props;
|
|
39
|
+
|
|
40
|
+
// We'll hide some complexity from the children here and ensure
|
|
41
|
+
// that our placement always has a value.
|
|
42
|
+
const placement: Placement =
|
|
43
|
+
// We know that popperProps.placement will only be one of our
|
|
44
|
+
// supported values, so just cast it.
|
|
45
|
+
(popperProps.placement: any) || this.props.placement;
|
|
46
|
+
|
|
47
|
+
// Just in case the callbacks have changed, let's update our reference
|
|
48
|
+
// trackers.
|
|
49
|
+
this._bubbleRefTracker.setCallback(popperProps.ref);
|
|
50
|
+
this._tailRefTracker.setCallback(popperProps.arrowProps.ref);
|
|
51
|
+
|
|
52
|
+
// Here we translate from the react-popper's PropperChildrenProps
|
|
53
|
+
// to our own TooltipBubbleProps.
|
|
54
|
+
const bubbleProps = {
|
|
55
|
+
placement,
|
|
56
|
+
style: {
|
|
57
|
+
// NOTE(jeresig): We can't just use `popperProps.style` here
|
|
58
|
+
// as the Flow type doesn't match Aphrodite's CSS flow props
|
|
59
|
+
// (as it doesn't camelCase props). So we just copy over the
|
|
60
|
+
// props that we need, instead.
|
|
61
|
+
top: popperProps.style.top,
|
|
62
|
+
left: popperProps.style.left,
|
|
63
|
+
bottom: popperProps.style.bottom,
|
|
64
|
+
right: popperProps.style.right,
|
|
65
|
+
position: popperProps.style.position,
|
|
66
|
+
transform: popperProps.style.transform,
|
|
67
|
+
},
|
|
68
|
+
updateBubbleRef: this._bubbleRefTracker.updateRef,
|
|
69
|
+
tailOffset: {
|
|
70
|
+
bottom: popperProps.arrowProps.style.bottom,
|
|
71
|
+
right: popperProps.arrowProps.style.right,
|
|
72
|
+
top: popperProps.arrowProps.style.top,
|
|
73
|
+
left: popperProps.arrowProps.style.left,
|
|
74
|
+
transform: popperProps.arrowProps.style.transform,
|
|
75
|
+
},
|
|
76
|
+
updateTailRef: this._tailRefTracker.updateRef,
|
|
77
|
+
isReferenceHidden: popperProps.isReferenceHidden,
|
|
78
|
+
};
|
|
79
|
+
return children(bubbleProps);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
render(): React.Node {
|
|
83
|
+
const {anchorElement, placement} = this.props;
|
|
84
|
+
return (
|
|
85
|
+
<Popper
|
|
86
|
+
referenceElement={anchorElement}
|
|
87
|
+
placement={placement}
|
|
88
|
+
modifiers={[
|
|
89
|
+
{
|
|
90
|
+
name: "preventOverflow",
|
|
91
|
+
options: {
|
|
92
|
+
rootBoundary: "document",
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
]}
|
|
96
|
+
>
|
|
97
|
+
{(props) => this._renderPositionedContent(props)}
|
|
98
|
+
</Popper>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import {css, StyleSheet} from "aphrodite";
|
|
4
|
+
|
|
5
|
+
import Colors from "@khanacademy/wonder-blocks-color";
|
|
6
|
+
import {View} from "@khanacademy/wonder-blocks-core";
|
|
7
|
+
import Spacing from "@khanacademy/wonder-blocks-spacing";
|
|
8
|
+
|
|
9
|
+
import type {StyleType} from "@khanacademy/wonder-blocks-core";
|
|
10
|
+
import type {getRefFn, Placement, Offset} from "../util/types.js";
|
|
11
|
+
|
|
12
|
+
export type Props = {|
|
|
13
|
+
/**
|
|
14
|
+
* Whether we should use the default white background color or switch to a
|
|
15
|
+
* different bg color.
|
|
16
|
+
*
|
|
17
|
+
* NOTE: Added to support custom popovers
|
|
18
|
+
* @ignore
|
|
19
|
+
*/
|
|
20
|
+
color: $Keys<typeof Colors>,
|
|
21
|
+
|
|
22
|
+
/** The offset of the tail indicating where it should be positioned. */
|
|
23
|
+
offset?: Offset,
|
|
24
|
+
|
|
25
|
+
/** The placement of the tail with respect to the tooltip anchor. */
|
|
26
|
+
placement: Placement,
|
|
27
|
+
|
|
28
|
+
/** A callback to update the ref of the tail element. */
|
|
29
|
+
updateRef?: getRefFn,
|
|
30
|
+
|};
|
|
31
|
+
|
|
32
|
+
type DefaultProps = {|
|
|
33
|
+
color: $PropertyType<Props, "color">,
|
|
34
|
+
|};
|
|
35
|
+
|
|
36
|
+
type Dimensions = {|
|
|
37
|
+
trimlinePoints: [string, string],
|
|
38
|
+
points: [string, string, string],
|
|
39
|
+
height: number,
|
|
40
|
+
width: number,
|
|
41
|
+
|};
|
|
42
|
+
|
|
43
|
+
type FilterPosition = {|
|
|
44
|
+
y: string,
|
|
45
|
+
x: string,
|
|
46
|
+
offsetShadowX: number,
|
|
47
|
+
|};
|
|
48
|
+
|
|
49
|
+
// TODO(somewhatabstract): Replace this really basic unique ID work with
|
|
50
|
+
// something SSR-friendly and more robust.
|
|
51
|
+
let tempIdCounter = 0;
|
|
52
|
+
|
|
53
|
+
export default class TooltipTail extends React.Component<Props> {
|
|
54
|
+
static defaultProps: DefaultProps = {
|
|
55
|
+
color: "white",
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
_calculateDimensionsFromPlacement(): Dimensions {
|
|
59
|
+
const {placement} = this.props;
|
|
60
|
+
|
|
61
|
+
// The trimline, which we draw to make the tail flush to the bubble,
|
|
62
|
+
// has a thickness of 1. Since the line is drawn centered to the
|
|
63
|
+
// coordinates, we use an offset of 0.5 so that it properly covers what
|
|
64
|
+
// we want it to.
|
|
65
|
+
const trimlineOffset = 0.5;
|
|
66
|
+
|
|
67
|
+
// Calculate the three points of the arrow. Depending on the tail's
|
|
68
|
+
// direction (i.e., the tooltip's "side"), we choose different points,
|
|
69
|
+
// and set our SVG's bounds differently.
|
|
70
|
+
//
|
|
71
|
+
// Note that when the tail points to the left or right, the width/height
|
|
72
|
+
// are inverted.
|
|
73
|
+
switch (placement) {
|
|
74
|
+
case "top":
|
|
75
|
+
return {
|
|
76
|
+
trimlinePoints: [
|
|
77
|
+
`0,-${trimlineOffset}`,
|
|
78
|
+
`${ARROW_WIDTH},-${trimlineOffset}`,
|
|
79
|
+
],
|
|
80
|
+
points: [
|
|
81
|
+
"0,0",
|
|
82
|
+
`${ARROW_WIDTH / 2},${ARROW_HEIGHT}`,
|
|
83
|
+
`${ARROW_WIDTH},0`,
|
|
84
|
+
],
|
|
85
|
+
height: ARROW_HEIGHT,
|
|
86
|
+
width: ARROW_WIDTH,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
case "right":
|
|
90
|
+
return {
|
|
91
|
+
trimlinePoints: [
|
|
92
|
+
`${ARROW_HEIGHT + trimlineOffset},0`,
|
|
93
|
+
`${ARROW_HEIGHT + trimlineOffset},${ARROW_WIDTH}`,
|
|
94
|
+
],
|
|
95
|
+
points: [
|
|
96
|
+
`${ARROW_HEIGHT},0`,
|
|
97
|
+
`0,${ARROW_WIDTH / 2}`,
|
|
98
|
+
`${ARROW_HEIGHT},${ARROW_WIDTH}`,
|
|
99
|
+
],
|
|
100
|
+
width: ARROW_HEIGHT,
|
|
101
|
+
height: ARROW_WIDTH,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
case "bottom":
|
|
105
|
+
return {
|
|
106
|
+
trimlinePoints: [
|
|
107
|
+
`0, ${ARROW_HEIGHT + trimlineOffset}`,
|
|
108
|
+
`${ARROW_WIDTH},${ARROW_HEIGHT + trimlineOffset}`,
|
|
109
|
+
],
|
|
110
|
+
points: [
|
|
111
|
+
`0, ${ARROW_HEIGHT}`,
|
|
112
|
+
`${ARROW_WIDTH / 2},0`,
|
|
113
|
+
`${ARROW_WIDTH},${ARROW_HEIGHT}`,
|
|
114
|
+
],
|
|
115
|
+
width: ARROW_WIDTH,
|
|
116
|
+
height: ARROW_HEIGHT,
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
case "left":
|
|
120
|
+
return {
|
|
121
|
+
trimlinePoints: [
|
|
122
|
+
`-${trimlineOffset},0`,
|
|
123
|
+
`-${trimlineOffset},${ARROW_WIDTH}`,
|
|
124
|
+
],
|
|
125
|
+
points: [
|
|
126
|
+
`0,0`,
|
|
127
|
+
`${ARROW_HEIGHT},${ARROW_WIDTH / 2}`,
|
|
128
|
+
`0,${ARROW_WIDTH}`,
|
|
129
|
+
],
|
|
130
|
+
width: ARROW_HEIGHT,
|
|
131
|
+
height: ARROW_WIDTH,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
default:
|
|
135
|
+
throw new Error(`Unknown placement: ${placement}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
_getFilterPositioning(): ?FilterPosition {
|
|
140
|
+
const {placement} = this.props;
|
|
141
|
+
switch (placement) {
|
|
142
|
+
case "top":
|
|
143
|
+
return {
|
|
144
|
+
y: "-50%",
|
|
145
|
+
x: "-50%",
|
|
146
|
+
offsetShadowX: 0,
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
case "bottom":
|
|
150
|
+
// No shadow on the arrow as it falls "under" the bubble.
|
|
151
|
+
return null;
|
|
152
|
+
|
|
153
|
+
case "left":
|
|
154
|
+
return {
|
|
155
|
+
y: "-50%",
|
|
156
|
+
x: "0%",
|
|
157
|
+
offsetShadowX: 1,
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
case "right":
|
|
161
|
+
return {
|
|
162
|
+
y: "-50%",
|
|
163
|
+
x: "-100%",
|
|
164
|
+
offsetShadowX: -1,
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
default:
|
|
168
|
+
throw new Error(`Unknown placement: ${placement}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Create an SVG filter that applies a blur to an element.
|
|
174
|
+
* We'll apply it to a dark shape outlining the tooltip, which
|
|
175
|
+
* will produce the overall effect of a drop-shadow.
|
|
176
|
+
*
|
|
177
|
+
* Also, scope its ID by side, so that tooltips with other
|
|
178
|
+
* "side" values don't end up using the wrong filter from
|
|
179
|
+
* elsewhere in the document. (The `height` value depends on
|
|
180
|
+
* which way the arrow is turned!)
|
|
181
|
+
*/
|
|
182
|
+
_maybeRenderDropshadow(points: [string, string, string]): React.Node {
|
|
183
|
+
const position = this._getFilterPositioning();
|
|
184
|
+
if (!position) {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
const {placement} = this.props;
|
|
188
|
+
const {y, x, offsetShadowX} = position;
|
|
189
|
+
const dropShadowFilterId = `tooltip-dropshadow-${placement}-${tempIdCounter++}`;
|
|
190
|
+
return [
|
|
191
|
+
<filter
|
|
192
|
+
key="filter"
|
|
193
|
+
id={dropShadowFilterId}
|
|
194
|
+
// Height and width tell the filter how big of a canvas to
|
|
195
|
+
// draw based on its parent size. i.e. 2 times bigger.
|
|
196
|
+
// This is so that the diffuse gaussian blur has space to
|
|
197
|
+
// bleed into.
|
|
198
|
+
width="200%"
|
|
199
|
+
height="200%"
|
|
200
|
+
// The x and y values tell the filter where, relative to its
|
|
201
|
+
// parent, it should begin showing its canvas. Without these
|
|
202
|
+
// the filter would clip at 0,0, which would look really
|
|
203
|
+
// strange.
|
|
204
|
+
x={x}
|
|
205
|
+
y={y}
|
|
206
|
+
>
|
|
207
|
+
{/* Here we provide a nice blur that will be our shadow
|
|
208
|
+
* The stdDeviation is the spread of the blur. We don't want it
|
|
209
|
+
* too diffuse.
|
|
210
|
+
*/}
|
|
211
|
+
<feGaussianBlur
|
|
212
|
+
in="SourceAlpha"
|
|
213
|
+
stdDeviation={Spacing.xxSmall_6 / 2}
|
|
214
|
+
/>
|
|
215
|
+
|
|
216
|
+
{/* Here we adjust the alpha (feFuncA) linearly so as to blend
|
|
217
|
+
* the shadow to match the rest of the tooltip bubble shadow.
|
|
218
|
+
* It is a combination of the diffuse blur and this alpha
|
|
219
|
+
* value that will make it look right.
|
|
220
|
+
*
|
|
221
|
+
* The value of 0.3. was arrived at from trial and error.
|
|
222
|
+
*/}
|
|
223
|
+
<feComponentTransfer>
|
|
224
|
+
<feFuncA type="linear" slope="0.3" />
|
|
225
|
+
</feComponentTransfer>
|
|
226
|
+
</filter>,
|
|
227
|
+
/**
|
|
228
|
+
* Draw the tooltip arrow and apply the blur filter we created
|
|
229
|
+
* above, to produce a drop shadow effect.
|
|
230
|
+
* We move it down a bit with a translation, so that it is what
|
|
231
|
+
* we want.
|
|
232
|
+
*
|
|
233
|
+
* We offset the shadow on the X-axis because for left/right
|
|
234
|
+
* tails, we move the tail 1px toward the bubble. If we didn't
|
|
235
|
+
* offset the shadow, it would crash the bubble outline.
|
|
236
|
+
*
|
|
237
|
+
* See styles below for why we offset the arrow.
|
|
238
|
+
*/
|
|
239
|
+
<g key="dropshadow" transform={`translate(${offsetShadowX},5.5)`}>
|
|
240
|
+
<polyline
|
|
241
|
+
fill={Colors.offBlack16}
|
|
242
|
+
points={points.join(" ")}
|
|
243
|
+
stroke={Colors.offBlack32}
|
|
244
|
+
filter={`url(#${dropShadowFilterId})`}
|
|
245
|
+
/>
|
|
246
|
+
</g>,
|
|
247
|
+
];
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
_minDistanceFromCorners(placement: Placement): number {
|
|
251
|
+
const minDistanceFromCornersForTopBottom = Spacing.medium_16;
|
|
252
|
+
const minDistanceFromCornersForLeftRight = 7;
|
|
253
|
+
|
|
254
|
+
switch (placement) {
|
|
255
|
+
case "top":
|
|
256
|
+
case "bottom":
|
|
257
|
+
return minDistanceFromCornersForTopBottom;
|
|
258
|
+
|
|
259
|
+
case "left":
|
|
260
|
+
case "right":
|
|
261
|
+
return minDistanceFromCornersForLeftRight;
|
|
262
|
+
|
|
263
|
+
default:
|
|
264
|
+
throw new Error(`Unknown placement: ${placement}`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
_getFullTailWidth(): number {
|
|
269
|
+
return ARROW_WIDTH + 2 * MIN_DISTANCE_FROM_CORNERS;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
_getFullTailHeight(): number {
|
|
273
|
+
return ARROW_HEIGHT + DISTANCE_FROM_ANCHOR;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
_getContainerStyle(): StyleType {
|
|
277
|
+
const {placement} = this.props;
|
|
278
|
+
/**
|
|
279
|
+
* Ensure the container is sized properly for us to be placed correctly
|
|
280
|
+
* by the Popper.js code.
|
|
281
|
+
*
|
|
282
|
+
* Here we offset the arrow 1px toward the bubble. This ensures the arrow
|
|
283
|
+
* outline meets the bubble outline and allows the arrow to erase the bubble
|
|
284
|
+
* outline between the ends of the arrow outline. We do this so that the
|
|
285
|
+
* arrow outline and bubble outline create a single, seamless outline of
|
|
286
|
+
* the callout.
|
|
287
|
+
*
|
|
288
|
+
* NOTE: The widths and heights refer to the downward-pointing tail
|
|
289
|
+
* (i.e. placement="top"). When the tail points to the left or right
|
|
290
|
+
* instead, the width/height are inverted.
|
|
291
|
+
*/
|
|
292
|
+
const fullTailWidth = this._getFullTailWidth();
|
|
293
|
+
const fullTailHeight = this._getFullTailHeight();
|
|
294
|
+
|
|
295
|
+
switch (placement) {
|
|
296
|
+
case "top":
|
|
297
|
+
return {
|
|
298
|
+
top: -1,
|
|
299
|
+
width: fullTailWidth,
|
|
300
|
+
height: fullTailHeight,
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
case "right":
|
|
304
|
+
return {
|
|
305
|
+
left: 1,
|
|
306
|
+
width: fullTailHeight,
|
|
307
|
+
height: fullTailWidth,
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
case "bottom":
|
|
311
|
+
return {
|
|
312
|
+
top: 1,
|
|
313
|
+
width: fullTailWidth,
|
|
314
|
+
height: fullTailHeight,
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
case "left":
|
|
318
|
+
return {
|
|
319
|
+
left: -1,
|
|
320
|
+
width: fullTailHeight,
|
|
321
|
+
height: fullTailWidth,
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
default:
|
|
325
|
+
throw new Error(`Unknown placement: ${placement}`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
_getArrowStyle(): StyleType {
|
|
330
|
+
const {placement} = this.props;
|
|
331
|
+
switch (placement) {
|
|
332
|
+
case "top":
|
|
333
|
+
return {
|
|
334
|
+
marginLeft: MIN_DISTANCE_FROM_CORNERS,
|
|
335
|
+
marginRight: MIN_DISTANCE_FROM_CORNERS,
|
|
336
|
+
paddingBottom: DISTANCE_FROM_ANCHOR,
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
case "right":
|
|
340
|
+
return {
|
|
341
|
+
marginTop: MIN_DISTANCE_FROM_CORNERS,
|
|
342
|
+
marginBottom: MIN_DISTANCE_FROM_CORNERS,
|
|
343
|
+
paddingLeft: DISTANCE_FROM_ANCHOR,
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
case "bottom":
|
|
347
|
+
return {
|
|
348
|
+
marginLeft: MIN_DISTANCE_FROM_CORNERS,
|
|
349
|
+
marginRight: MIN_DISTANCE_FROM_CORNERS,
|
|
350
|
+
paddingTop: DISTANCE_FROM_ANCHOR,
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
case "left":
|
|
354
|
+
return {
|
|
355
|
+
marginTop: MIN_DISTANCE_FROM_CORNERS,
|
|
356
|
+
marginBottom: MIN_DISTANCE_FROM_CORNERS,
|
|
357
|
+
paddingRight: DISTANCE_FROM_ANCHOR,
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
default:
|
|
361
|
+
throw new Error(`Unknown placement: ${placement}`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
_renderArrow(): React.Node {
|
|
366
|
+
const {
|
|
367
|
+
trimlinePoints,
|
|
368
|
+
points,
|
|
369
|
+
height,
|
|
370
|
+
width,
|
|
371
|
+
} = this._calculateDimensionsFromPlacement();
|
|
372
|
+
|
|
373
|
+
const {color} = this.props;
|
|
374
|
+
|
|
375
|
+
return (
|
|
376
|
+
<svg
|
|
377
|
+
className={css(styles.arrow)}
|
|
378
|
+
style={this._getArrowStyle()}
|
|
379
|
+
width={width}
|
|
380
|
+
height={height}
|
|
381
|
+
>
|
|
382
|
+
{this._maybeRenderDropshadow(points)}
|
|
383
|
+
|
|
384
|
+
{/**
|
|
385
|
+
* Draw the actual background of the tooltip arrow.
|
|
386
|
+
*
|
|
387
|
+
* We draw the outline in white too so that when we draw the
|
|
388
|
+
* outline, it draws over white and not the dropshadow behind.
|
|
389
|
+
*/}
|
|
390
|
+
<polyline
|
|
391
|
+
fill={Colors[color]}
|
|
392
|
+
stroke={Colors[color]}
|
|
393
|
+
points={points.join(" ")}
|
|
394
|
+
/>
|
|
395
|
+
|
|
396
|
+
{/* Draw the tooltip outline around the tooltip arrow. */}
|
|
397
|
+
<polyline
|
|
398
|
+
// Redraw the stroke on top of the background color,
|
|
399
|
+
// so that the ends aren't extra dark where they meet
|
|
400
|
+
// the border of the tooltip.
|
|
401
|
+
fill={Colors[color]}
|
|
402
|
+
points={points.join(" ")}
|
|
403
|
+
stroke={Colors.offBlack16}
|
|
404
|
+
/>
|
|
405
|
+
|
|
406
|
+
{/* Draw a trimline to make the arrow appear flush */}
|
|
407
|
+
<polyline
|
|
408
|
+
stroke={Colors[color]}
|
|
409
|
+
points={trimlinePoints.join(" ")}
|
|
410
|
+
/>
|
|
411
|
+
</svg>
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
render(): React.Node {
|
|
416
|
+
const {offset, placement, updateRef} = this.props;
|
|
417
|
+
return (
|
|
418
|
+
<View
|
|
419
|
+
style={[
|
|
420
|
+
styles.tailContainer,
|
|
421
|
+
{...offset},
|
|
422
|
+
this._getContainerStyle(),
|
|
423
|
+
]}
|
|
424
|
+
data-placement={placement}
|
|
425
|
+
ref={updateRef}
|
|
426
|
+
>
|
|
427
|
+
{this._renderArrow()}
|
|
428
|
+
</View>
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Some constants to make style generation easier to understand.
|
|
435
|
+
* NOTE: The widths and heights refer to the downward-pointing tail
|
|
436
|
+
* (i.e. placement="top"). When the tail points to the left or right instead,
|
|
437
|
+
* the width/height are inverted.
|
|
438
|
+
*/
|
|
439
|
+
const DISTANCE_FROM_ANCHOR = Spacing.xSmall_8;
|
|
440
|
+
|
|
441
|
+
const MIN_DISTANCE_FROM_CORNERS = Spacing.xSmall_8;
|
|
442
|
+
|
|
443
|
+
const ARROW_WIDTH = Spacing.large_24;
|
|
444
|
+
const ARROW_HEIGHT = Spacing.small_12;
|
|
445
|
+
|
|
446
|
+
const styles = StyleSheet.create({
|
|
447
|
+
/**
|
|
448
|
+
* Container
|
|
449
|
+
*/
|
|
450
|
+
tailContainer: {
|
|
451
|
+
position: "relative",
|
|
452
|
+
pointerEvents: "none",
|
|
453
|
+
},
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Arrow
|
|
457
|
+
*/
|
|
458
|
+
arrow: {
|
|
459
|
+
// Ensure the dropshadow bleeds outside our bounds.
|
|
460
|
+
overflow: "visible",
|
|
461
|
+
},
|
|
462
|
+
});
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
The `TooltipTail` renders the tail, including appropriate padding from the anchor location and corners of the tooltip bubble (so as space out the bubble corners).
|
|
2
|
+
|
|
3
|
+
Each example is shown next to a bar that indicates the padding either side of the tail and the tail width itself. The bar also indicates how far away from the anchor element the tail will render.
|
|
4
|
+
|
|
5
|
+
### Placement top
|
|
6
|
+
|
|
7
|
+
```jsx
|
|
8
|
+
import {StyleSheet} from "aphrodite";
|
|
9
|
+
import {View} from "@khanacademy/wonder-blocks-core";
|
|
10
|
+
import {Spring} from "@khanacademy/wonder-blocks-layout";
|
|
11
|
+
import Spacing from "@khanacademy/wonder-blocks-spacing";
|
|
12
|
+
|
|
13
|
+
const styles = StyleSheet.create({
|
|
14
|
+
guideContainer: {
|
|
15
|
+
flexDirection: "row",
|
|
16
|
+
height: Spacing.xxxSmall_4,
|
|
17
|
+
},
|
|
18
|
+
padding: {
|
|
19
|
+
backgroundColor: "bisque",
|
|
20
|
+
width: Spacing.xSmall_8,
|
|
21
|
+
},
|
|
22
|
+
tail: {
|
|
23
|
+
backgroundColor: "green",
|
|
24
|
+
width: Spacing.large_24,
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
<View>
|
|
29
|
+
<TooltipTail placement="top" />
|
|
30
|
+
<View style={styles.guideContainer}>
|
|
31
|
+
<View key="padleft" style={styles.padding} />
|
|
32
|
+
<View key="tail" style={styles.tail} />
|
|
33
|
+
<View key="padright" style={styles.padding} />
|
|
34
|
+
<Spring key="spring" />
|
|
35
|
+
</View>
|
|
36
|
+
</View>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Placement right
|
|
40
|
+
|
|
41
|
+
```jsx
|
|
42
|
+
import {StyleSheet} from "aphrodite";
|
|
43
|
+
import {View} from "@khanacademy/wonder-blocks-core";
|
|
44
|
+
import {Spring} from "@khanacademy/wonder-blocks-layout";
|
|
45
|
+
import Spacing from "@khanacademy/wonder-blocks-spacing";
|
|
46
|
+
|
|
47
|
+
const styles = StyleSheet.create({
|
|
48
|
+
exampleContainer: {
|
|
49
|
+
flexDirection: "row",
|
|
50
|
+
},
|
|
51
|
+
guideContainer: {
|
|
52
|
+
width: Spacing.xxxSmall_4,
|
|
53
|
+
},
|
|
54
|
+
padding: {
|
|
55
|
+
backgroundColor: "bisque",
|
|
56
|
+
height: Spacing.xSmall_8,
|
|
57
|
+
},
|
|
58
|
+
tail: {
|
|
59
|
+
backgroundColor: "green",
|
|
60
|
+
height: Spacing.large_24,
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
<View style={styles.exampleContainer}>
|
|
65
|
+
<View style={styles.guideContainer}>
|
|
66
|
+
<View key="padleft" style={styles.padding} />
|
|
67
|
+
<View key="tail" style={styles.tail} />
|
|
68
|
+
<View key="padright" style={styles.padding} />
|
|
69
|
+
<Spring key="spring" />
|
|
70
|
+
</View>
|
|
71
|
+
<TooltipTail placement="right" />
|
|
72
|
+
</View>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Placement bottom
|
|
76
|
+
|
|
77
|
+
```jsx
|
|
78
|
+
import {StyleSheet} from "aphrodite";
|
|
79
|
+
import {View} from "@khanacademy/wonder-blocks-core";
|
|
80
|
+
import {Spring} from "@khanacademy/wonder-blocks-layout";
|
|
81
|
+
import Spacing from "@khanacademy/wonder-blocks-spacing";
|
|
82
|
+
|
|
83
|
+
const styles = StyleSheet.create({
|
|
84
|
+
guideContainer: {
|
|
85
|
+
flexDirection: "row",
|
|
86
|
+
height: Spacing.xxxSmall_4,
|
|
87
|
+
},
|
|
88
|
+
padding: {
|
|
89
|
+
backgroundColor: "bisque",
|
|
90
|
+
width: Spacing.xSmall_8,
|
|
91
|
+
},
|
|
92
|
+
tail: {
|
|
93
|
+
backgroundColor: "green",
|
|
94
|
+
width: Spacing.large_24,
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
<View>
|
|
99
|
+
<View style={styles.guideContainer}>
|
|
100
|
+
<View key="padleft" style={styles.padding} />
|
|
101
|
+
<View key="tail" style={styles.tail} />
|
|
102
|
+
<View key="padright" style={styles.padding} />
|
|
103
|
+
<Spring key="spring" />
|
|
104
|
+
</View>
|
|
105
|
+
<TooltipTail placement="bottom" />
|
|
106
|
+
</View>
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Placement left
|
|
110
|
+
|
|
111
|
+
```jsx
|
|
112
|
+
import {StyleSheet} from "aphrodite";
|
|
113
|
+
import {View} from "@khanacademy/wonder-blocks-core";
|
|
114
|
+
import {Spring} from "@khanacademy/wonder-blocks-layout";
|
|
115
|
+
import Spacing from "@khanacademy/wonder-blocks-spacing";
|
|
116
|
+
|
|
117
|
+
const styles = StyleSheet.create({
|
|
118
|
+
exampleContainer: {
|
|
119
|
+
flexDirection: "row",
|
|
120
|
+
},
|
|
121
|
+
guideContainer: {
|
|
122
|
+
width: Spacing.xxxSmall_4,
|
|
123
|
+
},
|
|
124
|
+
padding: {
|
|
125
|
+
backgroundColor: "bisque",
|
|
126
|
+
height: Spacing.xSmall_8,
|
|
127
|
+
},
|
|
128
|
+
tail: {
|
|
129
|
+
backgroundColor: "green",
|
|
130
|
+
height: Spacing.large_24,
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
<View style={styles.exampleContainer}>
|
|
135
|
+
<TooltipTail placement="left" />
|
|
136
|
+
<View style={styles.guideContainer}>
|
|
137
|
+
<View key="padleft" style={styles.padding} />
|
|
138
|
+
<View key="tail" style={styles.tail} />
|
|
139
|
+
<View key="padright" style={styles.padding} />
|
|
140
|
+
<Spring key="spring" />
|
|
141
|
+
</View>
|
|
142
|
+
</View>
|
|
143
|
+
```
|