@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
|
@@ -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,16 +557,14 @@ 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}
|
|
583
565
|
opener={opener}
|
|
584
566
|
openerElement={this.state.openerElement}
|
|
567
|
+
selectionType="multi"
|
|
585
568
|
style={style}
|
|
586
569
|
className={className}
|
|
587
570
|
onSearchTextChanged={
|
|
@@ -589,6 +572,8 @@ export default class MultiSelect extends React.Component<Props, State> {
|
|
|
589
572
|
}
|
|
590
573
|
searchText={isFilterable ? searchText : ""}
|
|
591
574
|
labels={{
|
|
575
|
+
clearSearch,
|
|
576
|
+
filter,
|
|
592
577
|
noResults,
|
|
593
578
|
someSelected,
|
|
594
579
|
}}
|
|
@@ -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,16 +373,13 @@ 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
|
|
397
381
|
role="listbox"
|
|
382
|
+
selectionType="single"
|
|
398
383
|
alignment={alignment}
|
|
399
384
|
dropdownStyle={[
|
|
400
385
|
isFilterable && filterableDropdownStyle,
|
|
@@ -410,6 +395,7 @@ export default class SingleSelect extends React.Component<Props, State> {
|
|
|
410
395
|
openerElement={this.state.openerElement}
|
|
411
396
|
style={style}
|
|
412
397
|
className={className}
|
|
398
|
+
isFilterable={isFilterable}
|
|
413
399
|
onSearchTextChanged={
|
|
414
400
|
isFilterable ? this.handleSearchTextChanged : null
|
|
415
401
|
}
|
|
@@ -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];
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import {debounce, getStringForKey} from "../helpers.js";
|
|
3
|
+
|
|
4
|
+
describe("getStringForKey", () => {
|
|
5
|
+
it("should get a valid string", () => {
|
|
6
|
+
// Arrange
|
|
7
|
+
|
|
8
|
+
// Act
|
|
9
|
+
const key = getStringForKey("a");
|
|
10
|
+
|
|
11
|
+
// Assert
|
|
12
|
+
expect(key).toBe("a");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("should return empty if we use a glyph modifier key (e.g. Shift)", () => {
|
|
16
|
+
// Arrange
|
|
17
|
+
|
|
18
|
+
// Act
|
|
19
|
+
const key = getStringForKey("Shift");
|
|
20
|
+
|
|
21
|
+
// Assert
|
|
22
|
+
expect(key).toBe("");
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe("debounce", () => {
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
jest.useFakeTimers();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("should call the debounced function", () => {
|
|
32
|
+
// Arrange
|
|
33
|
+
const callbackFnMock = jest.fn();
|
|
34
|
+
const debounced = debounce(callbackFnMock, 500);
|
|
35
|
+
|
|
36
|
+
// Act
|
|
37
|
+
debounced();
|
|
38
|
+
jest.advanceTimersByTime(501);
|
|
39
|
+
|
|
40
|
+
// Assert
|
|
41
|
+
expect(callbackFnMock).toHaveBeenCalled();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("should call the debounced function only once", () => {
|
|
45
|
+
// Arrange
|
|
46
|
+
const callbackFnMock = jest.fn();
|
|
47
|
+
const debounced = debounce(callbackFnMock, 500);
|
|
48
|
+
|
|
49
|
+
// Act
|
|
50
|
+
debounced();
|
|
51
|
+
debounced();
|
|
52
|
+
debounced();
|
|
53
|
+
jest.advanceTimersByTime(501);
|
|
54
|
+
|
|
55
|
+
// Assert
|
|
56
|
+
expect(callbackFnMock).toHaveBeenCalledTimes(1);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should execute the last call with the exact args", () => {
|
|
60
|
+
// Arrange
|
|
61
|
+
const callbackFnMock = jest.fn();
|
|
62
|
+
const debounced = debounce(callbackFnMock, 500);
|
|
63
|
+
|
|
64
|
+
// Act
|
|
65
|
+
debounced("a");
|
|
66
|
+
debounced("ab");
|
|
67
|
+
debounced("abc");
|
|
68
|
+
jest.advanceTimersByTime(501);
|
|
69
|
+
|
|
70
|
+
// Assert
|
|
71
|
+
expect(callbackFnMock).toHaveBeenCalledWith("abc");
|
|
72
|
+
});
|
|
73
|
+
});
|
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
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Checks if a given key is a valid ASCII value.
|
|
5
|
+
*
|
|
6
|
+
* @param {string} key The key that is being typed in.
|
|
7
|
+
* @returns A valid string representation of the given key.
|
|
8
|
+
*/
|
|
9
|
+
export function getStringForKey(key: string): string {
|
|
10
|
+
// If the key is of length 1, it is an ASCII value.
|
|
11
|
+
// Otherwise, if there are no ASCII characters in the key name,
|
|
12
|
+
// it is a Unicode character.
|
|
13
|
+
// See https://www.w3.org/TR/uievents-key/
|
|
14
|
+
if (key.length === 1 || !/^[A-Z]/i.test(key)) {
|
|
15
|
+
return key;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return "";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
*
|
|
23
|
+
* @param {fn} callback The function that will be executed after the debounce is resolved.
|
|
24
|
+
* @param {number} wait The period of time that will be executed the debounced
|
|
25
|
+
* function.
|
|
26
|
+
* @returns The function that will be executed after the wait period is
|
|
27
|
+
* fulfilled.
|
|
28
|
+
*/
|
|
29
|
+
export function debounce(
|
|
30
|
+
callback: (...args: any) => void,
|
|
31
|
+
wait: number,
|
|
32
|
+
): (...args: any) => void {
|
|
33
|
+
let timeout;
|
|
34
|
+
|
|
35
|
+
return function executedFunction(...args) {
|
|
36
|
+
const later = () => {
|
|
37
|
+
clearTimeout(timeout);
|
|
38
|
+
callback(...args);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
clearTimeout(timeout);
|
|
42
|
+
timeout = setTimeout(later, wait);
|
|
43
|
+
};
|
|
44
|
+
}
|
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
|
-
];
|