@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.
Files changed (36) hide show
  1. package/LICENSE +21 -0
  2. package/dist/es/index.js +1133 -0
  3. package/dist/index.js +1389 -0
  4. package/dist/index.js.flow +2 -0
  5. package/docs.md +11 -0
  6. package/package.json +37 -0
  7. package/src/__tests__/__snapshots__/generated-snapshot.test.js.snap +2674 -0
  8. package/src/__tests__/generated-snapshot.test.js +475 -0
  9. package/src/components/__tests__/__snapshots__/tooltip-tail.test.js.snap +9 -0
  10. package/src/components/__tests__/__snapshots__/tooltip.test.js.snap +47 -0
  11. package/src/components/__tests__/tooltip-anchor.test.js +987 -0
  12. package/src/components/__tests__/tooltip-bubble.test.js +80 -0
  13. package/src/components/__tests__/tooltip-popper.test.js +71 -0
  14. package/src/components/__tests__/tooltip-tail.test.js +117 -0
  15. package/src/components/__tests__/tooltip.integration.test.js +79 -0
  16. package/src/components/__tests__/tooltip.test.js +401 -0
  17. package/src/components/tooltip-anchor.js +330 -0
  18. package/src/components/tooltip-bubble.js +150 -0
  19. package/src/components/tooltip-bubble.md +92 -0
  20. package/src/components/tooltip-content.js +76 -0
  21. package/src/components/tooltip-content.md +34 -0
  22. package/src/components/tooltip-popper.js +101 -0
  23. package/src/components/tooltip-tail.js +462 -0
  24. package/src/components/tooltip-tail.md +143 -0
  25. package/src/components/tooltip.js +235 -0
  26. package/src/components/tooltip.md +194 -0
  27. package/src/components/tooltip.stories.js +76 -0
  28. package/src/index.js +12 -0
  29. package/src/util/__tests__/__snapshots__/active-tracker.test.js.snap +3 -0
  30. package/src/util/__tests__/__snapshots__/ref-tracker.test.js.snap +3 -0
  31. package/src/util/__tests__/active-tracker.test.js +142 -0
  32. package/src/util/__tests__/ref-tracker.test.js +153 -0
  33. package/src/util/active-tracker.js +94 -0
  34. package/src/util/constants.js +7 -0
  35. package/src/util/ref-tracker.js +46 -0
  36. 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
+ ```