@khanacademy/wonder-blocks-dropdown 2.7.4 → 2.8.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 +28 -0
- package/dist/es/index.js +167 -167
- package/dist/index.js +389 -360
- package/package.json +7 -7
- package/src/components/__docs__/action-menu.argtypes.js +44 -0
- package/src/components/__docs__/action-menu.stories.js +435 -0
- package/src/components/__docs__/base-select.argtypes.js +54 -0
- package/src/components/__docs__/multi-select.stories.js +509 -0
- package/src/components/__docs__/single-select.accessibility.stories.mdx +59 -0
- package/src/components/__docs__/single-select.argtypes.js +54 -0
- package/src/components/__docs__/single-select.stories.js +464 -0
- package/src/components/__tests__/dropdown-core-virtualized.test.js +0 -15
- package/src/components/__tests__/dropdown-core.test.js +113 -209
- package/src/components/__tests__/multi-select.test.js +49 -3
- package/src/components/__tests__/single-select.test.js +43 -50
- package/src/components/action-menu.js +11 -0
- package/src/components/dropdown-core-virtualized.js +0 -5
- package/src/components/dropdown-core.js +224 -130
- package/src/components/multi-select.js +18 -33
- package/src/components/single-select.js +16 -30
- package/src/util/__tests__/dropdown-menu-styles.test.js +0 -26
- package/src/util/__tests__/helpers.test.js +73 -0
- package/src/util/constants.js +0 -11
- package/src/util/dropdown-menu-styles.js +0 -5
- package/src/util/helpers.js +44 -0
- package/src/util/types.js +2 -5
- package/src/components/__tests__/search-text-input.test.js +0 -212
- package/src/components/action-menu.stories.js +0 -48
- package/src/components/multi-select.stories.js +0 -124
- package/src/components/search-text-input.js +0 -115
- package/src/components/single-select.stories.js +0 -247
|
@@ -99,6 +99,10 @@ describe("SingleSelect", () => {
|
|
|
99
99
|
});
|
|
100
100
|
|
|
101
101
|
describe("keyboard", () => {
|
|
102
|
+
beforeEach(() => {
|
|
103
|
+
jest.useFakeTimers();
|
|
104
|
+
});
|
|
105
|
+
|
|
102
106
|
describe.each([{key: "{enter}"}, {key: "{space}"}])(
|
|
103
107
|
"$key",
|
|
104
108
|
({key}) => {
|
|
@@ -129,7 +133,7 @@ describe("SingleSelect", () => {
|
|
|
129
133
|
},
|
|
130
134
|
);
|
|
131
135
|
|
|
132
|
-
it("should
|
|
136
|
+
it("should select an item when pressing {enter}", () => {
|
|
133
137
|
// Arrange
|
|
134
138
|
render(uncontrolledSingleSelect);
|
|
135
139
|
userEvent.tab();
|
|
@@ -139,8 +143,8 @@ describe("SingleSelect", () => {
|
|
|
139
143
|
userEvent.keyboard("{enter}");
|
|
140
144
|
|
|
141
145
|
// Assert
|
|
142
|
-
expect(onChange).
|
|
143
|
-
expect(screen.
|
|
146
|
+
expect(onChange).toHaveBeenCalledWith("1");
|
|
147
|
+
expect(screen.queryByRole("listbox")).not.toBeInTheDocument();
|
|
144
148
|
});
|
|
145
149
|
|
|
146
150
|
it("should select an item when pressing {space}", () => {
|
|
@@ -157,6 +161,27 @@ describe("SingleSelect", () => {
|
|
|
157
161
|
expect(screen.queryByRole("listbox")).not.toBeInTheDocument();
|
|
158
162
|
});
|
|
159
163
|
|
|
164
|
+
it("should find and select an item using the keyboard", () => {
|
|
165
|
+
// Arrange
|
|
166
|
+
render(
|
|
167
|
+
<SingleSelect onChange={onChange} placeholder="Choose">
|
|
168
|
+
<OptionItem label="apple" value="apple" />
|
|
169
|
+
<OptionItem label="orange" value="orange" />
|
|
170
|
+
<OptionItem label="pear" value="pear" />
|
|
171
|
+
</SingleSelect>,
|
|
172
|
+
);
|
|
173
|
+
userEvent.tab();
|
|
174
|
+
|
|
175
|
+
// Act
|
|
176
|
+
// find first occurrence
|
|
177
|
+
userEvent.keyboard("or");
|
|
178
|
+
jest.advanceTimersByTime(501);
|
|
179
|
+
|
|
180
|
+
// Assert
|
|
181
|
+
expect(onChange).toHaveBeenCalledWith("orange");
|
|
182
|
+
expect(screen.queryByRole("listbox")).not.toBeInTheDocument();
|
|
183
|
+
});
|
|
184
|
+
|
|
160
185
|
it("should dismiss the dropdown when pressing {escape}", () => {
|
|
161
186
|
// Arrange
|
|
162
187
|
render(uncontrolledSingleSelect);
|
|
@@ -214,7 +239,7 @@ describe("SingleSelect", () => {
|
|
|
214
239
|
render(<ControlledComponent onToggle={onToggleMock} />);
|
|
215
240
|
|
|
216
241
|
// Act
|
|
217
|
-
userEvent.click(screen.
|
|
242
|
+
userEvent.click(screen.getByRole("button", {name: "Choose"}));
|
|
218
243
|
|
|
219
244
|
// Assert
|
|
220
245
|
expect(onToggleMock).toHaveBeenCalledWith(true);
|
|
@@ -225,7 +250,7 @@ describe("SingleSelect", () => {
|
|
|
225
250
|
const onToggleMock = jest.fn();
|
|
226
251
|
render(<ControlledComponent onToggle={onToggleMock} />);
|
|
227
252
|
// open the menu from the outside
|
|
228
|
-
userEvent.click(screen.
|
|
253
|
+
userEvent.click(screen.getByRole("button", {name: "Choose"}));
|
|
229
254
|
|
|
230
255
|
// Act
|
|
231
256
|
// click on first item
|
|
@@ -264,10 +289,11 @@ describe("SingleSelect", () => {
|
|
|
264
289
|
render(<ControlledComponent />);
|
|
265
290
|
|
|
266
291
|
// Act
|
|
292
|
+
const opener = screen.getByRole("button", {name: "Choose"});
|
|
267
293
|
// open the menu from the outside
|
|
268
|
-
userEvent.click(
|
|
294
|
+
userEvent.click(opener);
|
|
269
295
|
// click on the dropdown anchor to hide the menu
|
|
270
|
-
userEvent.click(
|
|
296
|
+
userEvent.click(opener);
|
|
271
297
|
|
|
272
298
|
// Assert
|
|
273
299
|
expect(screen.queryByRole("listbox")).not.toBeInTheDocument();
|
|
@@ -360,12 +386,7 @@ describe("SingleSelect", () => {
|
|
|
360
386
|
testId="openTest"
|
|
361
387
|
onChange={jest.fn()}
|
|
362
388
|
opener={({text}) => (
|
|
363
|
-
<button
|
|
364
|
-
onClick={jest.fn()}
|
|
365
|
-
data-test-id="custom-opener"
|
|
366
|
-
>
|
|
367
|
-
{text}
|
|
368
|
-
</button>
|
|
389
|
+
<button onClick={jest.fn()}>{text}</button>
|
|
369
390
|
)}
|
|
370
391
|
>
|
|
371
392
|
<OptionItem label="Toggle A" value="toggle_a" />
|
|
@@ -374,7 +395,7 @@ describe("SingleSelect", () => {
|
|
|
374
395
|
);
|
|
375
396
|
|
|
376
397
|
// Act
|
|
377
|
-
const opener = screen.
|
|
398
|
+
const opener = screen.getByRole("button");
|
|
378
399
|
// open dropdown
|
|
379
400
|
userEvent.click(opener);
|
|
380
401
|
|
|
@@ -405,12 +426,7 @@ describe("SingleSelect", () => {
|
|
|
405
426
|
selectedValue={this.state.selectedValue}
|
|
406
427
|
placeholder="Custom placeholder"
|
|
407
428
|
opener={({text}) => (
|
|
408
|
-
<button
|
|
409
|
-
onClick={jest.fn()}
|
|
410
|
-
data-test-id="custom-opener"
|
|
411
|
-
>
|
|
412
|
-
{text}
|
|
413
|
-
</button>
|
|
429
|
+
<button onClick={jest.fn()}>{text}</button>
|
|
414
430
|
)}
|
|
415
431
|
>
|
|
416
432
|
<OptionItem label="Toggle A" value="toggle_a" />
|
|
@@ -424,7 +440,7 @@ describe("SingleSelect", () => {
|
|
|
424
440
|
render(<ControlledComponent />);
|
|
425
441
|
|
|
426
442
|
// Act
|
|
427
|
-
const opener = screen.
|
|
443
|
+
const opener = screen.getByRole("button");
|
|
428
444
|
// open dropdown
|
|
429
445
|
userEvent.click(opener);
|
|
430
446
|
userEvent.click(screen.getByText("Toggle B"));
|
|
@@ -437,7 +453,7 @@ describe("SingleSelect", () => {
|
|
|
437
453
|
});
|
|
438
454
|
|
|
439
455
|
describe("isFilterable", () => {
|
|
440
|
-
it("displays
|
|
456
|
+
it("displays SearchField when isFilterable is true", () => {
|
|
441
457
|
// Arrange
|
|
442
458
|
render(
|
|
443
459
|
<SingleSelect
|
|
@@ -484,7 +500,7 @@ describe("SingleSelect", () => {
|
|
|
484
500
|
expect(options[0]).toHaveTextContent("item 2");
|
|
485
501
|
});
|
|
486
502
|
|
|
487
|
-
it("Type something in
|
|
503
|
+
it("Type something in SearchField should update searchText in SingleSelect", () => {
|
|
488
504
|
// Arrange
|
|
489
505
|
render(
|
|
490
506
|
<SingleSelect
|
|
@@ -611,30 +627,7 @@ describe("SingleSelect", () => {
|
|
|
611
627
|
|
|
612
628
|
// Assert
|
|
613
629
|
const dropdownMenu = screen.getByRole("listbox");
|
|
614
|
-
expect(dropdownMenu).toHaveStyle("max-height:
|
|
615
|
-
});
|
|
616
|
-
|
|
617
|
-
it("should apply the default maxHeight to a filterable listbox", () => {
|
|
618
|
-
// Arrange
|
|
619
|
-
|
|
620
|
-
// Act
|
|
621
|
-
render(
|
|
622
|
-
<SingleSelect
|
|
623
|
-
onChange={onChange}
|
|
624
|
-
opened={true}
|
|
625
|
-
isFilterable={true}
|
|
626
|
-
placeholder="Choose"
|
|
627
|
-
selectedValue="2"
|
|
628
|
-
>
|
|
629
|
-
<OptionItem label="item 1" value="1" />
|
|
630
|
-
<OptionItem label="item 2" value="2" />
|
|
631
|
-
<OptionItem label="item 3" value="3" />
|
|
632
|
-
</SingleSelect>,
|
|
633
|
-
);
|
|
634
|
-
|
|
635
|
-
// Assert
|
|
636
|
-
const dropdownMenu = screen.getByRole("listbox");
|
|
637
|
-
expect(dropdownMenu).toHaveStyle("max-height: 184px");
|
|
630
|
+
expect(dropdownMenu).toHaveStyle("max-height: 120px");
|
|
638
631
|
});
|
|
639
632
|
|
|
640
633
|
it("should apply the default maxHeight to a virtualized listbox", () => {
|
|
@@ -665,7 +658,7 @@ describe("SingleSelect", () => {
|
|
|
665
658
|
// Assert
|
|
666
659
|
const dropdownMenu = screen.getByRole("listbox");
|
|
667
660
|
// Max allowed height
|
|
668
|
-
expect(dropdownMenu).toHaveStyle("max-height:
|
|
661
|
+
expect(dropdownMenu).toHaveStyle("max-height: 360px");
|
|
669
662
|
});
|
|
670
663
|
|
|
671
664
|
it("should override the default maxHeight to the listbox if a custom dropdownStyle is set", () => {
|
|
@@ -688,13 +681,13 @@ describe("SingleSelect", () => {
|
|
|
688
681
|
);
|
|
689
682
|
|
|
690
683
|
// Assert
|
|
691
|
-
const dropdownMenu = screen.
|
|
684
|
+
const dropdownMenu = screen.getByTestId("dropdown-core-container");
|
|
692
685
|
expect(dropdownMenu).toHaveStyle("max-height: 200px");
|
|
693
686
|
});
|
|
694
687
|
});
|
|
695
688
|
|
|
696
689
|
describe("a11y > Live region", () => {
|
|
697
|
-
it("should change the number of options after using the search filter",
|
|
690
|
+
it("should change the number of options after using the search filter", () => {
|
|
698
691
|
// Arrange
|
|
699
692
|
render(
|
|
700
693
|
<SingleSelect
|
|
@@ -103,6 +103,17 @@ type DefaultProps = {|
|
|
|
103
103
|
|
|
104
104
|
/**
|
|
105
105
|
* A menu that consists of various types of items.
|
|
106
|
+
*
|
|
107
|
+
* ## Usage
|
|
108
|
+
*
|
|
109
|
+
* ```jsx
|
|
110
|
+
* import {ActionMenu, ActionItem} from "@khanacademy/wonder-blocks-dropdown";
|
|
111
|
+
*
|
|
112
|
+
* <ActionMenu menuText="Menu">
|
|
113
|
+
* <ActionItem href="/profile" label="Profile" />
|
|
114
|
+
* <ActionItem label="Settings" onClick={() => {}} />
|
|
115
|
+
* </ActionMenu>
|
|
116
|
+
* ```
|
|
106
117
|
*/
|
|
107
118
|
export default class ActionMenu extends React.Component<Props, State> {
|
|
108
119
|
openerElement: ?HTMLElement;
|
|
@@ -9,7 +9,6 @@ import type {
|
|
|
9
9
|
WithoutActionScheduler,
|
|
10
10
|
} from "@khanacademy/wonder-blocks-timing";
|
|
11
11
|
import DropdownVirtualizedItem from "./dropdown-core-virtualized-item.js";
|
|
12
|
-
import SearchTextInput from "./search-text-input.js";
|
|
13
12
|
import SeparatorItem from "./separator-item.js";
|
|
14
13
|
|
|
15
14
|
import type {DropdownItem} from "../util/types.js";
|
|
@@ -17,7 +16,6 @@ import type {DropdownItem} from "../util/types.js";
|
|
|
17
16
|
import {
|
|
18
17
|
DROPDOWN_ITEM_HEIGHT,
|
|
19
18
|
MAX_VISIBLE_ITEMS,
|
|
20
|
-
SEARCH_ITEM_HEIGHT,
|
|
21
19
|
SEPARATOR_ITEM_HEIGHT,
|
|
22
20
|
} from "../util/constants.js";
|
|
23
21
|
import {getDropdownMenuHeight} from "../util/dropdown-menu-styles.js";
|
|
@@ -134,9 +132,6 @@ class DropdownCoreVirtualized extends React.Component<Props, State> {
|
|
|
134
132
|
if (SeparatorItem.isClassOf(item.component)) {
|
|
135
133
|
// this is the separator's height (1px) + vertical margin (8px)
|
|
136
134
|
return SEPARATOR_ITEM_HEIGHT;
|
|
137
|
-
} else if (SearchTextInput.isClassOf(item.component)) {
|
|
138
|
-
// search text input height
|
|
139
|
-
return SEARCH_ITEM_HEIGHT;
|
|
140
135
|
} else {
|
|
141
136
|
// default dropdown item height
|
|
142
137
|
return DROPDOWN_ITEM_HEIGHT;
|