@khanacademy/wonder-blocks-dropdown 2.3.19
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 +3403 -0
- package/dist/index.js +3966 -0
- package/dist/index.js.flow +2 -0
- package/docs.md +12 -0
- package/package.json +44 -0
- package/src/__tests__/__snapshots__/generated-snapshot.test.js.snap +4054 -0
- package/src/__tests__/generated-snapshot.test.js +1612 -0
- package/src/__tests__/index.test.js +23 -0
- package/src/components/__mocks__/dropdown-core-virtualized.js +40 -0
- package/src/components/__tests__/__snapshots__/action-item.test.js.snap +63 -0
- package/src/components/__tests__/action-item.test.js +43 -0
- package/src/components/__tests__/action-menu.test.js +544 -0
- package/src/components/__tests__/dropdown-core-virtualized.test.js +119 -0
- package/src/components/__tests__/dropdown-core.test.js +659 -0
- package/src/components/__tests__/multi-select.test.js +982 -0
- package/src/components/__tests__/search-text-input.test.js +144 -0
- package/src/components/__tests__/single-select.test.js +588 -0
- package/src/components/action-item.js +270 -0
- package/src/components/action-menu-opener-core.js +203 -0
- package/src/components/action-menu.js +300 -0
- package/src/components/action-menu.md +338 -0
- package/src/components/check.js +59 -0
- package/src/components/checkbox.js +111 -0
- package/src/components/dropdown-core-virtualized-item.js +62 -0
- package/src/components/dropdown-core-virtualized.js +246 -0
- package/src/components/dropdown-core.js +770 -0
- package/src/components/dropdown-opener.js +101 -0
- package/src/components/multi-select.js +597 -0
- package/src/components/multi-select.md +718 -0
- package/src/components/multi-select.stories.js +111 -0
- package/src/components/option-item.js +239 -0
- package/src/components/search-text-input.js +227 -0
- package/src/components/select-opener.js +297 -0
- package/src/components/separator-item.js +50 -0
- package/src/components/single-select.js +418 -0
- package/src/components/single-select.md +520 -0
- package/src/components/single-select.stories.js +107 -0
- package/src/index.js +20 -0
- package/src/util/constants.js +50 -0
- package/src/util/types.js +32 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
//@flow
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import {VariableSizeList as List} from "react-window";
|
|
4
|
+
import {mount} from "enzyme";
|
|
5
|
+
|
|
6
|
+
import OptionItem from "../option-item.js";
|
|
7
|
+
import SeparatorItem from "../separator-item.js";
|
|
8
|
+
import DropdownCoreVirtualized from "../dropdown-core-virtualized.js";
|
|
9
|
+
import SearchTextInput from "../search-text-input.js";
|
|
10
|
+
|
|
11
|
+
describe("DropdownCoreVirtualized", () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
jest.useFakeTimers();
|
|
14
|
+
|
|
15
|
+
// Jest doesn't fake out the animation frame API, so we're going to do
|
|
16
|
+
// it here and map it to timeouts, that way we can use the fake timer
|
|
17
|
+
// API to test our animation frame things.
|
|
18
|
+
jest.spyOn(global, "requestAnimationFrame").mockImplementation((fn) =>
|
|
19
|
+
setTimeout(fn, 0),
|
|
20
|
+
);
|
|
21
|
+
jest.spyOn(global, "cancelAnimationFrame").mockImplementation((id) =>
|
|
22
|
+
clearTimeout(id),
|
|
23
|
+
);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
jest.restoreAllMocks();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("should sort the items on first load", () => {
|
|
31
|
+
// Arrange
|
|
32
|
+
const optionItems = ["a", "bb", "ccc"].map((item, i) => ({
|
|
33
|
+
component: <OptionItem key={i} value={item} label={item} />,
|
|
34
|
+
focusable: true,
|
|
35
|
+
onClick: jest.fn(),
|
|
36
|
+
role: "option",
|
|
37
|
+
populatedProps: {
|
|
38
|
+
selected: false,
|
|
39
|
+
variant: "checkbox",
|
|
40
|
+
},
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
const initialItems = [
|
|
44
|
+
{
|
|
45
|
+
component: (
|
|
46
|
+
<SearchTextInput onChange={jest.fn()} searchText="" />
|
|
47
|
+
),
|
|
48
|
+
focusable: true,
|
|
49
|
+
populatedProps: {},
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
component: <SeparatorItem />,
|
|
53
|
+
focusable: false,
|
|
54
|
+
populatedProps: {},
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
const wrapper = mount(
|
|
59
|
+
<DropdownCoreVirtualized
|
|
60
|
+
data={[...initialItems, ...optionItems]}
|
|
61
|
+
/>,
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
jest.runAllTimers();
|
|
65
|
+
|
|
66
|
+
// Act
|
|
67
|
+
const firstLabel = wrapper.find(OptionItem).first().text();
|
|
68
|
+
|
|
69
|
+
// Assert
|
|
70
|
+
// make sure we are rendering the longest item first
|
|
71
|
+
expect(firstLabel).toEqual("ccc");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("should render a virtualized list", () => {
|
|
75
|
+
// Arrange
|
|
76
|
+
const optionItems = new Array(10).fill(null).map((item, i) => ({
|
|
77
|
+
component: (
|
|
78
|
+
<OptionItem
|
|
79
|
+
key={i}
|
|
80
|
+
value={(i + 1).toString()}
|
|
81
|
+
label={`School ${i + 1} in Wizarding World`}
|
|
82
|
+
/>
|
|
83
|
+
),
|
|
84
|
+
focusable: true,
|
|
85
|
+
onClick: jest.fn(),
|
|
86
|
+
role: "option",
|
|
87
|
+
populatedProps: {
|
|
88
|
+
selected: false,
|
|
89
|
+
variant: "checkbox",
|
|
90
|
+
},
|
|
91
|
+
}));
|
|
92
|
+
|
|
93
|
+
const initialItems = [
|
|
94
|
+
{
|
|
95
|
+
component: (
|
|
96
|
+
<SearchTextInput onChange={jest.fn()} searchText="" />
|
|
97
|
+
),
|
|
98
|
+
focusable: true,
|
|
99
|
+
populatedProps: {},
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
component: <SeparatorItem />,
|
|
103
|
+
focusable: false,
|
|
104
|
+
populatedProps: {},
|
|
105
|
+
},
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
const wrapper = mount(
|
|
109
|
+
<DropdownCoreVirtualized data={initialItems} width={300} />,
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// Act
|
|
113
|
+
// append items to update container height
|
|
114
|
+
wrapper.setProps({data: [...initialItems, ...optionItems]});
|
|
115
|
+
|
|
116
|
+
// Assert
|
|
117
|
+
expect(wrapper.find(List)).toExist();
|
|
118
|
+
});
|
|
119
|
+
});
|
|
@@ -0,0 +1,659 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import * as ReactDOM from "react-dom";
|
|
4
|
+
import {mount} from "enzyme";
|
|
5
|
+
|
|
6
|
+
import OptionItem from "../option-item.js";
|
|
7
|
+
import SearchTextInput from "../search-text-input.js";
|
|
8
|
+
import DropdownCore from "../dropdown-core.js";
|
|
9
|
+
import {keyCodes} from "../../util/constants.js";
|
|
10
|
+
|
|
11
|
+
jest.mock("../dropdown-core-virtualized.js");
|
|
12
|
+
|
|
13
|
+
const elementAtIndex = (wrapper, index) =>
|
|
14
|
+
wrapper.find(`[data-test-id="item-${index}"]`).first().getDOMNode();
|
|
15
|
+
|
|
16
|
+
describe("DropdownCore", () => {
|
|
17
|
+
window.getComputedStyle = jest.fn();
|
|
18
|
+
|
|
19
|
+
let dropdown;
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
jest.useFakeTimers();
|
|
23
|
+
|
|
24
|
+
// Jest doesn't fake out the animation frame API, so we're going to do
|
|
25
|
+
// it here and map it to timeouts, that way we can use the fake timer
|
|
26
|
+
// API to test our animation frame things.
|
|
27
|
+
jest.spyOn(global, "requestAnimationFrame").mockImplementation((fn) =>
|
|
28
|
+
setTimeout(fn, 0),
|
|
29
|
+
);
|
|
30
|
+
jest.spyOn(global, "cancelAnimationFrame").mockImplementation((id) =>
|
|
31
|
+
clearTimeout(id),
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const dummyOpener = <button />;
|
|
35
|
+
const openChanged = jest.fn();
|
|
36
|
+
dropdown = mount(
|
|
37
|
+
<DropdownCore
|
|
38
|
+
initialFocusedIndex={0}
|
|
39
|
+
// mock the items
|
|
40
|
+
items={[
|
|
41
|
+
{
|
|
42
|
+
component: (
|
|
43
|
+
<OptionItem
|
|
44
|
+
testId="item-0"
|
|
45
|
+
label="item 0"
|
|
46
|
+
value="0"
|
|
47
|
+
key="0"
|
|
48
|
+
/>
|
|
49
|
+
),
|
|
50
|
+
focusable: true,
|
|
51
|
+
populatedProps: {},
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
component: (
|
|
55
|
+
<OptionItem
|
|
56
|
+
testId="item-1"
|
|
57
|
+
label="item 1"
|
|
58
|
+
value="1"
|
|
59
|
+
key="1"
|
|
60
|
+
/>
|
|
61
|
+
),
|
|
62
|
+
focusable: true,
|
|
63
|
+
populatedProps: {},
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
component: (
|
|
67
|
+
<OptionItem
|
|
68
|
+
testId="item-2"
|
|
69
|
+
label="item 2"
|
|
70
|
+
value="2"
|
|
71
|
+
key="2"
|
|
72
|
+
/>
|
|
73
|
+
),
|
|
74
|
+
focusable: true,
|
|
75
|
+
populatedProps: {},
|
|
76
|
+
},
|
|
77
|
+
]}
|
|
78
|
+
role="listbox"
|
|
79
|
+
light={false}
|
|
80
|
+
open={false}
|
|
81
|
+
// mock the opener elements
|
|
82
|
+
opener={dummyOpener}
|
|
83
|
+
openerElement={null}
|
|
84
|
+
onOpenChanged={openChanged}
|
|
85
|
+
/>,
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
afterEach(() => {
|
|
90
|
+
jest.restoreAllMocks();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("handles basic keyboard navigation as expected", () => {
|
|
94
|
+
const handleOpen = jest.fn();
|
|
95
|
+
dropdown.setProps({
|
|
96
|
+
initialFocusedIndex: 0,
|
|
97
|
+
onOpenChanged: (open) => handleOpen(open),
|
|
98
|
+
open: true,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
jest.runAllTimers();
|
|
102
|
+
expect(document.activeElement).toBe(elementAtIndex(dropdown, 0));
|
|
103
|
+
|
|
104
|
+
// navigate down three times
|
|
105
|
+
dropdown.simulate("keydown", {keyCode: keyCodes.down});
|
|
106
|
+
dropdown.simulate("keyup", {keyCode: keyCodes.down});
|
|
107
|
+
jest.runAllTimers();
|
|
108
|
+
expect(document.activeElement).toBe(elementAtIndex(dropdown, 1));
|
|
109
|
+
|
|
110
|
+
dropdown.simulate("keydown", {keyCode: keyCodes.down});
|
|
111
|
+
dropdown.simulate("keyup", {keyCode: keyCodes.down});
|
|
112
|
+
jest.runAllTimers();
|
|
113
|
+
expect(document.activeElement).toBe(elementAtIndex(dropdown, 2));
|
|
114
|
+
|
|
115
|
+
dropdown.simulate("keydown", {keyCode: keyCodes.down});
|
|
116
|
+
dropdown.simulate("keyup", {keyCode: keyCodes.down});
|
|
117
|
+
jest.runAllTimers();
|
|
118
|
+
expect(document.activeElement).toBe(elementAtIndex(dropdown, 0));
|
|
119
|
+
|
|
120
|
+
// navigate up back two times
|
|
121
|
+
// to last item
|
|
122
|
+
dropdown.simulate("keydown", {keyCode: keyCodes.up});
|
|
123
|
+
dropdown.simulate("keyup", {keyCode: keyCodes.up});
|
|
124
|
+
jest.runAllTimers();
|
|
125
|
+
expect(document.activeElement).toBe(elementAtIndex(dropdown, 2));
|
|
126
|
+
// to the previous one
|
|
127
|
+
dropdown.simulate("keydown", {keyCode: keyCodes.up});
|
|
128
|
+
dropdown.simulate("keyup", {keyCode: keyCodes.up});
|
|
129
|
+
jest.runAllTimers();
|
|
130
|
+
expect(document.activeElement).toBe(elementAtIndex(dropdown, 1));
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("doesn't close on touch interaction with option", () => {
|
|
134
|
+
const handleOpen = jest.fn();
|
|
135
|
+
dropdown.setProps({
|
|
136
|
+
onOpenChanged: (open) => handleOpen(open),
|
|
137
|
+
open: true,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const option0 = dropdown.find("OptionItem").at(0);
|
|
141
|
+
// This is the full order of events fired when tapping an element
|
|
142
|
+
// on mobile
|
|
143
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/Touch_events/Supporting_both_TouchEvent_and_MouseEvent#Event_order
|
|
144
|
+
option0.simulate("touchstart");
|
|
145
|
+
option0.simulate("touchend");
|
|
146
|
+
option0.simulate("mousemove");
|
|
147
|
+
option0.simulate("mousedown");
|
|
148
|
+
option0.simulate("mouseup");
|
|
149
|
+
option0.simulate("click");
|
|
150
|
+
|
|
151
|
+
expect(handleOpen).toHaveBeenCalledTimes(0);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("closes on tab and escape as expected", () => {
|
|
155
|
+
const handleOpen = jest.fn();
|
|
156
|
+
dropdown.setProps({
|
|
157
|
+
onOpenChanged: (open) => handleOpen(open),
|
|
158
|
+
open: true,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// "close" some menus
|
|
162
|
+
dropdown.simulate("keydown", {keyCode: keyCodes.tab});
|
|
163
|
+
dropdown.simulate("keyup", {keyCode: keyCodes.tab});
|
|
164
|
+
dropdown.simulate("keydown", {keyCode: keyCodes.escape});
|
|
165
|
+
dropdown.simulate("keyup", {keyCode: keyCodes.escape});
|
|
166
|
+
expect(handleOpen).toHaveBeenCalledTimes(2);
|
|
167
|
+
// Test that we pass "false" to handleOpenChanged both times
|
|
168
|
+
expect(handleOpen.mock.calls[0][0]).toBe(false);
|
|
169
|
+
expect(handleOpen.mock.calls[1][0]).toBe(false);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("closes on external mouse click", () => {
|
|
173
|
+
const handleOpen = jest.fn();
|
|
174
|
+
dropdown.setProps({
|
|
175
|
+
onOpenChanged: (open) => handleOpen(open),
|
|
176
|
+
open: true,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const event = new MouseEvent("mouseup");
|
|
180
|
+
document.dispatchEvent(event);
|
|
181
|
+
|
|
182
|
+
expect(handleOpen).toHaveBeenCalledTimes(1);
|
|
183
|
+
expect(handleOpen.mock.calls[0][0]).toBe(false);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("closes on external mouse click on an element inside document.body", () => {
|
|
187
|
+
const handleOpen = jest.fn();
|
|
188
|
+
const container = document.createElement("container");
|
|
189
|
+
if (!document.body) {
|
|
190
|
+
throw new Error("No document.body");
|
|
191
|
+
}
|
|
192
|
+
document.body.appendChild(container);
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* According to https://stackoverflow.com/questions/36803733/jsdom-dispatchevent-addeventlistener-doesnt-seem-to-work
|
|
196
|
+
* Enzyme uses renderIntoDocument from React.TestUtils which doesn't actually
|
|
197
|
+
* render the component into document.body so testing behavior that relies
|
|
198
|
+
* on bubbling won't work. This test works around this limitation by using
|
|
199
|
+
* ReactDOM.render() to render our test component into a container that lives
|
|
200
|
+
* in document.body.
|
|
201
|
+
*/
|
|
202
|
+
|
|
203
|
+
ReactDOM.render(
|
|
204
|
+
<div>
|
|
205
|
+
<h1 id="foo">DropdownCore test</h1>
|
|
206
|
+
<DropdownCore
|
|
207
|
+
initialFocusedIndex={0}
|
|
208
|
+
// mock the items
|
|
209
|
+
items={[
|
|
210
|
+
{
|
|
211
|
+
component: (
|
|
212
|
+
<OptionItem label="item 0" value="0" key="0" />
|
|
213
|
+
),
|
|
214
|
+
focusable: true,
|
|
215
|
+
populatedProps: {},
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
component: (
|
|
219
|
+
<OptionItem label="item 1" value="1" key="1" />
|
|
220
|
+
),
|
|
221
|
+
focusable: true,
|
|
222
|
+
populatedProps: {},
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
component: (
|
|
226
|
+
<OptionItem label="item 2" value="2" key="2" />
|
|
227
|
+
),
|
|
228
|
+
focusable: true,
|
|
229
|
+
populatedProps: {},
|
|
230
|
+
},
|
|
231
|
+
]}
|
|
232
|
+
role="listbox"
|
|
233
|
+
light={false}
|
|
234
|
+
open={true}
|
|
235
|
+
// mock the opener elements
|
|
236
|
+
opener={<button />}
|
|
237
|
+
openerElement={null}
|
|
238
|
+
onOpenChanged={(open) => handleOpen(open)}
|
|
239
|
+
/>
|
|
240
|
+
</div>,
|
|
241
|
+
container,
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
const title = document.querySelector("#foo");
|
|
245
|
+
if (!title) {
|
|
246
|
+
throw new Error("Couldn't find title");
|
|
247
|
+
}
|
|
248
|
+
const event = new MouseEvent("mouseup", {bubbles: true});
|
|
249
|
+
title.dispatchEvent(event);
|
|
250
|
+
|
|
251
|
+
expect(handleOpen).toHaveBeenCalledTimes(1);
|
|
252
|
+
expect(handleOpen.mock.calls[0][0]).toBe(false);
|
|
253
|
+
|
|
254
|
+
// cleanup
|
|
255
|
+
ReactDOM.unmountComponentAtNode(container);
|
|
256
|
+
if (!document.body) {
|
|
257
|
+
throw new Error("No document.body");
|
|
258
|
+
}
|
|
259
|
+
document.body.removeChild(container);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("doesn't close on external mouse click if already closed", () => {
|
|
263
|
+
const handleOpen = jest.fn();
|
|
264
|
+
dropdown.setProps({
|
|
265
|
+
onOpenChanged: (open) => handleOpen(open),
|
|
266
|
+
open: false,
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const event = new MouseEvent("mouseup");
|
|
270
|
+
document.dispatchEvent(event);
|
|
271
|
+
|
|
272
|
+
expect(handleOpen).toHaveBeenCalledTimes(0);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("opens on down key as expected", () => {
|
|
276
|
+
const handleOpen = jest.fn();
|
|
277
|
+
dropdown.setProps({
|
|
278
|
+
onOpenChanged: (open) => handleOpen(open),
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
dropdown.simulate("keydown", {keyCode: keyCodes.down});
|
|
282
|
+
dropdown.simulate("keyup", {keyCode: keyCodes.down});
|
|
283
|
+
|
|
284
|
+
expect(handleOpen).toHaveBeenCalledTimes(1);
|
|
285
|
+
expect(handleOpen.mock.calls[0][0]).toBe(true);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it("selects correct item when starting off at a different index", () => {
|
|
289
|
+
const handleOpen = jest.fn();
|
|
290
|
+
dropdown.setProps({
|
|
291
|
+
initialFocusedIndex: 2,
|
|
292
|
+
onOpenChanged: (open) => handleOpen(open),
|
|
293
|
+
open: true,
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
jest.runAllTimers();
|
|
297
|
+
expect(document.activeElement).toBe(elementAtIndex(dropdown, 2));
|
|
298
|
+
|
|
299
|
+
// navigate down
|
|
300
|
+
dropdown.simulate("keydown", {keyCode: keyCodes.down});
|
|
301
|
+
dropdown.simulate("keyup", {keyCode: keyCodes.down});
|
|
302
|
+
jest.runAllTimers();
|
|
303
|
+
expect(document.activeElement).toBe(elementAtIndex(dropdown, 0));
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("focuses correct item with clicking/pressing with initial focused of not 0", () => {
|
|
307
|
+
// Same as the previous test, expect initialFocusedIndex is 2 now
|
|
308
|
+
dropdown.setProps({
|
|
309
|
+
initialFocusedIndex: 2,
|
|
310
|
+
open: true,
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
const option1 = dropdown.find("OptionItem").at(1);
|
|
314
|
+
|
|
315
|
+
// Click on item at index 1
|
|
316
|
+
option1.simulate("click");
|
|
317
|
+
|
|
318
|
+
// should move to next item
|
|
319
|
+
dropdown.simulate("keydown", {keyCode: keyCodes.down});
|
|
320
|
+
dropdown.simulate("keyup", {keyCode: keyCodes.down});
|
|
321
|
+
jest.runAllTimers();
|
|
322
|
+
expect(document.activeElement).toBe(elementAtIndex(dropdown, 2));
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it("focuses correct item with a disabled item", () => {
|
|
326
|
+
dropdown.setProps({
|
|
327
|
+
initialFocusedIndex: 0,
|
|
328
|
+
items: [
|
|
329
|
+
{
|
|
330
|
+
component: (
|
|
331
|
+
<OptionItem
|
|
332
|
+
testId="item-0"
|
|
333
|
+
label="item 0"
|
|
334
|
+
value="0"
|
|
335
|
+
key="0"
|
|
336
|
+
/>
|
|
337
|
+
),
|
|
338
|
+
focusable: true,
|
|
339
|
+
populatedProps: {},
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
component: (
|
|
343
|
+
<OptionItem
|
|
344
|
+
testId="item-1"
|
|
345
|
+
label="item 1"
|
|
346
|
+
value="1"
|
|
347
|
+
key="1"
|
|
348
|
+
/>
|
|
349
|
+
),
|
|
350
|
+
focusable: false,
|
|
351
|
+
populatedProps: {},
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
component: (
|
|
355
|
+
<OptionItem
|
|
356
|
+
testId="item-2"
|
|
357
|
+
label="item 2"
|
|
358
|
+
value="2"
|
|
359
|
+
key="2"
|
|
360
|
+
/>
|
|
361
|
+
),
|
|
362
|
+
focusable: true,
|
|
363
|
+
populatedProps: {},
|
|
364
|
+
},
|
|
365
|
+
],
|
|
366
|
+
open: true,
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
jest.runAllTimers();
|
|
370
|
+
expect(document.activeElement).toBe(elementAtIndex(dropdown, 0));
|
|
371
|
+
|
|
372
|
+
// Should select option2
|
|
373
|
+
dropdown.simulate("keydown", {keyCode: keyCodes.down});
|
|
374
|
+
dropdown.simulate("keyup", {keyCode: keyCodes.down});
|
|
375
|
+
jest.runAllTimers();
|
|
376
|
+
expect(document.activeElement).toBe(elementAtIndex(dropdown, 2));
|
|
377
|
+
|
|
378
|
+
// Should select option0
|
|
379
|
+
dropdown.simulate("keydown", {keyCode: keyCodes.down});
|
|
380
|
+
dropdown.simulate("keyup", {keyCode: keyCodes.down});
|
|
381
|
+
jest.runAllTimers();
|
|
382
|
+
expect(document.activeElement).toBe(elementAtIndex(dropdown, 0));
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it("focuses correct item after different items become focusable", () => {
|
|
386
|
+
dropdown.setProps({
|
|
387
|
+
initialFocusedIndex: 0,
|
|
388
|
+
open: true,
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
jest.runAllTimers();
|
|
392
|
+
expect(document.activeElement).toBe(elementAtIndex(dropdown, 0));
|
|
393
|
+
|
|
394
|
+
// Should select option1
|
|
395
|
+
dropdown.simulate("keydown", {keyCode: keyCodes.down});
|
|
396
|
+
dropdown.simulate("keyup", {keyCode: keyCodes.down});
|
|
397
|
+
jest.runAllTimers();
|
|
398
|
+
expect(document.activeElement).toBe(elementAtIndex(dropdown, 1));
|
|
399
|
+
|
|
400
|
+
dropdown.setProps({
|
|
401
|
+
items: [
|
|
402
|
+
{
|
|
403
|
+
component: (
|
|
404
|
+
<OptionItem
|
|
405
|
+
testId="item-0"
|
|
406
|
+
label="item 0"
|
|
407
|
+
value="0"
|
|
408
|
+
key="0"
|
|
409
|
+
/>
|
|
410
|
+
),
|
|
411
|
+
focusable: false,
|
|
412
|
+
populatedProps: {},
|
|
413
|
+
},
|
|
414
|
+
{
|
|
415
|
+
component: (
|
|
416
|
+
<OptionItem
|
|
417
|
+
testId="item-1"
|
|
418
|
+
label="item 1"
|
|
419
|
+
value="1"
|
|
420
|
+
key="1"
|
|
421
|
+
/>
|
|
422
|
+
),
|
|
423
|
+
focusable: true,
|
|
424
|
+
populatedProps: {},
|
|
425
|
+
},
|
|
426
|
+
{
|
|
427
|
+
component: (
|
|
428
|
+
<OptionItem
|
|
429
|
+
testId="item-2"
|
|
430
|
+
label="item 2"
|
|
431
|
+
value="2"
|
|
432
|
+
key="2"
|
|
433
|
+
/>
|
|
434
|
+
),
|
|
435
|
+
focusable: true,
|
|
436
|
+
populatedProps: {},
|
|
437
|
+
},
|
|
438
|
+
],
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
// Should figure out that option1, which is now at index 0, is selected
|
|
442
|
+
jest.runAllTimers();
|
|
443
|
+
expect(document.activeElement).toBe(elementAtIndex(dropdown, 1));
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it("focuses first item after currently focused item is no longer focusable", () => {
|
|
447
|
+
dropdown.setProps({
|
|
448
|
+
initialFocusedIndex: 0,
|
|
449
|
+
open: true,
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
jest.runAllTimers();
|
|
453
|
+
expect(document.activeElement).toBe(elementAtIndex(dropdown, 0));
|
|
454
|
+
|
|
455
|
+
const option0 = dropdown.find("OptionItem").at(0);
|
|
456
|
+
option0.simulate("click");
|
|
457
|
+
jest.runAllTimers();
|
|
458
|
+
expect(document.activeElement).toBe(elementAtIndex(dropdown, 0));
|
|
459
|
+
|
|
460
|
+
dropdown.setProps({
|
|
461
|
+
items: [
|
|
462
|
+
{
|
|
463
|
+
component: (
|
|
464
|
+
<OptionItem
|
|
465
|
+
testId="item-0"
|
|
466
|
+
label="item 0"
|
|
467
|
+
value="0"
|
|
468
|
+
key="0"
|
|
469
|
+
/>
|
|
470
|
+
),
|
|
471
|
+
focusable: false,
|
|
472
|
+
populatedProps: {},
|
|
473
|
+
},
|
|
474
|
+
{
|
|
475
|
+
component: (
|
|
476
|
+
<OptionItem
|
|
477
|
+
testId="item-1"
|
|
478
|
+
label="item 1"
|
|
479
|
+
value="1"
|
|
480
|
+
key="1"
|
|
481
|
+
/>
|
|
482
|
+
),
|
|
483
|
+
focusable: true,
|
|
484
|
+
populatedProps: {},
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
component: (
|
|
488
|
+
<OptionItem
|
|
489
|
+
testId="item-2"
|
|
490
|
+
label="item 2"
|
|
491
|
+
value="2"
|
|
492
|
+
key="2"
|
|
493
|
+
/>
|
|
494
|
+
),
|
|
495
|
+
focusable: true,
|
|
496
|
+
populatedProps: {},
|
|
497
|
+
},
|
|
498
|
+
],
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
// Should figure out that option1 is now selected
|
|
502
|
+
jest.runAllTimers();
|
|
503
|
+
expect(document.activeElement).toBe(elementAtIndex(dropdown, 1));
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
it("calls correct onclick for an option item", () => {
|
|
507
|
+
const onClick0 = jest.fn();
|
|
508
|
+
const onClick1 = jest.fn();
|
|
509
|
+
dropdown.setProps({
|
|
510
|
+
items: [
|
|
511
|
+
{
|
|
512
|
+
component: (
|
|
513
|
+
<OptionItem
|
|
514
|
+
label="item 0"
|
|
515
|
+
value="0"
|
|
516
|
+
key="0"
|
|
517
|
+
onClick={onClick0}
|
|
518
|
+
/>
|
|
519
|
+
),
|
|
520
|
+
focusable: true,
|
|
521
|
+
populatedProps: {},
|
|
522
|
+
},
|
|
523
|
+
{
|
|
524
|
+
component: (
|
|
525
|
+
<OptionItem
|
|
526
|
+
label="item 1"
|
|
527
|
+
value="1"
|
|
528
|
+
key="1"
|
|
529
|
+
onClick={onClick1}
|
|
530
|
+
/>
|
|
531
|
+
),
|
|
532
|
+
focusable: true,
|
|
533
|
+
populatedProps: {},
|
|
534
|
+
},
|
|
535
|
+
{
|
|
536
|
+
component: <OptionItem label="item 2" value="2" key="2" />,
|
|
537
|
+
focusable: true,
|
|
538
|
+
populatedProps: {},
|
|
539
|
+
},
|
|
540
|
+
],
|
|
541
|
+
open: true,
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
const option1 = dropdown.find("OptionItem").at(1);
|
|
545
|
+
|
|
546
|
+
option1.simulate("click");
|
|
547
|
+
expect(onClick1).toHaveBeenCalledTimes(1);
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
it("Displays no results when no items are left with filter", () => {
|
|
551
|
+
// Arrange
|
|
552
|
+
const handleSearchTextChanged = jest.fn();
|
|
553
|
+
|
|
554
|
+
// Act
|
|
555
|
+
dropdown.setProps({
|
|
556
|
+
onSearchTextChanged: (text) => handleSearchTextChanged(text),
|
|
557
|
+
searchText: "ab",
|
|
558
|
+
items: [
|
|
559
|
+
{
|
|
560
|
+
component: (
|
|
561
|
+
<SearchTextInput
|
|
562
|
+
key="search-text-input"
|
|
563
|
+
onChange={handleSearchTextChanged}
|
|
564
|
+
searchText={""}
|
|
565
|
+
/>
|
|
566
|
+
),
|
|
567
|
+
focusable: true,
|
|
568
|
+
populatedProps: {},
|
|
569
|
+
},
|
|
570
|
+
],
|
|
571
|
+
open: true,
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
// Assert
|
|
575
|
+
expect(dropdown.find("InnerPopper").text()).toContain("No results");
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
it("When SearchTextInput has input and focused, tab key should not close the select", () => {
|
|
579
|
+
// Arrange
|
|
580
|
+
const handleSearchTextChanged = jest.fn();
|
|
581
|
+
const handleOpen = jest.fn();
|
|
582
|
+
|
|
583
|
+
dropdown.setProps({
|
|
584
|
+
onOpenChanged: (open) => handleOpen(open),
|
|
585
|
+
onSearchTextChanged: (text) => handleSearchTextChanged(text),
|
|
586
|
+
searchText: "ab",
|
|
587
|
+
open: true,
|
|
588
|
+
items: [
|
|
589
|
+
{
|
|
590
|
+
component: (
|
|
591
|
+
<SearchTextInput
|
|
592
|
+
testId="item-0"
|
|
593
|
+
key="search-text-input"
|
|
594
|
+
onChange={handleSearchTextChanged}
|
|
595
|
+
searchText={""}
|
|
596
|
+
/>
|
|
597
|
+
),
|
|
598
|
+
focusable: true,
|
|
599
|
+
populatedProps: {},
|
|
600
|
+
},
|
|
601
|
+
],
|
|
602
|
+
});
|
|
603
|
+
// SearchTextInput should be focused
|
|
604
|
+
const searchInput = dropdown.find(SearchTextInput);
|
|
605
|
+
jest.runAllTimers();
|
|
606
|
+
|
|
607
|
+
expect(searchInput.state("focused")).toBe(true);
|
|
608
|
+
|
|
609
|
+
// Act
|
|
610
|
+
dropdown.simulate("keydown", {keyCode: keyCodes.tab});
|
|
611
|
+
jest.runAllTimers();
|
|
612
|
+
|
|
613
|
+
// Assert
|
|
614
|
+
expect(handleOpen).toHaveBeenCalledTimes(0);
|
|
615
|
+
expect(document.activeElement).toBe(elementAtIndex(dropdown, 0));
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
it("When SearchTextInput exists and focused, space key pressing should be allowed", () => {
|
|
619
|
+
// Arrange
|
|
620
|
+
const handleSearchTextChanged = jest.fn();
|
|
621
|
+
const preventDefaultMock = jest.fn();
|
|
622
|
+
dropdown.setProps({
|
|
623
|
+
onSearchTextChanged: (text) => handleSearchTextChanged(text),
|
|
624
|
+
searchText: "",
|
|
625
|
+
items: [
|
|
626
|
+
{
|
|
627
|
+
component: (
|
|
628
|
+
<SearchTextInput
|
|
629
|
+
testId="item-0"
|
|
630
|
+
key="search-text-input"
|
|
631
|
+
onChange={handleSearchTextChanged}
|
|
632
|
+
searchText={""}
|
|
633
|
+
/>
|
|
634
|
+
),
|
|
635
|
+
focusable: true,
|
|
636
|
+
populatedProps: {},
|
|
637
|
+
},
|
|
638
|
+
],
|
|
639
|
+
open: true,
|
|
640
|
+
});
|
|
641
|
+
// SearchTextInput should be focused
|
|
642
|
+
const searchInput = dropdown.find(SearchTextInput).find("input");
|
|
643
|
+
jest.runAllTimers();
|
|
644
|
+
expect(document.activeElement).toBe(elementAtIndex(dropdown, 0));
|
|
645
|
+
|
|
646
|
+
// Act
|
|
647
|
+
searchInput.simulate("keydown", {
|
|
648
|
+
keyCode: keyCodes.space,
|
|
649
|
+
preventDefault: preventDefaultMock,
|
|
650
|
+
});
|
|
651
|
+
searchInput.simulate("keyup", {
|
|
652
|
+
keyCode: keyCodes.space,
|
|
653
|
+
preventDefault: preventDefaultMock,
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
// Assert
|
|
657
|
+
expect(preventDefaultMock).toHaveBeenCalledTimes(0);
|
|
658
|
+
});
|
|
659
|
+
});
|