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