@khanacademy/wonder-blocks-tooltip 1.3.23 → 1.4.1
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/dist/es/index.js +6 -0
- package/dist/index.js +10 -0
- package/package.json +2 -2
- package/src/components/__docs__/tooltip.stories.js +96 -6
- package/src/components/__tests__/__snapshots__/tooltip.test.js.snap +6 -0
- package/src/components/__tests__/tooltip.test.js +50 -0
- package/src/components/tooltip.js +23 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @khanacademy/wonder-blocks-tooltip
|
|
2
2
|
|
|
3
|
+
## 1.4.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- @khanacademy/wonder-blocks-modal@3.0.2
|
|
8
|
+
|
|
9
|
+
## 1.4.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- b784d7a8: Allow Tooltip to be open/closed by its parent (controlled)
|
|
14
|
+
|
|
3
15
|
## 1.3.23
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/dist/es/index.js
CHANGED
|
@@ -781,6 +781,12 @@ class Tooltip extends React.Component {
|
|
|
781
781
|
};
|
|
782
782
|
}
|
|
783
783
|
|
|
784
|
+
static getDerivedStateFromProps(props, state) {
|
|
785
|
+
return {
|
|
786
|
+
active: typeof props.opened === "boolean" ? props.opened : state.active
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
|
|
784
790
|
_updateAnchorElement(ref) {
|
|
785
791
|
if (ref && ref !== this.state.anchorElement) {
|
|
786
792
|
this.setState({
|
package/dist/index.js
CHANGED
|
@@ -845,6 +845,16 @@ class Tooltip extends react__WEBPACK_IMPORTED_MODULE_0__["Component"] {
|
|
|
845
845
|
};
|
|
846
846
|
}
|
|
847
847
|
|
|
848
|
+
/**
|
|
849
|
+
* Used to sync the `opened` state when Tooltip acts as a controlled
|
|
850
|
+
* component
|
|
851
|
+
*/
|
|
852
|
+
static getDerivedStateFromProps(props, state) {
|
|
853
|
+
return {
|
|
854
|
+
active: typeof props.opened === "boolean" ? props.opened : state.active
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
|
|
848
858
|
_updateAnchorElement(ref) {
|
|
849
859
|
if (ref && ref !== this.state.anchorElement) {
|
|
850
860
|
this.setState({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@khanacademy/wonder-blocks-tooltip",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.1",
|
|
4
4
|
"design": "v1",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"@khanacademy/wonder-blocks-color": "^1.2.0",
|
|
20
20
|
"@khanacademy/wonder-blocks-core": "^4.5.0",
|
|
21
21
|
"@khanacademy/wonder-blocks-layout": "^1.4.12",
|
|
22
|
-
"@khanacademy/wonder-blocks-modal": "^3.0.
|
|
22
|
+
"@khanacademy/wonder-blocks-modal": "^3.0.2",
|
|
23
23
|
"@khanacademy/wonder-blocks-spacing": "^3.0.5",
|
|
24
24
|
"@khanacademy/wonder-blocks-typography": "^1.1.34"
|
|
25
25
|
},
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
// @flow
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
import {StyleSheet} from "aphrodite";
|
|
4
|
+
|
|
5
|
+
import {within, userEvent} from "@storybook/testing-library";
|
|
6
|
+
import {expect} from "@storybook/jest";
|
|
7
|
+
|
|
4
8
|
import Button from "@khanacademy/wonder-blocks-button";
|
|
5
9
|
import Color from "@khanacademy/wonder-blocks-color";
|
|
6
10
|
import {View} from "@khanacademy/wonder-blocks-core";
|
|
@@ -27,15 +31,15 @@ export default {
|
|
|
27
31
|
placement: "top",
|
|
28
32
|
},
|
|
29
33
|
parameters: {
|
|
30
|
-
// TODO(WB-1170): Reassess this after investigating more about Chromatic
|
|
31
|
-
// flakyness.
|
|
32
|
-
chromatic: {
|
|
33
|
-
disableSnapshot: true,
|
|
34
|
-
},
|
|
35
34
|
componentSubtitle: ((
|
|
36
35
|
<ComponentInfo name={name} version={version} />
|
|
37
36
|
): any),
|
|
38
37
|
},
|
|
38
|
+
decorators: [
|
|
39
|
+
(Story: any): React.Node => (
|
|
40
|
+
<View style={styles.storyCanvas}>{Story()}</View>
|
|
41
|
+
),
|
|
42
|
+
],
|
|
39
43
|
};
|
|
40
44
|
|
|
41
45
|
const Template = (args) => <Tooltip {...args} />;
|
|
@@ -50,6 +54,22 @@ Default.args = {
|
|
|
50
54
|
children: "some text",
|
|
51
55
|
};
|
|
52
56
|
|
|
57
|
+
Default.play = async ({canvasElement}) => {
|
|
58
|
+
// Arrange
|
|
59
|
+
// NOTE: Using `body` here to work with React Portals.
|
|
60
|
+
const canvas = within(canvasElement.ownerDocument.body);
|
|
61
|
+
|
|
62
|
+
// Act
|
|
63
|
+
// Triggers the hover state
|
|
64
|
+
const text = await canvas.findByText("some text");
|
|
65
|
+
await userEvent.hover(text);
|
|
66
|
+
|
|
67
|
+
// Assert
|
|
68
|
+
await expect(
|
|
69
|
+
await canvas.findByText("This is a text tooltip on the top"),
|
|
70
|
+
).toBeInTheDocument();
|
|
71
|
+
};
|
|
72
|
+
|
|
53
73
|
/**
|
|
54
74
|
* Complex anchor & title tooltip
|
|
55
75
|
*/
|
|
@@ -57,9 +77,10 @@ export const ComplexAnchorAndTitle: StoryComponentType = Template.bind({});
|
|
|
57
77
|
|
|
58
78
|
ComplexAnchorAndTitle.args = {
|
|
59
79
|
forceAnchorFocusivity: false,
|
|
80
|
+
placement: "bottom",
|
|
60
81
|
id: "my-a11y-tooltip",
|
|
61
82
|
title: "This tooltip has a title",
|
|
62
|
-
content: "I'm at the
|
|
83
|
+
content: "I'm at the bottom!",
|
|
63
84
|
children: (
|
|
64
85
|
<View>
|
|
65
86
|
Some text
|
|
@@ -73,6 +94,22 @@ ComplexAnchorAndTitle.args = {
|
|
|
73
94
|
),
|
|
74
95
|
};
|
|
75
96
|
|
|
97
|
+
ComplexAnchorAndTitle.play = async ({canvasElement}) => {
|
|
98
|
+
// Arrange
|
|
99
|
+
// NOTE: Using `body` here to work with React Portals.
|
|
100
|
+
const canvas = within(canvasElement.ownerDocument.body);
|
|
101
|
+
|
|
102
|
+
// Act
|
|
103
|
+
// Triggers the hover state
|
|
104
|
+
const text = await canvas.findByText("Some text");
|
|
105
|
+
await userEvent.hover(text);
|
|
106
|
+
|
|
107
|
+
// Assert
|
|
108
|
+
await expect(
|
|
109
|
+
await canvas.findByText("This tooltip has a title"),
|
|
110
|
+
).toBeInTheDocument();
|
|
111
|
+
};
|
|
112
|
+
|
|
76
113
|
ComplexAnchorAndTitle.parameters = {
|
|
77
114
|
docs: {
|
|
78
115
|
description: {
|
|
@@ -102,6 +139,10 @@ export const AnchorInScrollableParent: StoryComponentType = () => (
|
|
|
102
139
|
);
|
|
103
140
|
|
|
104
141
|
AnchorInScrollableParent.parameters = {
|
|
142
|
+
// Disable Chromatic because it only shows the trigger element.
|
|
143
|
+
chromatic: {
|
|
144
|
+
disableSnapshot: true,
|
|
145
|
+
},
|
|
105
146
|
docs: {
|
|
106
147
|
description: {
|
|
107
148
|
story: "In this example, we have the anchor in a scrollable parent. Notice how, when the anchor is focused but scrolled out of bounds, the tooltip disappears.",
|
|
@@ -139,6 +180,10 @@ export const TooltipInModal: StoryComponentType = () => {
|
|
|
139
180
|
};
|
|
140
181
|
|
|
141
182
|
TooltipInModal.parameters = {
|
|
183
|
+
// Disable Chromatic because it initially renders the modal offscreen.
|
|
184
|
+
chromatic: {
|
|
185
|
+
disableSnapshot: true,
|
|
186
|
+
},
|
|
142
187
|
docs: {
|
|
143
188
|
description: {
|
|
144
189
|
story: "This checks that the tooltip works how we want inside a modal. Click the button to take a look.",
|
|
@@ -169,6 +214,10 @@ export const SideBySide: StoryComponentType = () => (
|
|
|
169
214
|
SideBySide.storyName = "Side-by-side";
|
|
170
215
|
|
|
171
216
|
SideBySide.parameters = {
|
|
217
|
+
// Disable Chromatic because it only shows the trigger element.
|
|
218
|
+
chromatic: {
|
|
219
|
+
disableSnapshot: true,
|
|
220
|
+
},
|
|
172
221
|
docs: {
|
|
173
222
|
description: {
|
|
174
223
|
story: "Here, we can see that the first tooltip shown has an initial delay before it appears, as does the last tooltip shown, yet when moving between tooltipped items, the transition from one to another is instantaneous.",
|
|
@@ -213,7 +262,48 @@ TooltipOnButtons.parameters = {
|
|
|
213
262
|
},
|
|
214
263
|
};
|
|
215
264
|
|
|
265
|
+
/**
|
|
266
|
+
* Opening a tooltip programatically (Controlled)
|
|
267
|
+
*/
|
|
268
|
+
export const Controlled: StoryComponentType = () => {
|
|
269
|
+
const [opened, setOpened] = React.useState(true);
|
|
270
|
+
const buttonText = `Click to ${opened ? "close" : "open"} tooltip`;
|
|
271
|
+
|
|
272
|
+
return (
|
|
273
|
+
<View style={[styles.centered, styles.row]}>
|
|
274
|
+
<Tooltip
|
|
275
|
+
content="You opened the tooltip with a button"
|
|
276
|
+
opened={opened}
|
|
277
|
+
>
|
|
278
|
+
tooltip
|
|
279
|
+
</Tooltip>
|
|
280
|
+
<Button onClick={() => setOpened(!opened)}>{buttonText}</Button>
|
|
281
|
+
</View>
|
|
282
|
+
);
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
Controlled.parameters = {
|
|
286
|
+
docs: {
|
|
287
|
+
description: {
|
|
288
|
+
story: `Sometimes you'll want to trigger a tooltip programmatically.
|
|
289
|
+
This can be done by setting the \`opened\` prop to \`true\`. In
|
|
290
|
+
this situation the \`Tooltip\` is a controlled component. The
|
|
291
|
+
parent is responsible for managing the opening/closing of the
|
|
292
|
+
tooltip when using this prop. This means that you'll also have
|
|
293
|
+
to update \`opened\` to \`false\` in response to the
|
|
294
|
+
\`onClose\` callback being triggered.`,
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
};
|
|
298
|
+
|
|
216
299
|
const styles = StyleSheet.create({
|
|
300
|
+
storyCanvas: {
|
|
301
|
+
// NOTE: This is needed for Chromatic to include the tooltip bubble.
|
|
302
|
+
minHeight: 280,
|
|
303
|
+
padding: Spacing.xxxLarge_64,
|
|
304
|
+
justifyContent: "center",
|
|
305
|
+
textAlign: "center",
|
|
306
|
+
},
|
|
217
307
|
row: {
|
|
218
308
|
flexDirection: "row",
|
|
219
309
|
},
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
2
2
|
|
|
3
|
+
exports[`Tooltip Controlled can be opened programmatically: Similar to <TooltipContent>Content</TooltipContent> 1`] = `
|
|
4
|
+
<TooltipContent>
|
|
5
|
+
Content
|
|
6
|
+
</TooltipContent>
|
|
7
|
+
`;
|
|
8
|
+
|
|
3
9
|
exports[`Tooltip content is TooltipContent with title, overrides title of TooltipContent: Similar to <Body>Some custom content</Body> 1`] = `
|
|
4
10
|
<Body
|
|
5
11
|
tag="span"
|
|
@@ -399,4 +399,54 @@ describe("Tooltip", () => {
|
|
|
399
399
|
});
|
|
400
400
|
});
|
|
401
401
|
});
|
|
402
|
+
|
|
403
|
+
describe("Controlled", () => {
|
|
404
|
+
test("can be opened programmatically", async () => {
|
|
405
|
+
// Arrange
|
|
406
|
+
await new Promise((resolve) => {
|
|
407
|
+
const nodes = (
|
|
408
|
+
<View>
|
|
409
|
+
<Tooltip id="tooltip" content="Content" opened={true}>
|
|
410
|
+
<View ref={resolve}>Anchor</View>
|
|
411
|
+
</Tooltip>
|
|
412
|
+
</View>
|
|
413
|
+
);
|
|
414
|
+
mount(nodes);
|
|
415
|
+
});
|
|
416
|
+
jest.runOnlyPendingTimers();
|
|
417
|
+
|
|
418
|
+
// Act
|
|
419
|
+
// Flow doesn't like jest mocks
|
|
420
|
+
// $FlowFixMe[prop-missing]
|
|
421
|
+
const result = TooltipBubble.mock.instances[0].props["children"];
|
|
422
|
+
|
|
423
|
+
// Assert
|
|
424
|
+
expect(result).toMatchSnapshot(
|
|
425
|
+
`Similar to <TooltipContent>Content</TooltipContent>`,
|
|
426
|
+
);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
test("can be closed programmatically", async () => {
|
|
430
|
+
// Arrange
|
|
431
|
+
await new Promise((resolve) => {
|
|
432
|
+
const nodes = (
|
|
433
|
+
<View>
|
|
434
|
+
<Tooltip id="tooltip" content="Content" opened={false}>
|
|
435
|
+
<View ref={resolve}>Anchor</View>
|
|
436
|
+
</Tooltip>
|
|
437
|
+
</View>
|
|
438
|
+
);
|
|
439
|
+
mount(nodes);
|
|
440
|
+
});
|
|
441
|
+
jest.runOnlyPendingTimers();
|
|
442
|
+
|
|
443
|
+
// Act
|
|
444
|
+
// Flow doesn't like jest mocks
|
|
445
|
+
// $FlowFixMe[prop-missing]
|
|
446
|
+
const result = TooltipBubble.mock.instances[0];
|
|
447
|
+
|
|
448
|
+
// Assert
|
|
449
|
+
expect(result).toBeUndefined();
|
|
450
|
+
});
|
|
451
|
+
});
|
|
402
452
|
});
|
|
@@ -96,6 +96,15 @@ type Props = {|
|
|
|
96
96
|
*/
|
|
97
97
|
placement: Placement,
|
|
98
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Renders the tooltip when true, renders nothing when false.
|
|
101
|
+
*
|
|
102
|
+
* Using this prop makes the component behave as a controlled component. The
|
|
103
|
+
* parent is responsible for managing the opening/closing of the tooltip
|
|
104
|
+
* when using this prop.
|
|
105
|
+
*/
|
|
106
|
+
opened?: boolean,
|
|
107
|
+
|
|
99
108
|
/**
|
|
100
109
|
* Test ID used for e2e testing.
|
|
101
110
|
*/
|
|
@@ -152,6 +161,20 @@ export default class Tooltip extends React.Component<Props, State> {
|
|
|
152
161
|
placement: "top",
|
|
153
162
|
};
|
|
154
163
|
|
|
164
|
+
/**
|
|
165
|
+
* Used to sync the `opened` state when Tooltip acts as a controlled
|
|
166
|
+
* component
|
|
167
|
+
*/
|
|
168
|
+
static getDerivedStateFromProps(
|
|
169
|
+
props: Props,
|
|
170
|
+
state: State,
|
|
171
|
+
): ?Partial<State> {
|
|
172
|
+
return {
|
|
173
|
+
active:
|
|
174
|
+
typeof props.opened === "boolean" ? props.opened : state.active,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
155
178
|
state: State = {
|
|
156
179
|
active: false,
|
|
157
180
|
activeBubble: false,
|