@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,80 @@
1
+ // @flow
2
+ import * as React from "react";
3
+ import * as ReactDOM from "react-dom";
4
+ import {mount} from "enzyme";
5
+
6
+ import {View} from "@khanacademy/wonder-blocks-core";
7
+
8
+ import TooltipBubble from "../tooltip-bubble.js";
9
+ import typeof TooltipContent from "../tooltip-content.js";
10
+
11
+ const sleep = (duration = 0) =>
12
+ new Promise((resolve, reject) => setTimeout(resolve, duration));
13
+
14
+ describe("TooltipBubble", () => {
15
+ // A little helper method to make the actual test more readable.
16
+ const makePopperProps = () => ({
17
+ placement: "top",
18
+ tailOffset: {
19
+ top: "0",
20
+ left: "50",
21
+ bottom: undefined,
22
+ right: undefined,
23
+ transform: "translate3d(50, 0, 0)",
24
+ },
25
+ });
26
+
27
+ test("updates reference to bubble container", async () => {
28
+ // Arrange
29
+ const bubbleNode = await new Promise((resolve) => {
30
+ // Get some props and set the ref to our assert, that way we assert
31
+ // when the bubble component is mounted.
32
+ const props = makePopperProps();
33
+
34
+ // Do some casting to pretend this is `TooltipContent`. That way
35
+ // we are isolating behaviors a bit more.
36
+ const fakeContent = (((
37
+ <View id="content">Some content</View>
38
+ ): any): React.Element<TooltipContent>);
39
+ const nodes = (
40
+ <View>
41
+ <TooltipBubble
42
+ id="bubble"
43
+ placement={props.placement}
44
+ tailOffset={props.tailOffset}
45
+ updateBubbleRef={resolve}
46
+ onActiveChanged={() => {}}
47
+ >
48
+ {fakeContent}
49
+ </TooltipBubble>
50
+ </View>
51
+ );
52
+
53
+ // Act
54
+ mount(nodes);
55
+ });
56
+
57
+ /**
58
+ * All we're doing is making sure we got called and verifying that
59
+ * we got called with an element we expect.
60
+ */
61
+ // Assert
62
+ // Did we get a node?
63
+ expect(bubbleNode).toBeDefined();
64
+
65
+ // Is the node a mounted element?
66
+ const realElement = ReactDOM.findDOMNode(bubbleNode);
67
+ expect(realElement instanceof Element).toBeTruthy();
68
+
69
+ // Keep flow happy...
70
+ if (realElement instanceof Element) {
71
+ // Did we apply our data attribute?
72
+ expect(realElement.getAttribute("data-placement")).toBe("top");
73
+
74
+ // Did we render our content?
75
+ await sleep();
76
+ const contentElement = document.getElementById("content");
77
+ expect(contentElement).toBeDefined();
78
+ }
79
+ });
80
+ });
@@ -0,0 +1,71 @@
1
+ // @flow
2
+ import * as React from "react";
3
+ import * as ReactDOM from "react-dom";
4
+ import {mount} from "enzyme";
5
+
6
+ import {View} from "@khanacademy/wonder-blocks-core";
7
+
8
+ import typeof TooltipBubble from "../tooltip-bubble.js";
9
+ import TooltipPopper from "../tooltip-popper.js";
10
+
11
+ type State = {|ref: ?HTMLElement|};
12
+ /**
13
+ * A little wrapper for the TooltipPopper so that we can provide an anchor
14
+ * element reference and test that the children get rendered.
15
+ */
16
+ class TestHarness extends React.Component<any, State> {
17
+ state: State = {
18
+ ref: null,
19
+ };
20
+
21
+ updateRef(ref) {
22
+ const actualRef = ref && ReactDOM.findDOMNode(ref);
23
+ if (actualRef && this.state.ref !== actualRef) {
24
+ this.setState({ref: ((actualRef: any): ?HTMLElement)});
25
+ }
26
+ }
27
+
28
+ render(): React.Node {
29
+ const fakeBubble = (((
30
+ <View ref={(ref) => this.props.resultRef(ref)}>Fake bubble</View>
31
+ ): any): React.Element<TooltipBubble>);
32
+ return (
33
+ <View>
34
+ <View ref={(ref) => this.updateRef(ref)}>Anchor</View>
35
+ <TooltipPopper
36
+ placement={this.props.placement}
37
+ anchorElement={this.state.ref}
38
+ >
39
+ {(props) => fakeBubble}
40
+ </TooltipPopper>
41
+ </View>
42
+ );
43
+ }
44
+ }
45
+
46
+ describe("TooltipPopper", () => {
47
+ // The TooltipPopper component is just a wrapper around react-popper.
48
+ // PopperJS requires full visual rendering and we don't do that here as
49
+ // we're not in a browser.
50
+ // So, let's do a test that we at least render the content how we expect
51
+ // and use other things to test the overall placement things.
52
+ test("ensure component renders", async () => {
53
+ // Arrange
54
+ const ref = await new Promise((resolve, reject) => {
55
+ const nodes = (
56
+ <View>
57
+ <TestHarness placement="bottom" resultRef={resolve} />
58
+ </View>
59
+ );
60
+ mount(nodes);
61
+ });
62
+
63
+ if (!ref) {
64
+ return;
65
+ }
66
+
67
+ // Act
68
+ // Assert
69
+ expect(ref).toBeDefined();
70
+ });
71
+ });
@@ -0,0 +1,117 @@
1
+ // @flow
2
+ import * as React from "react";
3
+ import {shallow} from "enzyme";
4
+
5
+ import TooltipTail from "../tooltip-tail.js";
6
+
7
+ import type {Placement} from "../../util/types.js";
8
+
9
+ describe("TooltipTail", () => {
10
+ describe("#render", () => {
11
+ test("unknown placement, throws", () => {
12
+ // Arrange
13
+ const fakePlacement = (("notaplacement": any): Placement);
14
+ const nodes = <TooltipTail placement={fakePlacement} />;
15
+
16
+ // Act
17
+ const underTest = () => shallow(nodes);
18
+
19
+ // Assert
20
+ expect(underTest).toThrowErrorMatchingSnapshot();
21
+ });
22
+
23
+ test("known placement, does not throw", () => {
24
+ // Arrange
25
+ const testPoints = ["top", "right", "bottom", "left"];
26
+ const makeNode = (p) => <TooltipTail placement={p} />;
27
+
28
+ // Act
29
+ const testees = testPoints.map((tp) => () => shallow(makeNode(tp)));
30
+
31
+ // Assert
32
+ for (const testee of testees) {
33
+ expect(testee).not.toThrowError();
34
+ }
35
+ });
36
+ });
37
+
38
+ describe("INTERNALS", () => {
39
+ // We have some code internally that is there for code maintenance to
40
+ // catch if we add a new placement string and forget to update one of
41
+ // our methods. These tests are to verify those maintenance checks
42
+ // so they poke at the insides of the TooltipTail. We only test the
43
+ // throw case in these examples and rely on testing the external
44
+ // behavior of the component to verify the expected execution paths.
45
+ test("_getFilterPositioning throws on bad placement", () => {
46
+ // Arrange
47
+ const fakePlacement = (("notaplacement": any): Placement);
48
+ // We need an instance so let's make one with a valid placement so
49
+ // that we can render it.
50
+ const nodes = <TooltipTail placement="top" />;
51
+ const wrapper = shallow(nodes);
52
+ const tailInstance = wrapper.instance();
53
+ // Sneakily change props so as not to re-render.
54
+ // This means getting rid of the read-only props and
55
+ // recreating so that we can write a fake placement value
56
+ // for testing.
57
+ const oldProps = tailInstance.props;
58
+ // $FlowIgnore
59
+ delete tailInstance.props;
60
+ tailInstance.props = {
61
+ ...oldProps,
62
+ placement: fakePlacement,
63
+ };
64
+
65
+ // Act
66
+ const underTest = () => tailInstance._getFilterPositioning();
67
+
68
+ // Assert
69
+ expect(underTest).toThrowErrorMatchingSnapshot();
70
+ });
71
+
72
+ test("_calculateDimensionsFromPlacement throws on bad placement", () => {
73
+ // Arrange
74
+ const fakePlacement = (("notaplacement": any): Placement);
75
+ // We need an instance so let's make one with a valid placement so
76
+ // that we can render it.
77
+ const nodes = <TooltipTail placement="top" />;
78
+ const wrapper = shallow(nodes);
79
+ const tailInstance = wrapper.instance();
80
+ // Sneakily change props so as not to re-render.
81
+ // This means getting rid of the read-only props and
82
+ // recreating so that we can write a fake placement value
83
+ // for testing.
84
+ const oldProps = tailInstance.props;
85
+ // $FlowIgnore
86
+ delete tailInstance.props;
87
+ tailInstance.props = {
88
+ ...oldProps,
89
+ placement: fakePlacement,
90
+ };
91
+
92
+ // Act
93
+ const underTest = () =>
94
+ tailInstance._calculateDimensionsFromPlacement();
95
+
96
+ // Assert
97
+ expect(underTest).toThrowErrorMatchingSnapshot();
98
+ });
99
+
100
+ test("_minDistanceFromCorners throws on bad placement", () => {
101
+ // Arrange
102
+ const fakePlacement = (("notaplacement": any): Placement);
103
+ // We need an instance so let's make one with a valid placement so
104
+ // that we can render it.
105
+ const nodes = <TooltipTail placement="top" />;
106
+ const wrapper = shallow(nodes);
107
+ const tailInstance = wrapper.instance();
108
+
109
+ // Act
110
+ const underTest = () =>
111
+ tailInstance._minDistanceFromCorners(fakePlacement);
112
+
113
+ // Assert
114
+ expect(underTest).toThrowErrorMatchingSnapshot();
115
+ });
116
+ });
117
+ });
@@ -0,0 +1,79 @@
1
+ // @flow
2
+ import * as React from "react";
3
+
4
+ import {render, screen, fireEvent} from "@testing-library/react";
5
+ // eslint-disable-next-line import/no-unassigned-import
6
+ import "@testing-library/jest-dom/extend-expect";
7
+ import userEvent from "@testing-library/user-event";
8
+
9
+ import Tooltip from "../tooltip.js";
10
+
11
+ describe("tooltip integration tests", () => {
12
+ beforeEach(() => {
13
+ jest.useFakeTimers();
14
+ });
15
+
16
+ it("should set timeoutId be null when TooltipBubble is active", () => {
17
+ // Arrange
18
+ render(<Tooltip content="hello, world">an anchor</Tooltip>);
19
+ const anchor = screen.getByText("an anchor");
20
+
21
+ // Act
22
+ userEvent.hover(anchor);
23
+ // There's a 100ms delay before TooltipAnchor calls _setActiveState with
24
+ // instant set to true. This second call is what actually triggers the
25
+ // call to this.props.onActiveChanged() which updates Tooltip's active
26
+ // state.
27
+ jest.runAllTimers();
28
+
29
+ // Assert
30
+ expect(screen.getByRole("tooltip")).toBeInTheDocument();
31
+ });
32
+
33
+ it("should hide the bubble on mouseleave on TooltipAnchor", () => {
34
+ // Arrange
35
+ render(<Tooltip content="hello, world">an anchor</Tooltip>);
36
+
37
+ const anchor = screen.getByText("an anchor");
38
+ userEvent.hover(anchor);
39
+
40
+ // Act
41
+ userEvent.unhover(anchor);
42
+ // There's a 100ms delay before TooltipAnchor calls _setActiveState with
43
+ // instant set to true. This second call is what actually triggers the
44
+ // call to this.props.onActiveChanged() which updates Tooltip's active
45
+ // state.
46
+ jest.runAllTimers();
47
+
48
+ // Assert
49
+ expect(screen.queryByRole("tooltip")).toBeNull();
50
+ });
51
+
52
+ it("should close TooltipBubble on mouseleave on TooltipBubble", async () => {
53
+ // Arrange
54
+ render(<Tooltip content="hello, world">an anchor</Tooltip>);
55
+
56
+ const anchor = screen.getByText("an anchor");
57
+ userEvent.hover(anchor);
58
+ // hover on bubble to keep it active
59
+ const bubbleWrapper = await screen.findByRole("tooltip");
60
+ userEvent.unhover(anchor);
61
+
62
+ // Used because RTL complains about the bubble containing a child
63
+ // element with pointerEvents: none
64
+ // eslint-disable-next-line testing-library/prefer-user-event
65
+ fireEvent.mouseEnter(bubbleWrapper);
66
+
67
+ // Act
68
+ // eslint-disable-next-line testing-library/prefer-user-event
69
+ fireEvent.mouseLeave(bubbleWrapper);
70
+ // There's a 100ms delay before TooltipAnchor calls _setActiveState with
71
+ // instant set to true. This second call is what actually triggers the
72
+ // call to this.props.onActiveChanged() which updates Tooltip's active
73
+ // state.
74
+ jest.runAllTimers();
75
+
76
+ // Assert
77
+ expect(screen.queryByRole("tooltip")).not.toBeInTheDocument();
78
+ });
79
+ });