@khanacademy/wonder-blocks-dropdown 2.9.4 → 2.10.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 +16 -0
- package/dist/es/index.js +21 -6
- package/dist/index.js +22 -5
- package/package.json +5 -5
- package/src/components/__docs__/single-select.stories.js +113 -14
- package/src/components/__tests__/single-select.test.js +51 -0
- package/src/components/dropdown-core.js +62 -31
- package/src/components/option-item.js +1 -0
- package/src/components/single-select.js +54 -30
- package/src/components/__docs__/single-select.argtypes.js +0 -54
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @khanacademy/wonder-blocks-dropdown
|
|
2
2
|
|
|
3
|
+
## 2.10.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 0e773ce6: Add `autoFocus` and `enableTypeAhead` props to improve keyboard navigation with the `SingleSelect` component
|
|
8
|
+
|
|
9
|
+
## 2.9.5
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- ceb111df: ClickableBehavior no longer has tabIndex 0 by default. It must be passed in.
|
|
14
|
+
- Updated dependencies [ceb111df]
|
|
15
|
+
- @khanacademy/wonder-blocks-clickable@2.4.0
|
|
16
|
+
- @khanacademy/wonder-blocks-modal@3.0.1
|
|
17
|
+
- @khanacademy/wonder-blocks-search-field@1.0.13
|
|
18
|
+
|
|
3
19
|
## 2.9.4
|
|
4
20
|
|
|
5
21
|
### Patch Changes
|
package/dist/es/index.js
CHANGED
|
@@ -302,7 +302,8 @@ class OptionItem extends React.Component {
|
|
|
302
302
|
return React.createElement(ClickableBehavior, {
|
|
303
303
|
disabled: disabled,
|
|
304
304
|
onClick: this.handleClick,
|
|
305
|
-
role: role
|
|
305
|
+
role: role,
|
|
306
|
+
tabIndex: 0
|
|
306
307
|
}, (state, childrenProps) => {
|
|
307
308
|
const {
|
|
308
309
|
pressed,
|
|
@@ -748,13 +749,14 @@ class DropdownCore extends React.Component {
|
|
|
748
749
|
|
|
749
750
|
this.handleKeyDown = event => {
|
|
750
751
|
const {
|
|
752
|
+
enableTypeAhead,
|
|
751
753
|
onOpenChanged,
|
|
752
754
|
open,
|
|
753
755
|
searchText
|
|
754
756
|
} = this.props;
|
|
755
757
|
const keyCode = event.which || event.keyCode;
|
|
756
758
|
|
|
757
|
-
if (getStringForKey(event.key)) {
|
|
759
|
+
if (enableTypeAhead && getStringForKey(event.key)) {
|
|
758
760
|
event.stopPropagation();
|
|
759
761
|
this.textSuggestion += event.key;
|
|
760
762
|
this.handleKeyDownDebounced(this.textSuggestion);
|
|
@@ -913,7 +915,7 @@ class DropdownCore extends React.Component {
|
|
|
913
915
|
|
|
914
916
|
componentDidMount() {
|
|
915
917
|
this.updateEventListeners();
|
|
916
|
-
this.
|
|
918
|
+
this.maybeFocusInitialItem();
|
|
917
919
|
}
|
|
918
920
|
|
|
919
921
|
componentDidUpdate(prevProps) {
|
|
@@ -923,7 +925,7 @@ class DropdownCore extends React.Component {
|
|
|
923
925
|
|
|
924
926
|
if (prevProps.open !== open) {
|
|
925
927
|
this.updateEventListeners();
|
|
926
|
-
this.
|
|
928
|
+
this.maybeFocusInitialItem();
|
|
927
929
|
} else if (open) {
|
|
928
930
|
const {
|
|
929
931
|
itemRefs,
|
|
@@ -972,11 +974,16 @@ class DropdownCore extends React.Component {
|
|
|
972
974
|
}
|
|
973
975
|
}
|
|
974
976
|
|
|
975
|
-
|
|
977
|
+
maybeFocusInitialItem() {
|
|
976
978
|
const {
|
|
979
|
+
autoFocus,
|
|
977
980
|
open
|
|
978
981
|
} = this.props;
|
|
979
982
|
|
|
983
|
+
if (!autoFocus) {
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
|
|
980
987
|
if (open) {
|
|
981
988
|
this.resetFocusedIndex();
|
|
982
989
|
this.scheduleToFocusCurrentItem();
|
|
@@ -1268,6 +1275,8 @@ class DropdownCore extends React.Component {
|
|
|
1268
1275
|
|
|
1269
1276
|
DropdownCore.defaultProps = {
|
|
1270
1277
|
alignment: "left",
|
|
1278
|
+
autoFocus: true,
|
|
1279
|
+
enableTypeAhead: true,
|
|
1271
1280
|
labels: {
|
|
1272
1281
|
clearSearch: defaultLabels.clearSearch,
|
|
1273
1282
|
filter: defaultLabels.filter,
|
|
@@ -1811,7 +1820,7 @@ const _generateStyles = (light, placeholder) => {
|
|
|
1811
1820
|
return stateStyles[styleKey];
|
|
1812
1821
|
};
|
|
1813
1822
|
|
|
1814
|
-
const _excluded$1 = ["children", "disabled", "id", "light", "opener", "placeholder", "selectedValue", "testId", "alignment", "dropdownStyle", "isFilterable", "labels", "onChange", "onToggle", "opened", "style", "className"];
|
|
1823
|
+
const _excluded$1 = ["children", "disabled", "id", "light", "opener", "placeholder", "selectedValue", "testId", "alignment", "autoFocus", "dropdownStyle", "enableTypeAhead", "isFilterable", "labels", "onChange", "onToggle", "opened", "style", "className"];
|
|
1815
1824
|
class SingleSelect extends React.Component {
|
|
1816
1825
|
constructor(props) {
|
|
1817
1826
|
super(props);
|
|
@@ -1963,9 +1972,11 @@ class SingleSelect extends React.Component {
|
|
|
1963
1972
|
render() {
|
|
1964
1973
|
const {
|
|
1965
1974
|
alignment,
|
|
1975
|
+
autoFocus,
|
|
1966
1976
|
children,
|
|
1967
1977
|
className,
|
|
1968
1978
|
dropdownStyle,
|
|
1979
|
+
enableTypeAhead,
|
|
1969
1980
|
isFilterable,
|
|
1970
1981
|
labels,
|
|
1971
1982
|
light,
|
|
@@ -1981,6 +1992,8 @@ class SingleSelect extends React.Component {
|
|
|
1981
1992
|
role: "listbox",
|
|
1982
1993
|
selectionType: "single",
|
|
1983
1994
|
alignment: alignment,
|
|
1995
|
+
autoFocus: autoFocus,
|
|
1996
|
+
enableTypeAhead: enableTypeAhead,
|
|
1984
1997
|
dropdownStyle: [isFilterable && filterableDropdownStyle, selectDropdownStyle, dropdownStyle],
|
|
1985
1998
|
initialFocusedIndex: this.selectedIndex,
|
|
1986
1999
|
items: items,
|
|
@@ -2001,7 +2014,9 @@ class SingleSelect extends React.Component {
|
|
|
2001
2014
|
}
|
|
2002
2015
|
SingleSelect.defaultProps = {
|
|
2003
2016
|
alignment: "left",
|
|
2017
|
+
autoFocus: true,
|
|
2004
2018
|
disabled: false,
|
|
2019
|
+
enableTypeAhead: true,
|
|
2005
2020
|
light: false,
|
|
2006
2021
|
labels: {
|
|
2007
2022
|
clearSearch: defaultLabels.clearSearch,
|
package/dist/index.js
CHANGED
|
@@ -536,7 +536,8 @@ class OptionItem extends react__WEBPACK_IMPORTED_MODULE_1__["Component"] {
|
|
|
536
536
|
return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_1__["createElement"](ClickableBehavior, {
|
|
537
537
|
disabled: disabled,
|
|
538
538
|
onClick: this.handleClick,
|
|
539
|
-
role: role
|
|
539
|
+
role: role,
|
|
540
|
+
tabIndex: 0
|
|
540
541
|
}, (state, childrenProps) => {
|
|
541
542
|
const {
|
|
542
543
|
pressed,
|
|
@@ -819,13 +820,14 @@ class DropdownCore extends react__WEBPACK_IMPORTED_MODULE_0__["Component"] {
|
|
|
819
820
|
|
|
820
821
|
this.handleKeyDown = event => {
|
|
821
822
|
const {
|
|
823
|
+
enableTypeAhead,
|
|
822
824
|
onOpenChanged,
|
|
823
825
|
open,
|
|
824
826
|
searchText
|
|
825
827
|
} = this.props;
|
|
826
828
|
const keyCode = event.which || event.keyCode; // Listen for the keydown events if we are using ASCII characters.
|
|
827
829
|
|
|
828
|
-
if (Object(_util_helpers_js__WEBPACK_IMPORTED_MODULE_14__[/* getStringForKey */ "b"])(event.key)) {
|
|
830
|
+
if (enableTypeAhead && Object(_util_helpers_js__WEBPACK_IMPORTED_MODULE_14__[/* getStringForKey */ "b"])(event.key)) {
|
|
829
831
|
event.stopPropagation();
|
|
830
832
|
this.textSuggestion += event.key; // Trigger the filter logic only after the debounce is resolved.
|
|
831
833
|
|
|
@@ -1016,7 +1018,7 @@ class DropdownCore extends react__WEBPACK_IMPORTED_MODULE_0__["Component"] {
|
|
|
1016
1018
|
|
|
1017
1019
|
componentDidMount() {
|
|
1018
1020
|
this.updateEventListeners();
|
|
1019
|
-
this.
|
|
1021
|
+
this.maybeFocusInitialItem();
|
|
1020
1022
|
}
|
|
1021
1023
|
|
|
1022
1024
|
componentDidUpdate(prevProps) {
|
|
@@ -1026,7 +1028,7 @@ class DropdownCore extends react__WEBPACK_IMPORTED_MODULE_0__["Component"] {
|
|
|
1026
1028
|
|
|
1027
1029
|
if (prevProps.open !== open) {
|
|
1028
1030
|
this.updateEventListeners();
|
|
1029
|
-
this.
|
|
1031
|
+
this.maybeFocusInitialItem();
|
|
1030
1032
|
} // If the menu changed, but from open to open, figure out if we need
|
|
1031
1033
|
// to recalculate the focus somehow.
|
|
1032
1034
|
else if (open) {
|
|
@@ -1096,11 +1098,16 @@ class DropdownCore extends react__WEBPACK_IMPORTED_MODULE_0__["Component"] {
|
|
|
1096
1098
|
// to closed or vice versa
|
|
1097
1099
|
|
|
1098
1100
|
|
|
1099
|
-
|
|
1101
|
+
maybeFocusInitialItem() {
|
|
1100
1102
|
const {
|
|
1103
|
+
autoFocus,
|
|
1101
1104
|
open
|
|
1102
1105
|
} = this.props;
|
|
1103
1106
|
|
|
1107
|
+
if (!autoFocus) {
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1104
1111
|
if (open) {
|
|
1105
1112
|
this.resetFocusedIndex();
|
|
1106
1113
|
this.scheduleToFocusCurrentItem();
|
|
@@ -1455,6 +1462,8 @@ class DropdownCore extends react__WEBPACK_IMPORTED_MODULE_0__["Component"] {
|
|
|
1455
1462
|
|
|
1456
1463
|
DropdownCore.defaultProps = {
|
|
1457
1464
|
alignment: "left",
|
|
1465
|
+
autoFocus: true,
|
|
1466
|
+
enableTypeAhead: true,
|
|
1458
1467
|
labels: {
|
|
1459
1468
|
clearSearch: _util_constants_js__WEBPACK_IMPORTED_MODULE_12__[/* defaultLabels */ "d"].clearSearch,
|
|
1460
1469
|
filter: _util_constants_js__WEBPACK_IMPORTED_MODULE_12__[/* defaultLabels */ "d"].filter,
|
|
@@ -2286,7 +2295,9 @@ class SingleSelect extends react__WEBPACK_IMPORTED_MODULE_1__["Component"] {
|
|
|
2286
2295
|
|
|
2287
2296
|
/* eslint-disable no-unused-vars */
|
|
2288
2297
|
alignment,
|
|
2298
|
+
autoFocus,
|
|
2289
2299
|
dropdownStyle,
|
|
2300
|
+
enableTypeAhead,
|
|
2290
2301
|
isFilterable,
|
|
2291
2302
|
labels,
|
|
2292
2303
|
onChange,
|
|
@@ -2323,9 +2334,11 @@ class SingleSelect extends react__WEBPACK_IMPORTED_MODULE_1__["Component"] {
|
|
|
2323
2334
|
render() {
|
|
2324
2335
|
const {
|
|
2325
2336
|
alignment,
|
|
2337
|
+
autoFocus,
|
|
2326
2338
|
children,
|
|
2327
2339
|
className,
|
|
2328
2340
|
dropdownStyle,
|
|
2341
|
+
enableTypeAhead,
|
|
2329
2342
|
isFilterable,
|
|
2330
2343
|
labels,
|
|
2331
2344
|
light,
|
|
@@ -2341,6 +2354,8 @@ class SingleSelect extends react__WEBPACK_IMPORTED_MODULE_1__["Component"] {
|
|
|
2341
2354
|
role: "listbox",
|
|
2342
2355
|
selectionType: "single",
|
|
2343
2356
|
alignment: alignment,
|
|
2357
|
+
autoFocus: autoFocus,
|
|
2358
|
+
enableTypeAhead: enableTypeAhead,
|
|
2344
2359
|
dropdownStyle: [isFilterable && _util_constants_js__WEBPACK_IMPORTED_MODULE_6__[/* filterableDropdownStyle */ "e"], _util_constants_js__WEBPACK_IMPORTED_MODULE_6__[/* selectDropdownStyle */ "g"], dropdownStyle],
|
|
2345
2360
|
initialFocusedIndex: this.selectedIndex,
|
|
2346
2361
|
items: items,
|
|
@@ -2361,7 +2376,9 @@ class SingleSelect extends react__WEBPACK_IMPORTED_MODULE_1__["Component"] {
|
|
|
2361
2376
|
}
|
|
2362
2377
|
SingleSelect.defaultProps = {
|
|
2363
2378
|
alignment: "left",
|
|
2379
|
+
autoFocus: true,
|
|
2364
2380
|
disabled: false,
|
|
2381
|
+
enableTypeAhead: true,
|
|
2365
2382
|
light: false,
|
|
2366
2383
|
labels: {
|
|
2367
2384
|
clearSearch: _util_constants_js__WEBPACK_IMPORTED_MODULE_6__[/* defaultLabels */ "d"].clearSearch,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@khanacademy/wonder-blocks-dropdown",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.10.0",
|
|
4
4
|
"design": "v1",
|
|
5
5
|
"description": "Dropdown variants for Wonder Blocks.",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -16,13 +16,13 @@
|
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
18
|
"@babel/runtime": "^7.18.6",
|
|
19
|
-
"@khanacademy/wonder-blocks-clickable": "^2.
|
|
19
|
+
"@khanacademy/wonder-blocks-clickable": "^2.4.0",
|
|
20
20
|
"@khanacademy/wonder-blocks-color": "^1.2.0",
|
|
21
21
|
"@khanacademy/wonder-blocks-core": "^4.5.0",
|
|
22
22
|
"@khanacademy/wonder-blocks-icon": "^1.2.32",
|
|
23
23
|
"@khanacademy/wonder-blocks-layout": "^1.4.12",
|
|
24
|
-
"@khanacademy/wonder-blocks-modal": "^3.0.
|
|
25
|
-
"@khanacademy/wonder-blocks-search-field": "^1.0.
|
|
24
|
+
"@khanacademy/wonder-blocks-modal": "^3.0.1",
|
|
25
|
+
"@khanacademy/wonder-blocks-search-field": "^1.0.13",
|
|
26
26
|
"@khanacademy/wonder-blocks-spacing": "^3.0.5",
|
|
27
27
|
"@khanacademy/wonder-blocks-timing": "^2.1.0",
|
|
28
28
|
"@khanacademy/wonder-blocks-typography": "^1.1.34"
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"react-window": "^1.8.5"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
"@khanacademy/wonder-blocks-button": "^3.0.
|
|
41
|
+
"@khanacademy/wonder-blocks-button": "^3.0.7",
|
|
42
42
|
"wb-dev-build-settings": "^0.4.0"
|
|
43
43
|
}
|
|
44
44
|
}
|
|
@@ -5,6 +5,8 @@ import {StyleSheet} from "aphrodite";
|
|
|
5
5
|
import Button from "@khanacademy/wonder-blocks-button";
|
|
6
6
|
import Color from "@khanacademy/wonder-blocks-color";
|
|
7
7
|
import {View} from "@khanacademy/wonder-blocks-core";
|
|
8
|
+
import {TextField} from "@khanacademy/wonder-blocks-form";
|
|
9
|
+
import Icon from "@khanacademy/wonder-blocks-icon";
|
|
8
10
|
import {Strut} from "@khanacademy/wonder-blocks-layout";
|
|
9
11
|
import {OnePaneDialog, ModalLauncher} from "@khanacademy/wonder-blocks-modal";
|
|
10
12
|
import Spacing from "@khanacademy/wonder-blocks-spacing";
|
|
@@ -19,16 +21,23 @@ import {
|
|
|
19
21
|
} from "@khanacademy/wonder-blocks-dropdown";
|
|
20
22
|
|
|
21
23
|
import type {SingleSelectLabels} from "@khanacademy/wonder-blocks-dropdown";
|
|
24
|
+
import type {IconAsset} from "@khanacademy/wonder-blocks-icon";
|
|
22
25
|
|
|
23
26
|
import ComponentInfo from "../../../../../.storybook/components/component-info.js";
|
|
24
27
|
import {name, version} from "../../../package.json";
|
|
25
28
|
import singleSelectArgtypes from "./base-select.argtypes.js";
|
|
29
|
+
import {defaultLabels} from "../../util/constants.js";
|
|
26
30
|
|
|
27
31
|
export default {
|
|
28
32
|
title: "Dropdown / SingleSelect",
|
|
29
33
|
component: SingleSelect,
|
|
30
34
|
subcomponents: {OptionItem, SeparatorItem},
|
|
31
|
-
argTypes:
|
|
35
|
+
argTypes: {
|
|
36
|
+
...singleSelectArgtypes,
|
|
37
|
+
labels: {
|
|
38
|
+
defaultValue: defaultLabels,
|
|
39
|
+
},
|
|
40
|
+
},
|
|
32
41
|
args: {
|
|
33
42
|
isFilterable: true,
|
|
34
43
|
opened: false,
|
|
@@ -38,10 +47,8 @@ export default {
|
|
|
38
47
|
selectedValue: "",
|
|
39
48
|
},
|
|
40
49
|
decorators: [
|
|
41
|
-
(Story:
|
|
42
|
-
<View style={styles.example}>
|
|
43
|
-
<Story />
|
|
44
|
-
</View>
|
|
50
|
+
(Story: any): React.Element<typeof View> => (
|
|
51
|
+
<View style={styles.example}>{Story()}</View>
|
|
45
52
|
),
|
|
46
53
|
],
|
|
47
54
|
parameters: {
|
|
@@ -126,18 +133,23 @@ const styles = StyleSheet.create({
|
|
|
126
133
|
paddingRight: Spacing.medium_16,
|
|
127
134
|
paddingTop: Spacing.medium_16,
|
|
128
135
|
},
|
|
136
|
+
// AutoFocus
|
|
137
|
+
icon: {
|
|
138
|
+
position: "absolute",
|
|
139
|
+
right: Spacing.medium_16,
|
|
140
|
+
},
|
|
129
141
|
});
|
|
130
142
|
|
|
131
143
|
const items = [
|
|
132
|
-
<OptionItem label="Banana" value="banana" />,
|
|
133
|
-
<OptionItem label="Strawberry" value="strawberry" disabled />,
|
|
134
|
-
<OptionItem label="Pear" value="pear" />,
|
|
135
|
-
<OptionItem label="Orange" value="orange" />,
|
|
136
|
-
<OptionItem label="Watermelon" value="watermelon" />,
|
|
137
|
-
<OptionItem label="Apple" value="apple" />,
|
|
138
|
-
<OptionItem label="Grape" value="grape" />,
|
|
139
|
-
<OptionItem label="Lemon" value="lemon" />,
|
|
140
|
-
<OptionItem label="Mango" value="mango" />,
|
|
144
|
+
<OptionItem label="Banana" value="banana" key={0} />,
|
|
145
|
+
<OptionItem label="Strawberry" value="strawberry" disabled key={1} />,
|
|
146
|
+
<OptionItem label="Pear" value="pear" key={2} />,
|
|
147
|
+
<OptionItem label="Orange" value="orange" key={3} />,
|
|
148
|
+
<OptionItem label="Watermelon" value="watermelon" key={4} />,
|
|
149
|
+
<OptionItem label="Apple" value="apple" key={5} />,
|
|
150
|
+
<OptionItem label="Grape" value="grape" key={6} />,
|
|
151
|
+
<OptionItem label="Lemon" value="lemon" key={7} />,
|
|
152
|
+
<OptionItem label="Mango" value="mango" key={8} />,
|
|
141
153
|
];
|
|
142
154
|
|
|
143
155
|
const Template = (args) => {
|
|
@@ -516,3 +528,90 @@ CustomLabels.parameters = {
|
|
|
516
528
|
},
|
|
517
529
|
},
|
|
518
530
|
};
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Auto focus disabled
|
|
534
|
+
*/
|
|
535
|
+
const timeSlots = [
|
|
536
|
+
"12:00 AM",
|
|
537
|
+
"2:00 AM",
|
|
538
|
+
"4:00 AM",
|
|
539
|
+
"6:00 AM",
|
|
540
|
+
"8:00 AM",
|
|
541
|
+
"10:00 AM",
|
|
542
|
+
"12:00 PM",
|
|
543
|
+
"2:00 PM",
|
|
544
|
+
"4:00 PM",
|
|
545
|
+
"6:00 PM",
|
|
546
|
+
"8:00 PM",
|
|
547
|
+
"10:00 PM",
|
|
548
|
+
"11:59 PM",
|
|
549
|
+
];
|
|
550
|
+
|
|
551
|
+
const timeSlotOptions = timeSlots.map((timeSlot) => (
|
|
552
|
+
<OptionItem label={timeSlot} value={timeSlot} />
|
|
553
|
+
));
|
|
554
|
+
|
|
555
|
+
const clockIcon: IconAsset = {
|
|
556
|
+
small: `M0 8C0 3.58 3.58 0 7.99 0C12.42 0 16 3.58 16 8C16 12.42 12.42 16 7.99 16C3.58 16 0 12.42 0 8ZM1.6 8C1.6 11.54 4.46 14.4 8 14.4C11.54 14.4 14.4 11.54 14.4 8C14.4 4.46 11.54 1.6 8 1.6C4.46 1.6 1.6 4.46 1.6 8ZM7.2 4H8.4V8.2L12 10.34L11.4 11.32L7.2 8.8V4Z`,
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
export const AutoFocusDisabled: StoryComponentType = () => {
|
|
560
|
+
const textFieldRef = React.useRef(null);
|
|
561
|
+
const [value, setValue] = React.useState(null);
|
|
562
|
+
const [opened, setOpened] = React.useState(false);
|
|
563
|
+
|
|
564
|
+
return (
|
|
565
|
+
<View style={styles.wrapper}>
|
|
566
|
+
<SingleSelect
|
|
567
|
+
autoFocus={false}
|
|
568
|
+
enableTypeAhead={false}
|
|
569
|
+
onChange={setValue}
|
|
570
|
+
selectedValue={value}
|
|
571
|
+
opened={opened}
|
|
572
|
+
onToggle={setOpened}
|
|
573
|
+
placeholder="Choose a time"
|
|
574
|
+
opener={({focused, hovered, pressed, text}) => (
|
|
575
|
+
<View style={styles.row}>
|
|
576
|
+
<TextField
|
|
577
|
+
placeholder="Choose a time"
|
|
578
|
+
id="single-select-opener"
|
|
579
|
+
onChange={setValue}
|
|
580
|
+
value={value ?? ""}
|
|
581
|
+
ref={textFieldRef}
|
|
582
|
+
autoComplete="off"
|
|
583
|
+
style={styles.fullBleed}
|
|
584
|
+
/>
|
|
585
|
+
<Icon
|
|
586
|
+
color={Color.blue}
|
|
587
|
+
icon={clockIcon}
|
|
588
|
+
size="small"
|
|
589
|
+
style={styles.icon}
|
|
590
|
+
/>
|
|
591
|
+
</View>
|
|
592
|
+
)}
|
|
593
|
+
>
|
|
594
|
+
{timeSlotOptions}
|
|
595
|
+
</SingleSelect>
|
|
596
|
+
</View>
|
|
597
|
+
);
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
AutoFocusDisabled.parameters = {
|
|
601
|
+
docs: {
|
|
602
|
+
description: {
|
|
603
|
+
story:
|
|
604
|
+
`This example illustrates how you can disable the auto focus
|
|
605
|
+
of the \`SingleSelect\` component. Note that for this example,
|
|
606
|
+
we are using a \`TextField\` component as a custom opener to
|
|
607
|
+
ilustrate how the focus remains on the opener.\n\n` +
|
|
608
|
+
`**Note:** We also disabled the \`enableTypeAhead\` prop to be
|
|
609
|
+
able to use the textbox properly.`,
|
|
610
|
+
},
|
|
611
|
+
},
|
|
612
|
+
chromatic: {
|
|
613
|
+
// we don't need screenshots because this story only tests focus +
|
|
614
|
+
// keyboard behavior.
|
|
615
|
+
disableSnapshot: true,
|
|
616
|
+
},
|
|
617
|
+
};
|
|
@@ -97,6 +97,31 @@ describe("SingleSelect", () => {
|
|
|
97
97
|
// Assert
|
|
98
98
|
expect(onChange).toHaveBeenCalledWith("1"); // value
|
|
99
99
|
});
|
|
100
|
+
|
|
101
|
+
it("should not focus in the first item if autoFocus is disabled", async () => {
|
|
102
|
+
// Arrange
|
|
103
|
+
render(
|
|
104
|
+
<SingleSelect
|
|
105
|
+
autoFocus={false}
|
|
106
|
+
onChange={onChange}
|
|
107
|
+
placeholder="Choose"
|
|
108
|
+
opener={() => <input type="text" />}
|
|
109
|
+
>
|
|
110
|
+
<OptionItem label="item 1" value="1" />
|
|
111
|
+
<OptionItem label="item 2" value="2" />
|
|
112
|
+
<OptionItem label="item 3" value="3" />
|
|
113
|
+
</SingleSelect>,
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
// Act
|
|
117
|
+
userEvent.click(screen.getByRole("textbox"));
|
|
118
|
+
|
|
119
|
+
// wait for the dropdown to open
|
|
120
|
+
await screen.findByRole("listbox");
|
|
121
|
+
|
|
122
|
+
// Assert
|
|
123
|
+
expect(screen.getByRole("textbox")).toHaveFocus();
|
|
124
|
+
});
|
|
100
125
|
});
|
|
101
126
|
|
|
102
127
|
describe("keyboard", () => {
|
|
@@ -183,6 +208,32 @@ describe("SingleSelect", () => {
|
|
|
183
208
|
expect(screen.queryByRole("listbox")).not.toBeInTheDocument();
|
|
184
209
|
});
|
|
185
210
|
|
|
211
|
+
it("should NOT find/select an item using the keyboard if enableTypeAhead is set false", () => {
|
|
212
|
+
// Arrange
|
|
213
|
+
render(
|
|
214
|
+
<SingleSelect
|
|
215
|
+
onChange={onChange}
|
|
216
|
+
placeholder="Choose"
|
|
217
|
+
enableTypeAhead={false}
|
|
218
|
+
>
|
|
219
|
+
<OptionItem label="apple" value="apple" />
|
|
220
|
+
<OptionItem label="orange" value="orange" />
|
|
221
|
+
<OptionItem label="pear" value="pear" />
|
|
222
|
+
</SingleSelect>,
|
|
223
|
+
);
|
|
224
|
+
userEvent.tab();
|
|
225
|
+
|
|
226
|
+
// Act
|
|
227
|
+
|
|
228
|
+
// Try to find first occurrence but it should not be found
|
|
229
|
+
// as we have disabled type ahead.
|
|
230
|
+
userEvent.keyboard("or");
|
|
231
|
+
jest.advanceTimersByTime(501);
|
|
232
|
+
|
|
233
|
+
// Assert
|
|
234
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
235
|
+
});
|
|
236
|
+
|
|
186
237
|
it("should dismiss the dropdown when pressing {escape}", () => {
|
|
187
238
|
// Arrange
|
|
188
239
|
render(uncontrolledSingleSelect);
|
|
@@ -72,18 +72,38 @@ type Labels = {|
|
|
|
72
72
|
// values to the parent components that are instantiating this component
|
|
73
73
|
// @see https://flow.org/en/docs/react/hoc/#toc-exporting-wrapped-components
|
|
74
74
|
type DefaultProps = {|
|
|
75
|
-
/**
|
|
76
|
-
* An index that represents the index of the focused element when the menu
|
|
77
|
-
* is opened.
|
|
78
|
-
*/
|
|
79
|
-
initialFocusedIndex?: number,
|
|
80
|
-
|
|
81
75
|
/**
|
|
82
76
|
* Whether this menu should be left-aligned or right-aligned with the
|
|
83
77
|
* opener component. Defaults to left-aligned.
|
|
84
78
|
*/
|
|
85
79
|
alignment: "left" | "right",
|
|
86
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Whether to auto focus an option. Defaults to true.
|
|
83
|
+
*/
|
|
84
|
+
autoFocus: boolean,
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Whether to enable the type-ahead suggestions feature. Defaults to true.
|
|
88
|
+
*
|
|
89
|
+
* This feature allows to navigate the listbox using the keyboard.
|
|
90
|
+
* - Type a character: focus moves to the next item with a name that starts
|
|
91
|
+
* with the typed character.
|
|
92
|
+
* - Type multiple characters in rapid succession: focus moves to the next
|
|
93
|
+
* item with a name that starts with the string of characters typed.
|
|
94
|
+
*
|
|
95
|
+
* **NOTE:** Type-ahead is recommended for all listboxes, but there might be
|
|
96
|
+
* some cases where it's not desirable (for example when using a `TextField`
|
|
97
|
+
* as the opener element).
|
|
98
|
+
*/
|
|
99
|
+
enableTypeAhead: boolean,
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* An index that represents the index of the focused element when the menu
|
|
103
|
+
* is opened.
|
|
104
|
+
*/
|
|
105
|
+
initialFocusedIndex?: number,
|
|
106
|
+
|
|
87
107
|
/**
|
|
88
108
|
* The object containing the custom labels used inside this component.
|
|
89
109
|
*/
|
|
@@ -106,24 +126,13 @@ type ItemAriaRole = "option" | "menuitem";
|
|
|
106
126
|
|
|
107
127
|
type Props = {|
|
|
108
128
|
...DefaultProps,
|
|
109
|
-
/**
|
|
110
|
-
* Items for the menu.
|
|
111
|
-
*/
|
|
112
|
-
items: Array<DropdownItem>,
|
|
113
129
|
|
|
114
|
-
|
|
115
|
-
* An optional handler to set the searchText of the parent. When this and
|
|
116
|
-
* the searchText exist, SearchField will be displayed at the top of the
|
|
117
|
-
* dropdown body.
|
|
118
|
-
*/
|
|
119
|
-
onSearchTextChanged?: ?(searchText: string) => mixed,
|
|
130
|
+
// Required props
|
|
120
131
|
|
|
121
132
|
/**
|
|
122
|
-
*
|
|
123
|
-
* and the onSearchTextChanged exist, SearchField will be displayed at the
|
|
124
|
-
* top of the dropdown body.
|
|
133
|
+
* Items for the menu.
|
|
125
134
|
*/
|
|
126
|
-
|
|
135
|
+
items: Array<DropdownItem>,
|
|
127
136
|
|
|
128
137
|
/**
|
|
129
138
|
* Callback for when the menu is opened or closed. Parameter is whether
|
|
@@ -146,6 +155,27 @@ type Props = {|
|
|
|
146
155
|
*/
|
|
147
156
|
openerElement: ?HTMLElement,
|
|
148
157
|
|
|
158
|
+
/**
|
|
159
|
+
* The aria "role" applied to the dropdown container.
|
|
160
|
+
*/
|
|
161
|
+
role: DropdownAriaRole,
|
|
162
|
+
|
|
163
|
+
// Optional props
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* An optional handler to set the searchText of the parent. When this and
|
|
167
|
+
* the searchText exist, SearchField will be displayed at the top of the
|
|
168
|
+
* dropdown body.
|
|
169
|
+
*/
|
|
170
|
+
onSearchTextChanged?: ?(searchText: string) => mixed,
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* An optional string that the user entered to search the items. When this
|
|
174
|
+
* and the onSearchTextChanged exist, SearchField will be displayed at the
|
|
175
|
+
* top of the dropdown body.
|
|
176
|
+
*/
|
|
177
|
+
searchText?: ?string,
|
|
178
|
+
|
|
149
179
|
/**
|
|
150
180
|
* Styling specific to the dropdown component that isn't part of the opener,
|
|
151
181
|
* passed by the specific implementation of the dropdown menu,
|
|
@@ -162,11 +192,6 @@ type Props = {|
|
|
|
162
192
|
*/
|
|
163
193
|
className?: string,
|
|
164
194
|
|
|
165
|
-
/**
|
|
166
|
-
* The aria "role" applied to the dropdown container.
|
|
167
|
-
*/
|
|
168
|
-
role: DropdownAriaRole,
|
|
169
|
-
|
|
170
195
|
/**
|
|
171
196
|
* When this is true, the dropdown body shows a search text input at the
|
|
172
197
|
* top. The items will be filtered by the input.
|
|
@@ -247,6 +272,8 @@ class DropdownCore extends React.Component<Props, State> {
|
|
|
247
272
|
|
|
248
273
|
static defaultProps: DefaultProps = {
|
|
249
274
|
alignment: "left",
|
|
275
|
+
autoFocus: true,
|
|
276
|
+
enableTypeAhead: true,
|
|
250
277
|
labels: {
|
|
251
278
|
clearSearch: defaultLabels.clearSearch,
|
|
252
279
|
filter: defaultLabels.filter,
|
|
@@ -320,7 +347,7 @@ class DropdownCore extends React.Component<Props, State> {
|
|
|
320
347
|
|
|
321
348
|
componentDidMount() {
|
|
322
349
|
this.updateEventListeners();
|
|
323
|
-
this.
|
|
350
|
+
this.maybeFocusInitialItem();
|
|
324
351
|
}
|
|
325
352
|
|
|
326
353
|
componentDidUpdate(prevProps: Props) {
|
|
@@ -328,7 +355,7 @@ class DropdownCore extends React.Component<Props, State> {
|
|
|
328
355
|
|
|
329
356
|
if (prevProps.open !== open) {
|
|
330
357
|
this.updateEventListeners();
|
|
331
|
-
this.
|
|
358
|
+
this.maybeFocusInitialItem();
|
|
332
359
|
}
|
|
333
360
|
// If the menu changed, but from open to open, figure out if we need
|
|
334
361
|
// to recalculate the focus somehow.
|
|
@@ -396,8 +423,12 @@ class DropdownCore extends React.Component<Props, State> {
|
|
|
396
423
|
|
|
397
424
|
// Figure out focus states for the dropdown after it has changed from open
|
|
398
425
|
// to closed or vice versa
|
|
399
|
-
|
|
400
|
-
const {open} = this.props;
|
|
426
|
+
maybeFocusInitialItem() {
|
|
427
|
+
const {autoFocus, open} = this.props;
|
|
428
|
+
|
|
429
|
+
if (!autoFocus) {
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
401
432
|
|
|
402
433
|
if (open) {
|
|
403
434
|
this.resetFocusedIndex();
|
|
@@ -544,11 +575,11 @@ class DropdownCore extends React.Component<Props, State> {
|
|
|
544
575
|
}
|
|
545
576
|
|
|
546
577
|
handleKeyDown: (event: SyntheticKeyboardEvent<>) => void = (event) => {
|
|
547
|
-
const {onOpenChanged, open, searchText} = this.props;
|
|
578
|
+
const {enableTypeAhead, onOpenChanged, open, searchText} = this.props;
|
|
548
579
|
const keyCode = event.which || event.keyCode;
|
|
549
580
|
|
|
550
581
|
// Listen for the keydown events if we are using ASCII characters.
|
|
551
|
-
if (getStringForKey(event.key)) {
|
|
582
|
+
if (enableTypeAhead && getStringForKey(event.key)) {
|
|
552
583
|
event.stopPropagation();
|
|
553
584
|
this.textSuggestion += event.key;
|
|
554
585
|
// Trigger the filter logic only after the debounce is resolved.
|
|
@@ -39,8 +39,54 @@ export type SingleSelectLabels = {|
|
|
|
39
39
|
someResults: (numOptions: number) => string,
|
|
40
40
|
|};
|
|
41
41
|
|
|
42
|
+
type DefaultProps = {|
|
|
43
|
+
/**
|
|
44
|
+
* Whether this dropdown should be left-aligned or right-aligned with the
|
|
45
|
+
* opener component. Defaults to left-aligned.
|
|
46
|
+
*/
|
|
47
|
+
alignment: "left" | "right",
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Whether to auto focus an option. Defaults to true.
|
|
51
|
+
*/
|
|
52
|
+
autoFocus: boolean,
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Whether to enable the type-ahead suggestions feature. Defaults to true.
|
|
56
|
+
*
|
|
57
|
+
* This feature allows to navigate the listbox using the keyboard.
|
|
58
|
+
* - Type a character: focus moves to the next item with a name that starts
|
|
59
|
+
* with the typed character.
|
|
60
|
+
* - Type multiple characters in rapid succession: focus moves to the next
|
|
61
|
+
* item with a name that starts with the string of characters typed.
|
|
62
|
+
*
|
|
63
|
+
* **NOTE:** Type-ahead is recommended for all listboxes, but there might be
|
|
64
|
+
* some cases where it's not desirable (for example when using a `TextField`
|
|
65
|
+
* as the opener element).
|
|
66
|
+
*/
|
|
67
|
+
enableTypeAhead: boolean,
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Whether this component is disabled. A disabled dropdown may not be opened
|
|
71
|
+
* and does not support interaction. Defaults to false.
|
|
72
|
+
*/
|
|
73
|
+
disabled: boolean,
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Whether to display the "light" version of this component instead, for
|
|
77
|
+
* use when the component is used on a dark background.
|
|
78
|
+
*/
|
|
79
|
+
light: boolean,
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The object containing the custom labels used inside this component.
|
|
83
|
+
*/
|
|
84
|
+
labels: SingleSelectLabels,
|
|
85
|
+
|};
|
|
86
|
+
|
|
42
87
|
type Props = {|
|
|
43
88
|
...AriaProps,
|
|
89
|
+
...DefaultProps,
|
|
44
90
|
|
|
45
91
|
/**
|
|
46
92
|
* The items in this select.
|
|
@@ -81,24 +127,6 @@ type Props = {|
|
|
|
81
127
|
*/
|
|
82
128
|
selectedValue?: ?string,
|
|
83
129
|
|
|
84
|
-
/**
|
|
85
|
-
* Whether this dropdown should be left-aligned or right-aligned with the
|
|
86
|
-
* opener component. Defaults to left-aligned.
|
|
87
|
-
*/
|
|
88
|
-
alignment: "left" | "right",
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Whether this component is disabled. A disabled dropdown may not be opened
|
|
92
|
-
* and does not support interaction. Defaults to false.
|
|
93
|
-
*/
|
|
94
|
-
disabled: boolean,
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Whether to display the "light" version of this component instead, for
|
|
98
|
-
* use when the component is used on a dark background.
|
|
99
|
-
*/
|
|
100
|
-
light: boolean,
|
|
101
|
-
|
|
102
130
|
/**
|
|
103
131
|
* Optional styling to add to the opener component wrapper.
|
|
104
132
|
*/
|
|
@@ -131,11 +159,6 @@ type Props = {|
|
|
|
131
159
|
* top. The items will be filtered by the input.
|
|
132
160
|
*/
|
|
133
161
|
isFilterable?: boolean,
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* The object containing the custom labels used inside this component.
|
|
137
|
-
*/
|
|
138
|
-
labels: SingleSelectLabels,
|
|
139
162
|
|};
|
|
140
163
|
|
|
141
164
|
type State = {|
|
|
@@ -157,13 +180,6 @@ type State = {|
|
|
|
157
180
|
openerElement: ?HTMLElement,
|
|
158
181
|
|};
|
|
159
182
|
|
|
160
|
-
type DefaultProps = {|
|
|
161
|
-
alignment: Props["alignment"],
|
|
162
|
-
disabled: Props["disabled"],
|
|
163
|
-
light: Props["light"],
|
|
164
|
-
labels: Props["labels"],
|
|
165
|
-
|};
|
|
166
|
-
|
|
167
183
|
/**
|
|
168
184
|
* The single select allows the selection of one item. Clients are responsible
|
|
169
185
|
* for keeping track of the selected item in the select.
|
|
@@ -194,7 +210,9 @@ export default class SingleSelect extends React.Component<Props, State> {
|
|
|
194
210
|
|
|
195
211
|
static defaultProps: DefaultProps = {
|
|
196
212
|
alignment: "left",
|
|
213
|
+
autoFocus: true,
|
|
197
214
|
disabled: false,
|
|
215
|
+
enableTypeAhead: true,
|
|
198
216
|
light: false,
|
|
199
217
|
labels: {
|
|
200
218
|
clearSearch: defaultLabels.clearSearch,
|
|
@@ -351,7 +369,9 @@ export default class SingleSelect extends React.Component<Props, State> {
|
|
|
351
369
|
// passing them down to the opener as part of sharedProps
|
|
352
370
|
/* eslint-disable no-unused-vars */
|
|
353
371
|
alignment,
|
|
372
|
+
autoFocus,
|
|
354
373
|
dropdownStyle,
|
|
374
|
+
enableTypeAhead,
|
|
355
375
|
isFilterable,
|
|
356
376
|
labels,
|
|
357
377
|
onChange,
|
|
@@ -400,9 +420,11 @@ export default class SingleSelect extends React.Component<Props, State> {
|
|
|
400
420
|
render(): React.Node {
|
|
401
421
|
const {
|
|
402
422
|
alignment,
|
|
423
|
+
autoFocus,
|
|
403
424
|
children,
|
|
404
425
|
className,
|
|
405
426
|
dropdownStyle,
|
|
427
|
+
enableTypeAhead,
|
|
406
428
|
isFilterable,
|
|
407
429
|
labels,
|
|
408
430
|
light,
|
|
@@ -418,6 +440,8 @@ export default class SingleSelect extends React.Component<Props, State> {
|
|
|
418
440
|
role="listbox"
|
|
419
441
|
selectionType="single"
|
|
420
442
|
alignment={alignment}
|
|
443
|
+
autoFocus={autoFocus}
|
|
444
|
+
enableTypeAhead={enableTypeAhead}
|
|
421
445
|
dropdownStyle={[
|
|
422
446
|
isFilterable && filterableDropdownStyle,
|
|
423
447
|
selectDropdownStyle,
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
// @flow
|
|
2
|
-
export default {
|
|
3
|
-
alignment: {
|
|
4
|
-
table: {
|
|
5
|
-
category: "Layout",
|
|
6
|
-
},
|
|
7
|
-
},
|
|
8
|
-
disabled: {
|
|
9
|
-
table: {
|
|
10
|
-
category: "States",
|
|
11
|
-
},
|
|
12
|
-
},
|
|
13
|
-
isFilterable: {
|
|
14
|
-
table: {
|
|
15
|
-
category: "States",
|
|
16
|
-
},
|
|
17
|
-
},
|
|
18
|
-
light: {
|
|
19
|
-
table: {
|
|
20
|
-
category: "States",
|
|
21
|
-
},
|
|
22
|
-
},
|
|
23
|
-
opened: {
|
|
24
|
-
control: "boolean",
|
|
25
|
-
table: {
|
|
26
|
-
category: "States",
|
|
27
|
-
},
|
|
28
|
-
},
|
|
29
|
-
onToggle: {
|
|
30
|
-
table: {
|
|
31
|
-
category: "Events",
|
|
32
|
-
},
|
|
33
|
-
},
|
|
34
|
-
onChange: {
|
|
35
|
-
table: {
|
|
36
|
-
category: "Events",
|
|
37
|
-
},
|
|
38
|
-
},
|
|
39
|
-
dropdownStyle: {
|
|
40
|
-
table: {
|
|
41
|
-
category: "Styling",
|
|
42
|
-
},
|
|
43
|
-
},
|
|
44
|
-
style: {
|
|
45
|
-
table: {
|
|
46
|
-
category: "Styling",
|
|
47
|
-
},
|
|
48
|
-
},
|
|
49
|
-
className: {
|
|
50
|
-
table: {
|
|
51
|
-
category: "Styling",
|
|
52
|
-
},
|
|
53
|
-
},
|
|
54
|
-
};
|