@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.
- package/LICENSE +21 -0
- package/dist/es/index.js +1133 -0
- package/dist/index.js +1389 -0
- package/dist/index.js.flow +2 -0
- package/docs.md +11 -0
- package/package.json +37 -0
- package/src/__tests__/__snapshots__/generated-snapshot.test.js.snap +2674 -0
- package/src/__tests__/generated-snapshot.test.js +475 -0
- package/src/components/__tests__/__snapshots__/tooltip-tail.test.js.snap +9 -0
- package/src/components/__tests__/__snapshots__/tooltip.test.js.snap +47 -0
- package/src/components/__tests__/tooltip-anchor.test.js +987 -0
- package/src/components/__tests__/tooltip-bubble.test.js +80 -0
- package/src/components/__tests__/tooltip-popper.test.js +71 -0
- package/src/components/__tests__/tooltip-tail.test.js +117 -0
- package/src/components/__tests__/tooltip.integration.test.js +79 -0
- package/src/components/__tests__/tooltip.test.js +401 -0
- package/src/components/tooltip-anchor.js +330 -0
- package/src/components/tooltip-bubble.js +150 -0
- package/src/components/tooltip-bubble.md +92 -0
- package/src/components/tooltip-content.js +76 -0
- package/src/components/tooltip-content.md +34 -0
- package/src/components/tooltip-popper.js +101 -0
- package/src/components/tooltip-tail.js +462 -0
- package/src/components/tooltip-tail.md +143 -0
- package/src/components/tooltip.js +235 -0
- package/src/components/tooltip.md +194 -0
- package/src/components/tooltip.stories.js +76 -0
- package/src/index.js +12 -0
- package/src/util/__tests__/__snapshots__/active-tracker.test.js.snap +3 -0
- package/src/util/__tests__/__snapshots__/ref-tracker.test.js.snap +3 -0
- package/src/util/__tests__/active-tracker.test.js +142 -0
- package/src/util/__tests__/ref-tracker.test.js +153 -0
- package/src/util/active-tracker.js +94 -0
- package/src/util/constants.js +7 -0
- package/src/util/ref-tracker.js +46 -0
- package/src/util/types.js +29 -0
|
@@ -0,0 +1,401 @@
|
|
|
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
|
+
import {Body, HeadingSmall} from "@khanacademy/wonder-blocks-typography";
|
|
8
|
+
|
|
9
|
+
import Tooltip from "../tooltip.js";
|
|
10
|
+
import TooltipBubble from "../tooltip-bubble.js";
|
|
11
|
+
import TooltipContent from "../tooltip-content.js";
|
|
12
|
+
|
|
13
|
+
const mockIDENTIFIER = "mock-identifier";
|
|
14
|
+
jest.mock("../tooltip-bubble.js");
|
|
15
|
+
jest.mock("@khanacademy/wonder-blocks-core", () => {
|
|
16
|
+
const Core = jest.requireActual("@khanacademy/wonder-blocks-core");
|
|
17
|
+
// We want all of Core to be the regular thing except for UniqueIDProvider
|
|
18
|
+
return {
|
|
19
|
+
...Core,
|
|
20
|
+
UniqueIDProvider: (props) =>
|
|
21
|
+
props.children({
|
|
22
|
+
get: () => mockIDENTIFIER,
|
|
23
|
+
}),
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("Tooltip", () => {
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
jest.clearAllMocks();
|
|
30
|
+
jest.clearAllTimers();
|
|
31
|
+
jest.useFakeTimers();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("content is a string, wraps in TooltipContent", () => {
|
|
35
|
+
test("with title", async () => {
|
|
36
|
+
// Arrange
|
|
37
|
+
const ref = await new Promise((resolve) => {
|
|
38
|
+
const nodes = (
|
|
39
|
+
<View>
|
|
40
|
+
<Tooltip id="tooltip" title="Title" content="Content">
|
|
41
|
+
<View ref={resolve}>Anchor</View>
|
|
42
|
+
</Tooltip>
|
|
43
|
+
</View>
|
|
44
|
+
);
|
|
45
|
+
mount(nodes);
|
|
46
|
+
});
|
|
47
|
+
const node = (ReactDOM.findDOMNode(ref): any);
|
|
48
|
+
node && node.dispatchEvent(new FocusEvent("focusin"));
|
|
49
|
+
jest.runOnlyPendingTimers();
|
|
50
|
+
|
|
51
|
+
// Act
|
|
52
|
+
// Flow doesn't like jest mocks
|
|
53
|
+
// $FlowFixMe[prop-missing]
|
|
54
|
+
const result = TooltipBubble.mock.instances[0].props["children"];
|
|
55
|
+
|
|
56
|
+
// Assert
|
|
57
|
+
expect(result).toMatchSnapshot(
|
|
58
|
+
`Similar to <TooltipContent title="Title">Content</TooltipContent>`,
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("without title", async () => {
|
|
63
|
+
// Arrange
|
|
64
|
+
const ref = await new Promise((resolve) => {
|
|
65
|
+
const nodes = (
|
|
66
|
+
<View>
|
|
67
|
+
<Tooltip id="tooltip" content="Content">
|
|
68
|
+
<View ref={resolve}>Anchor</View>
|
|
69
|
+
</Tooltip>
|
|
70
|
+
</View>
|
|
71
|
+
);
|
|
72
|
+
mount(nodes);
|
|
73
|
+
});
|
|
74
|
+
const node = (ReactDOM.findDOMNode(ref): any);
|
|
75
|
+
node && node.dispatchEvent(new FocusEvent("focusin"));
|
|
76
|
+
jest.runOnlyPendingTimers();
|
|
77
|
+
|
|
78
|
+
// Act
|
|
79
|
+
// Flow doesn't like jest mocks
|
|
80
|
+
// $FlowFixMe[prop-missing]
|
|
81
|
+
const result = TooltipBubble.mock.instances[0].props["children"];
|
|
82
|
+
|
|
83
|
+
// Assert
|
|
84
|
+
expect(result).toMatchSnapshot(
|
|
85
|
+
`Similar to <TooltipContent>Content</TooltipContent>`,
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("content is TooltipContent", () => {
|
|
91
|
+
test("with title, sets title of TooltipContent", async () => {
|
|
92
|
+
// Arrange
|
|
93
|
+
const ref = await new Promise((resolve) => {
|
|
94
|
+
const content = (
|
|
95
|
+
<TooltipContent>
|
|
96
|
+
<HeadingSmall>Some custom content</HeadingSmall>
|
|
97
|
+
</TooltipContent>
|
|
98
|
+
);
|
|
99
|
+
const title = <HeadingSmall>Title</HeadingSmall>;
|
|
100
|
+
const nodes = (
|
|
101
|
+
<View>
|
|
102
|
+
<Tooltip id="tooltip" title={title} content={content}>
|
|
103
|
+
<View ref={resolve}>Anchor</View>
|
|
104
|
+
</Tooltip>
|
|
105
|
+
</View>
|
|
106
|
+
);
|
|
107
|
+
mount(nodes);
|
|
108
|
+
});
|
|
109
|
+
const node = (ReactDOM.findDOMNode(ref): any);
|
|
110
|
+
node && node.dispatchEvent(new KeyboardEvent("focusin"));
|
|
111
|
+
jest.runOnlyPendingTimers();
|
|
112
|
+
|
|
113
|
+
// Act
|
|
114
|
+
// Flow doesn't like jest mocks
|
|
115
|
+
// $FlowFixMe[prop-missing]
|
|
116
|
+
const result = TooltipBubble.mock.instances[0].props["children"];
|
|
117
|
+
|
|
118
|
+
// Assert
|
|
119
|
+
expect(result.props["title"]).toMatchSnapshot(
|
|
120
|
+
`Similar to <HeadingSmall>Title</HeadingSmall>`,
|
|
121
|
+
);
|
|
122
|
+
expect(result.props["children"]).toMatchSnapshot(
|
|
123
|
+
`Similar to <HeadingSmall>Some custom content</HeadingSmall>`,
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("with title, overrides title of TooltipContent", async () => {
|
|
128
|
+
// Arrange
|
|
129
|
+
const ref = await new Promise((resolve) => {
|
|
130
|
+
const content = (
|
|
131
|
+
<TooltipContent title="Content title">
|
|
132
|
+
<Body>Some custom content</Body>
|
|
133
|
+
</TooltipContent>
|
|
134
|
+
);
|
|
135
|
+
const nodes = (
|
|
136
|
+
<View>
|
|
137
|
+
<Tooltip id="tooltip" title="Title" content={content}>
|
|
138
|
+
<View ref={resolve}>Anchor</View>
|
|
139
|
+
</Tooltip>
|
|
140
|
+
</View>
|
|
141
|
+
);
|
|
142
|
+
mount(nodes);
|
|
143
|
+
});
|
|
144
|
+
const node = (ReactDOM.findDOMNode(ref): any);
|
|
145
|
+
node && node.dispatchEvent(new KeyboardEvent("focusin"));
|
|
146
|
+
jest.runOnlyPendingTimers();
|
|
147
|
+
|
|
148
|
+
// Act
|
|
149
|
+
// Flow doesn't like jest mocks
|
|
150
|
+
// $FlowFixMe[prop-missing]
|
|
151
|
+
const result = TooltipBubble.mock.instances[0].props["children"];
|
|
152
|
+
|
|
153
|
+
// Assert
|
|
154
|
+
expect(result.props["title"]).toBe("Title");
|
|
155
|
+
expect(result.props["children"]).toMatchSnapshot(
|
|
156
|
+
`Similar to <Body>Some custom content</Body>`,
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("without title, renders content as-is", async () => {
|
|
161
|
+
// Arrange
|
|
162
|
+
const ref = await new Promise((resolve) => {
|
|
163
|
+
const content = (
|
|
164
|
+
<TooltipContent title="Content title">
|
|
165
|
+
<Body>Some custom content</Body>
|
|
166
|
+
</TooltipContent>
|
|
167
|
+
);
|
|
168
|
+
const nodes = (
|
|
169
|
+
<View>
|
|
170
|
+
<Tooltip id="tooltip" content={content}>
|
|
171
|
+
<View ref={resolve}>Anchor</View>
|
|
172
|
+
</Tooltip>
|
|
173
|
+
</View>
|
|
174
|
+
);
|
|
175
|
+
mount(nodes);
|
|
176
|
+
});
|
|
177
|
+
const node = (ReactDOM.findDOMNode(ref): any);
|
|
178
|
+
node && node.dispatchEvent(new KeyboardEvent("focusin"));
|
|
179
|
+
jest.runOnlyPendingTimers();
|
|
180
|
+
|
|
181
|
+
// Act
|
|
182
|
+
// Flow doesn't like jest mocks
|
|
183
|
+
// $FlowFixMe[prop-missing]
|
|
184
|
+
const result = TooltipBubble.mock.instances[0].props["children"];
|
|
185
|
+
|
|
186
|
+
// Assert
|
|
187
|
+
expect(result.props["title"]).toBe("Content title");
|
|
188
|
+
expect(result.props["children"]).toMatchSnapshot(
|
|
189
|
+
`<Body>Some custom content</Body>`,
|
|
190
|
+
);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe("accessibility", () => {
|
|
195
|
+
test("no id, sets identifier of TooltipBubble with UniqueIDProvider", async () => {
|
|
196
|
+
// Arrange
|
|
197
|
+
const ref = await new Promise((resolve) => {
|
|
198
|
+
const nodes = (
|
|
199
|
+
<View>
|
|
200
|
+
<Tooltip content="Content">
|
|
201
|
+
<View ref={resolve}>Anchor</View>
|
|
202
|
+
</Tooltip>
|
|
203
|
+
</View>
|
|
204
|
+
);
|
|
205
|
+
mount(nodes);
|
|
206
|
+
});
|
|
207
|
+
const node = (ReactDOM.findDOMNode(ref): any);
|
|
208
|
+
node && node.dispatchEvent(new KeyboardEvent("focusin"));
|
|
209
|
+
jest.runOnlyPendingTimers();
|
|
210
|
+
|
|
211
|
+
// Act
|
|
212
|
+
// Flow doesn't like jest mocks
|
|
213
|
+
// $FlowFixMe[prop-missing]
|
|
214
|
+
const result = TooltipBubble.mock.instances[0].props["id"];
|
|
215
|
+
|
|
216
|
+
// Assert
|
|
217
|
+
expect(result).toBe(mockIDENTIFIER);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("custom id, sets identifier of TooltipBubble", async () => {
|
|
221
|
+
// Arrange
|
|
222
|
+
const tooltipID = "tooltip-1";
|
|
223
|
+
const ref = await new Promise((resolve) => {
|
|
224
|
+
const nodes = (
|
|
225
|
+
<View>
|
|
226
|
+
<Tooltip id={tooltipID} title="Title" content="Content">
|
|
227
|
+
<View ref={resolve}>Anchor</View>
|
|
228
|
+
</Tooltip>
|
|
229
|
+
</View>
|
|
230
|
+
);
|
|
231
|
+
mount(nodes);
|
|
232
|
+
});
|
|
233
|
+
const node = (ReactDOM.findDOMNode(ref): any);
|
|
234
|
+
node && node.dispatchEvent(new KeyboardEvent("focusin"));
|
|
235
|
+
jest.runOnlyPendingTimers();
|
|
236
|
+
|
|
237
|
+
// Act
|
|
238
|
+
// Flow doesn't like jest mocks
|
|
239
|
+
// $FlowFixMe[prop-missing]
|
|
240
|
+
const result = TooltipBubble.mock.instances[0].props["id"];
|
|
241
|
+
|
|
242
|
+
// Assert
|
|
243
|
+
expect(result).toBe(tooltipID);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
describe("text-only anchor", () => {
|
|
247
|
+
test("wraps with element", async () => {
|
|
248
|
+
// Arrange
|
|
249
|
+
const ref = await new Promise((resolve) => {
|
|
250
|
+
const nodes = (
|
|
251
|
+
<View>
|
|
252
|
+
<Tooltip ref={resolve} content="Content">
|
|
253
|
+
Anchor
|
|
254
|
+
</Tooltip>
|
|
255
|
+
</View>
|
|
256
|
+
);
|
|
257
|
+
mount(nodes);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// Act
|
|
261
|
+
const result = (ReactDOM.findDOMNode(ref): any);
|
|
262
|
+
|
|
263
|
+
// Assert
|
|
264
|
+
expect(result).toBeInstanceOf(HTMLSpanElement);
|
|
265
|
+
expect(result.innerHTML).toBe("Anchor");
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test("id provided, does not attach aria-describedby", async () => {
|
|
269
|
+
// Arrange
|
|
270
|
+
const tooltipID = "tooltip-2";
|
|
271
|
+
const ref = await new Promise((resolve) => {
|
|
272
|
+
const nodes = (
|
|
273
|
+
<View>
|
|
274
|
+
<Tooltip
|
|
275
|
+
ref={resolve}
|
|
276
|
+
id={tooltipID}
|
|
277
|
+
content="Content"
|
|
278
|
+
>
|
|
279
|
+
Anchor
|
|
280
|
+
</Tooltip>
|
|
281
|
+
</View>
|
|
282
|
+
);
|
|
283
|
+
mount(nodes);
|
|
284
|
+
});
|
|
285
|
+
const node = (ReactDOM.findDOMNode(ref): any);
|
|
286
|
+
|
|
287
|
+
// Act
|
|
288
|
+
const result = node.getAttribute("aria-describedby");
|
|
289
|
+
|
|
290
|
+
// Assert
|
|
291
|
+
expect(result).toBeNull();
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test("no id provided, attaches aria-describedby", async () => {
|
|
295
|
+
// Arrange
|
|
296
|
+
const ref = await new Promise((resolve) => {
|
|
297
|
+
const nodes = (
|
|
298
|
+
<View>
|
|
299
|
+
<Tooltip ref={resolve} content="Content">
|
|
300
|
+
Anchor
|
|
301
|
+
</Tooltip>
|
|
302
|
+
</View>
|
|
303
|
+
);
|
|
304
|
+
mount(nodes);
|
|
305
|
+
});
|
|
306
|
+
const node = (ReactDOM.findDOMNode(ref): any);
|
|
307
|
+
|
|
308
|
+
// Act
|
|
309
|
+
const result = node.getAttribute("aria-describedby");
|
|
310
|
+
|
|
311
|
+
// Assert
|
|
312
|
+
expect(result).toBe(mockIDENTIFIER);
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
describe("element anchor", () => {
|
|
317
|
+
test("does not wrap", async () => {
|
|
318
|
+
// Arrange
|
|
319
|
+
const anchor = (
|
|
320
|
+
<View>
|
|
321
|
+
<View>Anchor</View>
|
|
322
|
+
</View>
|
|
323
|
+
);
|
|
324
|
+
const ref = await new Promise((resolve) => {
|
|
325
|
+
const nodes = (
|
|
326
|
+
<View>
|
|
327
|
+
<Tooltip ref={resolve} content="Content">
|
|
328
|
+
{anchor}
|
|
329
|
+
</Tooltip>
|
|
330
|
+
</View>
|
|
331
|
+
);
|
|
332
|
+
mount(nodes);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// Act
|
|
336
|
+
const result = (ReactDOM.findDOMNode(ref): any);
|
|
337
|
+
|
|
338
|
+
// Assert
|
|
339
|
+
expect(result).toBeInstanceOf(HTMLDivElement);
|
|
340
|
+
expect(result.innerHTML).not.toBe("Anchor");
|
|
341
|
+
expect(result.children[0].innerHTML).toBe("Anchor");
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
test("id provided, does not attach aria-describedby", async () => {
|
|
345
|
+
// Arrange
|
|
346
|
+
const anchor = (
|
|
347
|
+
<View>
|
|
348
|
+
<View>Anchor</View>
|
|
349
|
+
</View>
|
|
350
|
+
);
|
|
351
|
+
const ref = await new Promise((resolve) => {
|
|
352
|
+
const nodes = (
|
|
353
|
+
<View>
|
|
354
|
+
<Tooltip
|
|
355
|
+
id="tooltip-3"
|
|
356
|
+
ref={resolve}
|
|
357
|
+
content="Content"
|
|
358
|
+
>
|
|
359
|
+
{anchor}
|
|
360
|
+
</Tooltip>
|
|
361
|
+
</View>
|
|
362
|
+
);
|
|
363
|
+
mount(nodes);
|
|
364
|
+
});
|
|
365
|
+
const node = (ReactDOM.findDOMNode(ref): any);
|
|
366
|
+
|
|
367
|
+
// Act
|
|
368
|
+
const result = node.getAttribute("aria-describedby");
|
|
369
|
+
|
|
370
|
+
// Assert
|
|
371
|
+
expect(result).toBeNull();
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
test("no id provided, attaches aria-describedby", async () => {
|
|
375
|
+
// Arrange
|
|
376
|
+
const anchor = (
|
|
377
|
+
<View>
|
|
378
|
+
<View>Anchor</View>
|
|
379
|
+
</View>
|
|
380
|
+
);
|
|
381
|
+
const ref = await new Promise((resolve) => {
|
|
382
|
+
const nodes = (
|
|
383
|
+
<View>
|
|
384
|
+
<Tooltip ref={resolve} content="Content">
|
|
385
|
+
{anchor}
|
|
386
|
+
</Tooltip>
|
|
387
|
+
</View>
|
|
388
|
+
);
|
|
389
|
+
mount(nodes);
|
|
390
|
+
});
|
|
391
|
+
const node = (ReactDOM.findDOMNode(ref): any);
|
|
392
|
+
|
|
393
|
+
// Act
|
|
394
|
+
const result = node.getAttribute("aria-describedby");
|
|
395
|
+
|
|
396
|
+
// Assert
|
|
397
|
+
expect(result).toBe(mockIDENTIFIER);
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
});
|