@khanacademy/wonder-blocks-dropdown 2.7.3 → 2.7.6
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 +24 -0
- package/dist/es/index.js +92 -162
- package/dist/index.js +285 -374
- package/package.json +6 -6
- 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 +114 -208
- package/src/components/__tests__/multi-select.test.js +1 -3
- package/src/components/__tests__/single-select.test.js +15 -47
- package/src/components/action-menu.js +11 -0
- package/src/components/dropdown-core-virtualized.js +0 -5
- package/src/components/dropdown-core.js +140 -126
- package/src/components/multi-select.js +17 -33
- package/src/components/single-select.js +15 -30
- package/src/util/__tests__/dropdown-menu-styles.test.js +0 -26
- package/src/util/constants.js +0 -11
- package/src/util/dropdown-menu-styles.js +0 -5
- 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
|
@@ -8,7 +8,6 @@ import type {AriaProps, StyleType} from "@khanacademy/wonder-blocks-core";
|
|
|
8
8
|
import ActionItem from "./action-item.js";
|
|
9
9
|
import DropdownCore from "./dropdown-core.js";
|
|
10
10
|
import DropdownOpener from "./dropdown-opener.js";
|
|
11
|
-
import SearchTextInput from "./search-text-input.js";
|
|
12
11
|
import SelectOpener from "./select-opener.js";
|
|
13
12
|
import SeparatorItem from "./separator-item.js";
|
|
14
13
|
import {
|
|
@@ -211,6 +210,17 @@ type State = {|
|
|
|
211
210
|
*
|
|
212
211
|
* The multi select stays open until closed by the user. The onChange callback
|
|
213
212
|
* happens every time there is a change in the selection of the items.
|
|
213
|
+
*
|
|
214
|
+
* ## Usage
|
|
215
|
+
*
|
|
216
|
+
* ```jsx
|
|
217
|
+
* import {OptionItem, MultiSelect} from "@khanacademy/wonder-blocks-dropdown";
|
|
218
|
+
*
|
|
219
|
+
* <MultiSelect onChange={setSelectedValues} selectedValues={selectedValues}>
|
|
220
|
+
* <OptionItem value="pear">Pear</OptionItem>
|
|
221
|
+
* <OptionItem value="mango">Mango</OptionItem>
|
|
222
|
+
* </MultiSelect>
|
|
223
|
+
* ```
|
|
214
224
|
*/
|
|
215
225
|
export default class MultiSelect extends React.Component<Props, State> {
|
|
216
226
|
labels: Labels;
|
|
@@ -329,32 +339,6 @@ export default class MultiSelect extends React.Component<Props, State> {
|
|
|
329
339
|
}
|
|
330
340
|
}
|
|
331
341
|
|
|
332
|
-
getSearchField(): Array<DropdownItem> {
|
|
333
|
-
if (!this.props.isFilterable) {
|
|
334
|
-
return [];
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
const {clearSearch, filter} = this.state.labels;
|
|
338
|
-
|
|
339
|
-
return [
|
|
340
|
-
{
|
|
341
|
-
component: (
|
|
342
|
-
<SearchTextInput
|
|
343
|
-
key="search-text-input"
|
|
344
|
-
onChange={this.handleSearchTextChanged}
|
|
345
|
-
searchText={this.state.searchText}
|
|
346
|
-
labels={{
|
|
347
|
-
clearSearch,
|
|
348
|
-
filter,
|
|
349
|
-
}}
|
|
350
|
-
/>
|
|
351
|
-
),
|
|
352
|
-
focusable: true,
|
|
353
|
-
populatedProps: {},
|
|
354
|
-
},
|
|
355
|
-
];
|
|
356
|
-
}
|
|
357
|
-
|
|
358
342
|
getShortcuts(numOptions: number): Array<DropdownItem> {
|
|
359
343
|
const {selectedValues, shortcuts} = this.props;
|
|
360
344
|
const {selectAllLabel, selectNoneLabel} = this.state.labels;
|
|
@@ -556,7 +540,8 @@ export default class MultiSelect extends React.Component<Props, State> {
|
|
|
556
540
|
isFilterable,
|
|
557
541
|
} = this.props;
|
|
558
542
|
const {open, searchText} = this.state;
|
|
559
|
-
const {noResults, someSelected} =
|
|
543
|
+
const {clearSearch, filter, noResults, someSelected} =
|
|
544
|
+
this.state.labels;
|
|
560
545
|
|
|
561
546
|
const allChildren = React.Children.toArray(children).filter(Boolean);
|
|
562
547
|
const numOptions = allChildren.length;
|
|
@@ -572,11 +557,8 @@ export default class MultiSelect extends React.Component<Props, State> {
|
|
|
572
557
|
selectDropdownStyle,
|
|
573
558
|
dropdownStyle,
|
|
574
559
|
]}
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
...this.getShortcuts(numOptions),
|
|
578
|
-
...filteredItems,
|
|
579
|
-
]}
|
|
560
|
+
isFilterable={isFilterable}
|
|
561
|
+
items={[...this.getShortcuts(numOptions), ...filteredItems]}
|
|
580
562
|
light={light}
|
|
581
563
|
onOpenChanged={this.handleOpenChanged}
|
|
582
564
|
open={open}
|
|
@@ -589,6 +571,8 @@ export default class MultiSelect extends React.Component<Props, State> {
|
|
|
589
571
|
}
|
|
590
572
|
searchText={isFilterable ? searchText : ""}
|
|
591
573
|
labels={{
|
|
574
|
+
clearSearch,
|
|
575
|
+
filter,
|
|
592
576
|
noResults,
|
|
593
577
|
someSelected,
|
|
594
578
|
}}
|
|
@@ -9,14 +9,12 @@ import DropdownCore from "./dropdown-core.js";
|
|
|
9
9
|
import DropdownOpener from "./dropdown-opener.js";
|
|
10
10
|
import SelectOpener from "./select-opener.js";
|
|
11
11
|
import {
|
|
12
|
-
defaultLabels,
|
|
13
12
|
selectDropdownStyle,
|
|
14
13
|
filterableDropdownStyle,
|
|
15
14
|
} from "../util/constants.js";
|
|
16
15
|
|
|
17
16
|
import typeof OptionItem from "./option-item.js";
|
|
18
17
|
import type {DropdownItem, OpenerProps} from "../util/types.js";
|
|
19
|
-
import SearchTextInput from "./search-text-input.js";
|
|
20
18
|
|
|
21
19
|
type Props = {|
|
|
22
20
|
...AriaProps,
|
|
@@ -144,11 +142,23 @@ type DefaultProps = {|
|
|
|
144
142
|
* The single select dropdown closes after the selection of an item. If the same
|
|
145
143
|
* item is selected, there is no callback.
|
|
146
144
|
*
|
|
147
|
-
*
|
|
145
|
+
* **NOTE:** If there are more than 125 items, the component automatically uses
|
|
148
146
|
* [react-window](https://github.com/bvaughn/react-window) to improve
|
|
149
147
|
* performance when rendering these elements and is capable of handling many
|
|
150
148
|
* hundreds of items without performance problems.
|
|
151
149
|
*
|
|
150
|
+
* ## Usage
|
|
151
|
+
*
|
|
152
|
+
* ```jsx
|
|
153
|
+
* import {OptionItem, SingleSelect} from "@khanacademy/wonder-blocks-dropdown";
|
|
154
|
+
*
|
|
155
|
+
* const [selectedValue, setSelectedValue] = useState("");
|
|
156
|
+
*
|
|
157
|
+
* <SingleSelect placeholder="Choose a fruit" onChange={setSelectedValue} selectedValue={selectedValue}>
|
|
158
|
+
* <OptionItem value="pear">Pear</OptionItem>
|
|
159
|
+
* <OptionItem value="mango">Mango</OptionItem>
|
|
160
|
+
* </SingleSelect>
|
|
161
|
+
* ```
|
|
152
162
|
*/
|
|
153
163
|
export default class SingleSelect extends React.Component<Props, State> {
|
|
154
164
|
selectedIndex: number;
|
|
@@ -275,28 +285,6 @@ export default class SingleSelect extends React.Component<Props, State> {
|
|
|
275
285
|
);
|
|
276
286
|
}
|
|
277
287
|
|
|
278
|
-
getSearchField(): ?DropdownItem {
|
|
279
|
-
if (!this.props.isFilterable) {
|
|
280
|
-
return null;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
return {
|
|
284
|
-
component: (
|
|
285
|
-
<SearchTextInput
|
|
286
|
-
key="search-text-input"
|
|
287
|
-
onChange={this.handleSearchTextChanged}
|
|
288
|
-
searchText={this.state.searchText}
|
|
289
|
-
labels={{
|
|
290
|
-
clearSearch: defaultLabels.clearSearch,
|
|
291
|
-
filter: defaultLabels.filter,
|
|
292
|
-
}}
|
|
293
|
-
/>
|
|
294
|
-
),
|
|
295
|
-
focusable: true,
|
|
296
|
-
populatedProps: {},
|
|
297
|
-
};
|
|
298
|
-
}
|
|
299
|
-
|
|
300
288
|
handleSearchTextChanged: (searchText: string) => void = (searchText) => {
|
|
301
289
|
this.setState({searchText});
|
|
302
290
|
};
|
|
@@ -385,12 +373,8 @@ export default class SingleSelect extends React.Component<Props, State> {
|
|
|
385
373
|
} = this.props;
|
|
386
374
|
const {searchText} = this.state;
|
|
387
375
|
const allChildren = React.Children.toArray(children).filter(Boolean);
|
|
388
|
-
const
|
|
376
|
+
const items = this.getMenuItems(allChildren);
|
|
389
377
|
const opener = this.renderOpener(allChildren.length);
|
|
390
|
-
const searchField = this.getSearchField();
|
|
391
|
-
const items = searchField
|
|
392
|
-
? [searchField, ...filteredItems]
|
|
393
|
-
: filteredItems;
|
|
394
378
|
|
|
395
379
|
return (
|
|
396
380
|
<DropdownCore
|
|
@@ -410,6 +394,7 @@ export default class SingleSelect extends React.Component<Props, State> {
|
|
|
410
394
|
openerElement={this.state.openerElement}
|
|
411
395
|
style={style}
|
|
412
396
|
className={className}
|
|
397
|
+
isFilterable={isFilterable}
|
|
413
398
|
onSearchTextChanged={
|
|
414
399
|
isFilterable ? this.handleSearchTextChanged : null
|
|
415
400
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// @flow
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
import OptionItem from "../../components/option-item.js";
|
|
4
|
-
import SearchTextInput from "../../components/search-text-input.js";
|
|
5
4
|
import SeparatorItem from "../../components/separator-item.js";
|
|
6
5
|
|
|
7
6
|
import {getDropdownMenuHeight} from "../dropdown-menu-styles.js";
|
|
@@ -30,19 +29,6 @@ const optionItems = [
|
|
|
30
29
|
},
|
|
31
30
|
];
|
|
32
31
|
|
|
33
|
-
const searchFieldItem = {
|
|
34
|
-
component: (
|
|
35
|
-
<SearchTextInput
|
|
36
|
-
testId="item-0"
|
|
37
|
-
key="search-text-input"
|
|
38
|
-
onChange={jest.fn()}
|
|
39
|
-
searchText={""}
|
|
40
|
-
/>
|
|
41
|
-
),
|
|
42
|
-
focusable: true,
|
|
43
|
-
populatedProps: {},
|
|
44
|
-
};
|
|
45
|
-
|
|
46
32
|
const separatorItem = {
|
|
47
33
|
component: <SeparatorItem />,
|
|
48
34
|
focusable: false,
|
|
@@ -74,18 +60,6 @@ describe("getDropdownMenuHeight", () => {
|
|
|
74
60
|
expect(height).toBe(130);
|
|
75
61
|
});
|
|
76
62
|
|
|
77
|
-
it("should get a valid height for a filterable dropdown", () => {
|
|
78
|
-
// Arrange
|
|
79
|
-
const items = [searchFieldItem, ...optionItems];
|
|
80
|
-
|
|
81
|
-
// Act
|
|
82
|
-
const height = getDropdownMenuHeight(items);
|
|
83
|
-
|
|
84
|
-
// Assert
|
|
85
|
-
// search field + 3 option items
|
|
86
|
-
expect(height).toBe(172);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
63
|
it("should get a valid height for a dropdown with a SeparatorItem", () => {
|
|
90
64
|
// Arrange
|
|
91
65
|
const items = [separatorItem, ...optionItems];
|
package/src/util/constants.js
CHANGED
|
@@ -21,14 +21,6 @@ export const filterableDropdownStyle = {
|
|
|
21
21
|
minHeight: 100,
|
|
22
22
|
};
|
|
23
23
|
|
|
24
|
-
export const searchInputStyle = {
|
|
25
|
-
margin: Spacing.xSmall_8,
|
|
26
|
-
marginTop: Spacing.xxxSmall_4,
|
|
27
|
-
// Set `minHeight` to "auto" to stop the search field from having
|
|
28
|
-
// a height of 0 and being cut off.
|
|
29
|
-
minHeight: "auto",
|
|
30
|
-
};
|
|
31
|
-
|
|
32
24
|
// The default item height
|
|
33
25
|
export const DROPDOWN_ITEM_HEIGHT = 40;
|
|
34
26
|
|
|
@@ -41,9 +33,6 @@ export const MAX_VISIBLE_ITEMS = 9;
|
|
|
41
33
|
|
|
42
34
|
export const SEPARATOR_ITEM_HEIGHT = 9;
|
|
43
35
|
|
|
44
|
-
export const SEARCH_ITEM_HEIGHT: number =
|
|
45
|
-
DROPDOWN_ITEM_HEIGHT + searchInputStyle.margin + searchInputStyle.marginTop;
|
|
46
|
-
|
|
47
36
|
// The default labels that will be used by different components
|
|
48
37
|
export const defaultLabels = {
|
|
49
38
|
clearSearch: "Clear search",
|
|
@@ -6,12 +6,10 @@ import type {StyleType} from "@khanacademy/wonder-blocks-core";
|
|
|
6
6
|
import {
|
|
7
7
|
DROPDOWN_ITEM_HEIGHT,
|
|
8
8
|
MAX_VISIBLE_ITEMS,
|
|
9
|
-
SEARCH_ITEM_HEIGHT,
|
|
10
9
|
SEPARATOR_ITEM_HEIGHT,
|
|
11
10
|
} from "./constants.js";
|
|
12
11
|
|
|
13
12
|
import SeparatorItem from "../components/separator-item.js";
|
|
14
|
-
import SearchTextInput from "../components/search-text-input.js";
|
|
15
13
|
|
|
16
14
|
import type {DropdownItem} from "./types.js";
|
|
17
15
|
|
|
@@ -33,9 +31,6 @@ export function getDropdownMenuHeight(
|
|
|
33
31
|
return items.slice(0, MAX_VISIBLE_ITEMS).reduce((sum, item) => {
|
|
34
32
|
if (SeparatorItem.isClassOf(item.component)) {
|
|
35
33
|
return sum + SEPARATOR_ITEM_HEIGHT;
|
|
36
|
-
} else if (SearchTextInput.isClassOf(item.component)) {
|
|
37
|
-
// search text input height
|
|
38
|
-
return sum + SEARCH_ITEM_HEIGHT;
|
|
39
34
|
} else {
|
|
40
35
|
return sum + DROPDOWN_ITEM_HEIGHT;
|
|
41
36
|
}
|
package/src/util/types.js
CHANGED
|
@@ -5,18 +5,15 @@ import type {ClickableState} from "@khanacademy/wonder-blocks-clickable";
|
|
|
5
5
|
|
|
6
6
|
import typeof ActionItem from "../components/action-item.js";
|
|
7
7
|
import typeof OptionItem from "../components/option-item.js";
|
|
8
|
-
import typeof SearchTextInput from "../components/search-text-input.js";
|
|
9
8
|
import typeof SeparatorItem from "../components/separator-item.js";
|
|
10
9
|
|
|
11
10
|
//TODO: rename into something more descriptive
|
|
12
11
|
export type Item =
|
|
13
12
|
| false
|
|
14
|
-
| React.Element<ActionItem | OptionItem | SeparatorItem
|
|
13
|
+
| React.Element<ActionItem | OptionItem | SeparatorItem>;
|
|
15
14
|
|
|
16
15
|
export type DropdownItem = {|
|
|
17
|
-
component: React.Element<
|
|
18
|
-
ActionItem | OptionItem | SeparatorItem | SearchTextInput,
|
|
19
|
-
>,
|
|
16
|
+
component: React.Element<ActionItem | OptionItem | SeparatorItem>,
|
|
20
17
|
focusable: boolean,
|
|
21
18
|
populatedProps: any,
|
|
22
19
|
// extra props used by DropdownCore
|
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
// @flow
|
|
2
|
-
import * as React from "react";
|
|
3
|
-
import {render, screen} from "@testing-library/react";
|
|
4
|
-
import userEvent from "@testing-library/user-event";
|
|
5
|
-
|
|
6
|
-
import SearchTextInput from "../search-text-input.js";
|
|
7
|
-
|
|
8
|
-
describe("SearchTextInput", () => {
|
|
9
|
-
test("text input container should be focused when focusing on the input", () => {
|
|
10
|
-
// Arrange
|
|
11
|
-
render(
|
|
12
|
-
<SearchTextInput
|
|
13
|
-
searchText=""
|
|
14
|
-
testId="search-text-input"
|
|
15
|
-
onChange={jest.fn()}
|
|
16
|
-
/>,
|
|
17
|
-
);
|
|
18
|
-
|
|
19
|
-
const input = screen.getByTestId("search-text-input");
|
|
20
|
-
|
|
21
|
-
// Act
|
|
22
|
-
input.focus();
|
|
23
|
-
|
|
24
|
-
// Assert
|
|
25
|
-
expect(input).toHaveFocus();
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
test("text input should not be focused when losing focus", () => {
|
|
29
|
-
// Arrange
|
|
30
|
-
render(
|
|
31
|
-
<SearchTextInput
|
|
32
|
-
searchText=""
|
|
33
|
-
testId="search-text-input"
|
|
34
|
-
onChange={jest.fn()}
|
|
35
|
-
/>,
|
|
36
|
-
);
|
|
37
|
-
|
|
38
|
-
const input = screen.getByTestId("search-text-input");
|
|
39
|
-
// focus in
|
|
40
|
-
input.focus();
|
|
41
|
-
|
|
42
|
-
// Act
|
|
43
|
-
// focus out
|
|
44
|
-
input.blur();
|
|
45
|
-
|
|
46
|
-
// Assert
|
|
47
|
-
expect(input).not.toHaveFocus();
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
test("onChange should be invoked if text input changes", () => {
|
|
51
|
-
// Arrange
|
|
52
|
-
const onChangeMock = jest.fn();
|
|
53
|
-
|
|
54
|
-
render(
|
|
55
|
-
<SearchTextInput
|
|
56
|
-
searchText=""
|
|
57
|
-
testId="search-text-input"
|
|
58
|
-
onChange={onChangeMock}
|
|
59
|
-
/>,
|
|
60
|
-
);
|
|
61
|
-
|
|
62
|
-
const input = screen.getByTestId("search-text-input");
|
|
63
|
-
|
|
64
|
-
// Act
|
|
65
|
-
userEvent.paste(input, "value");
|
|
66
|
-
|
|
67
|
-
// Assert
|
|
68
|
-
expect(onChangeMock).toHaveBeenCalledWith("value");
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
test("displays the dismiss button when search text exists", () => {
|
|
72
|
-
// Arrange
|
|
73
|
-
const SearchFieldWrapper = () => {
|
|
74
|
-
const [value, setValue] = React.useState("");
|
|
75
|
-
return (
|
|
76
|
-
<SearchTextInput
|
|
77
|
-
searchText={value}
|
|
78
|
-
testId="search-text-input"
|
|
79
|
-
onChange={setValue}
|
|
80
|
-
/>
|
|
81
|
-
);
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
render(<SearchFieldWrapper />);
|
|
85
|
-
|
|
86
|
-
const input = screen.getByTestId("search-text-input");
|
|
87
|
-
|
|
88
|
-
// Act
|
|
89
|
-
userEvent.paste(input, "value");
|
|
90
|
-
|
|
91
|
-
// Assert
|
|
92
|
-
const clearIconButton = screen.queryByRole("button");
|
|
93
|
-
expect(clearIconButton).toBeInTheDocument();
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
test("search should be cleared if the clear icon is clicked", () => {
|
|
97
|
-
// Arrange
|
|
98
|
-
const SearchFieldWrapper = () => {
|
|
99
|
-
const [value, setValue] = React.useState("initial value");
|
|
100
|
-
return (
|
|
101
|
-
<SearchTextInput
|
|
102
|
-
searchText={value}
|
|
103
|
-
testId="search-text-input"
|
|
104
|
-
onChange={setValue}
|
|
105
|
-
/>
|
|
106
|
-
);
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
render(<SearchFieldWrapper />);
|
|
110
|
-
|
|
111
|
-
const input = screen.getByTestId("search-text-input");
|
|
112
|
-
const clearIconButton = screen.queryByRole("button");
|
|
113
|
-
|
|
114
|
-
// Act
|
|
115
|
-
userEvent.click(clearIconButton);
|
|
116
|
-
|
|
117
|
-
// Assert
|
|
118
|
-
expect(input).toHaveValue("");
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
test("focus should return to the input element after clear button is clicked", () => {
|
|
122
|
-
// Arrange
|
|
123
|
-
const SearchFieldWrapper = () => {
|
|
124
|
-
const [value, setValue] = React.useState("");
|
|
125
|
-
return (
|
|
126
|
-
<SearchTextInput
|
|
127
|
-
searchText={value}
|
|
128
|
-
testId="search-text-input"
|
|
129
|
-
onChange={setValue}
|
|
130
|
-
/>
|
|
131
|
-
);
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
render(<SearchFieldWrapper />);
|
|
135
|
-
|
|
136
|
-
const input = screen.getByTestId("search-text-input");
|
|
137
|
-
userEvent.paste(input, "something");
|
|
138
|
-
|
|
139
|
-
// Act
|
|
140
|
-
const clearIconButton = screen.queryByRole("button");
|
|
141
|
-
userEvent.click(clearIconButton);
|
|
142
|
-
|
|
143
|
-
// Assert
|
|
144
|
-
expect(input).toHaveFocus();
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
test("placeholder should be updated by the parent component", () => {
|
|
148
|
-
// Arrange
|
|
149
|
-
const {rerender} = render(
|
|
150
|
-
<SearchTextInput
|
|
151
|
-
searchText="query"
|
|
152
|
-
onChange={() => {}}
|
|
153
|
-
labels={{
|
|
154
|
-
clearSearch: "Clear",
|
|
155
|
-
filter: "Filter",
|
|
156
|
-
}}
|
|
157
|
-
testId="search-text-input"
|
|
158
|
-
/>,
|
|
159
|
-
);
|
|
160
|
-
|
|
161
|
-
const input = screen.getByTestId("search-text-input");
|
|
162
|
-
|
|
163
|
-
// Act
|
|
164
|
-
rerender(
|
|
165
|
-
<SearchTextInput
|
|
166
|
-
searchText="query"
|
|
167
|
-
onChange={() => {}}
|
|
168
|
-
labels={{
|
|
169
|
-
clearSearch: "Dismiss",
|
|
170
|
-
filter: "Search",
|
|
171
|
-
}}
|
|
172
|
-
testId="search-text-input"
|
|
173
|
-
/>,
|
|
174
|
-
);
|
|
175
|
-
|
|
176
|
-
// Assert
|
|
177
|
-
expect(input).toHaveAttribute("placeholder", "Search");
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
test("button label should be updated by the parent component", () => {
|
|
181
|
-
// Arrange
|
|
182
|
-
const {rerender} = render(
|
|
183
|
-
<SearchTextInput
|
|
184
|
-
searchText="query"
|
|
185
|
-
onChange={() => {}}
|
|
186
|
-
labels={{
|
|
187
|
-
clearSearch: "Clear",
|
|
188
|
-
filter: "Filter",
|
|
189
|
-
}}
|
|
190
|
-
testId="search-text-input"
|
|
191
|
-
/>,
|
|
192
|
-
);
|
|
193
|
-
|
|
194
|
-
const clearIconButton = screen.queryByRole("button");
|
|
195
|
-
|
|
196
|
-
// Act
|
|
197
|
-
rerender(
|
|
198
|
-
<SearchTextInput
|
|
199
|
-
searchText="query"
|
|
200
|
-
onChange={() => {}}
|
|
201
|
-
labels={{
|
|
202
|
-
clearSearch: "Dismiss",
|
|
203
|
-
filter: "Search",
|
|
204
|
-
}}
|
|
205
|
-
testId="search-text-input"
|
|
206
|
-
/>,
|
|
207
|
-
);
|
|
208
|
-
|
|
209
|
-
// Assert
|
|
210
|
-
expect(clearIconButton).toHaveAttribute("aria-label", "Dismiss");
|
|
211
|
-
});
|
|
212
|
-
});
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
// @flow
|
|
2
|
-
import * as React from "react";
|
|
3
|
-
|
|
4
|
-
import type {StoryComponentType} from "@storybook/react";
|
|
5
|
-
import ActionMenu from "./action-menu.js";
|
|
6
|
-
import ActionItem from "./action-item.js";
|
|
7
|
-
|
|
8
|
-
export default {
|
|
9
|
-
title: "Dropdown / ActionMenu",
|
|
10
|
-
component: ActionMenu,
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
export const ActionMenuWithLang: StoryComponentType = () => (
|
|
14
|
-
<ActionMenu menuText="Locales">
|
|
15
|
-
{locales.map((locale) => (
|
|
16
|
-
<ActionItem
|
|
17
|
-
key={locale.locale}
|
|
18
|
-
label={locale.localName}
|
|
19
|
-
lang={locale.locale}
|
|
20
|
-
testId={"language_picker_" + locale.locale}
|
|
21
|
-
/>
|
|
22
|
-
))}
|
|
23
|
-
</ActionMenu>
|
|
24
|
-
);
|
|
25
|
-
|
|
26
|
-
ActionMenuWithLang.storyName = "Using the ActionMenu with the lang attribute";
|
|
27
|
-
|
|
28
|
-
ActionMenuWithLang.parameters = {
|
|
29
|
-
docs: {
|
|
30
|
-
storyDescription:
|
|
31
|
-
"You can use the `lang` attribute to specify the language of the action item(s). This is useful if you want to avoid issues with Screen Readers trying to read the proper language for the rendered text.",
|
|
32
|
-
},
|
|
33
|
-
chromatic: {
|
|
34
|
-
disableSnapshot: true,
|
|
35
|
-
},
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
const locales = [
|
|
39
|
-
{id: "az", locale: "az", localName: "Azərbaycanca"},
|
|
40
|
-
{id: "id", locale: "id", localName: "Bahasa Indonesia"},
|
|
41
|
-
{id: "cs", locale: "cs", localName: "čeština"},
|
|
42
|
-
{id: "da", locale: "da", localName: "dansk"},
|
|
43
|
-
{id: "de", locale: "de", localName: "Deutsch"},
|
|
44
|
-
{id: "en", locale: "en", localName: "English"},
|
|
45
|
-
{id: "es", locale: "es", localName: "español"},
|
|
46
|
-
{id: "fr", locale: "fr", localName: "français"},
|
|
47
|
-
{id: "it", locale: "it", localName: "italiano"},
|
|
48
|
-
];
|
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
// @flow
|
|
2
|
-
import * as React from "react";
|
|
3
|
-
import {StyleSheet} from "aphrodite";
|
|
4
|
-
|
|
5
|
-
import {View} from "@khanacademy/wonder-blocks-core";
|
|
6
|
-
|
|
7
|
-
import type {Labels} from "@khanacademy/wonder-blocks-dropdown";
|
|
8
|
-
import type {StoryComponentType} from "@storybook/react";
|
|
9
|
-
|
|
10
|
-
import {MultiSelect, OptionItem} from "../index.js";
|
|
11
|
-
|
|
12
|
-
export default {
|
|
13
|
-
title: "Dropdown / MultiSelect",
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
// Custom MultiSelect labels
|
|
17
|
-
const dropdownLabels: $Shape<Labels> = {
|
|
18
|
-
noneSelected: "Solar system",
|
|
19
|
-
someSelected: (numSelectedValues) => `${numSelectedValues} planets`,
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
type Props = {|
|
|
23
|
-
opened: boolean,
|
|
24
|
-
|};
|
|
25
|
-
|
|
26
|
-
type State = {|
|
|
27
|
-
opened: boolean,
|
|
28
|
-
selectedValues: Array<string>,
|
|
29
|
-
|};
|
|
30
|
-
|
|
31
|
-
type DefaultProps = {|
|
|
32
|
-
opened: $PropertyType<Props, "opened">,
|
|
33
|
-
|};
|
|
34
|
-
|
|
35
|
-
class MultiSelectWithCustomStyles extends React.Component<Props, State> {
|
|
36
|
-
static defaultProps: DefaultProps = {
|
|
37
|
-
opened: false,
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
state: State = {
|
|
41
|
-
selectedValues: [],
|
|
42
|
-
opened: this.props.opened,
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
handleChange: (update: Array<string>) => void = (update) => {
|
|
46
|
-
this.setState({
|
|
47
|
-
selectedValues: update,
|
|
48
|
-
});
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
handleToggleMenu: (opened: boolean) => void = (opened) => {
|
|
52
|
-
this.setState({
|
|
53
|
-
opened,
|
|
54
|
-
});
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
render(): React.Node {
|
|
58
|
-
return (
|
|
59
|
-
<View style={styles.wrapper}>
|
|
60
|
-
<MultiSelect
|
|
61
|
-
onChange={this.handleChange}
|
|
62
|
-
selectedValues={this.state.selectedValues}
|
|
63
|
-
style={styles.setWidth}
|
|
64
|
-
dropdownStyle={styles.customDropdown}
|
|
65
|
-
labels={dropdownLabels}
|
|
66
|
-
opened={this.state.opened}
|
|
67
|
-
onToggle={this.handleToggleMenu}
|
|
68
|
-
>
|
|
69
|
-
<OptionItem label="Mercury" value="1" />
|
|
70
|
-
<OptionItem label="Venus" value="2" />
|
|
71
|
-
<OptionItem label="Earth" value="3" disabled />
|
|
72
|
-
<OptionItem label="Mars" value="4" />
|
|
73
|
-
<OptionItem label="Jupiter" value="5" />
|
|
74
|
-
<OptionItem label="Saturn" value="6" />
|
|
75
|
-
<OptionItem label="Neptune" value="7" />
|
|
76
|
-
<OptionItem label="Uranus" value="8" />
|
|
77
|
-
</MultiSelect>
|
|
78
|
-
</View>
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export const CustomStyles: StoryComponentType = () => (
|
|
84
|
-
<MultiSelectWithCustomStyles />
|
|
85
|
-
);
|
|
86
|
-
|
|
87
|
-
CustomStyles.parameters = {
|
|
88
|
-
chromatic: {
|
|
89
|
-
// we don't need screenshots because this story only tests behavior.
|
|
90
|
-
disableSnapshot: true,
|
|
91
|
-
},
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
export const CustomStylesOpened: StoryComponentType = () => (
|
|
95
|
-
<MultiSelectWithCustomStyles opened={true} />
|
|
96
|
-
);
|
|
97
|
-
|
|
98
|
-
export const DisabledMultiSelect: StoryComponentType = () => (
|
|
99
|
-
<MultiSelect disabled={true} onChange={() => {}}>
|
|
100
|
-
<OptionItem label="Mercury" value="1" />
|
|
101
|
-
<OptionItem label="Venus" value="2" />
|
|
102
|
-
</MultiSelect>
|
|
103
|
-
);
|
|
104
|
-
|
|
105
|
-
DisabledMultiSelect.parameters = {
|
|
106
|
-
docs: {
|
|
107
|
-
storyDescription:
|
|
108
|
-
"`MultiSelect` can be disabled by passing `disabled={true}`. This can be useful when you want to disable a control temporarily.",
|
|
109
|
-
},
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
const styles = StyleSheet.create({
|
|
113
|
-
setWidth: {
|
|
114
|
-
minWidth: 170,
|
|
115
|
-
width: "100%",
|
|
116
|
-
},
|
|
117
|
-
customDropdown: {
|
|
118
|
-
maxHeight: 200,
|
|
119
|
-
},
|
|
120
|
-
wrapper: {
|
|
121
|
-
height: "800px",
|
|
122
|
-
width: "1200px",
|
|
123
|
-
},
|
|
124
|
-
});
|