@khanacademy/wonder-blocks-tooltip 1.3.22 → 1.4.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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @khanacademy/wonder-blocks-tooltip
2
2
 
3
+ ## 1.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - b784d7a8: Allow Tooltip to be open/closed by its parent (controlled)
8
+
9
+ ## 1.3.23
10
+
11
+ ### Patch Changes
12
+
13
+ - @khanacademy/wonder-blocks-modal@3.0.1
14
+
3
15
  ## 1.3.22
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.22",
3
+ "version": "1.4.0",
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.0",
22
+ "@khanacademy/wonder-blocks-modal": "^3.0.1",
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 top!",
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,