@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 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.initialFocusItem();
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.initialFocusItem();
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
- initialFocusItem() {
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.initialFocusItem();
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.initialFocusItem();
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
- initialFocusItem() {
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.9.4",
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.3.3",
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.0",
25
- "@khanacademy/wonder-blocks-search-field": "^1.0.12",
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.6",
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: singleSelectArgtypes,
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: StoryComponentType): React.Element<typeof View> => (
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
- * An optional string that the user entered to search the items. When this
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
- searchText?: ?string,
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.initialFocusItem();
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.initialFocusItem();
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
- initialFocusItem() {
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.
@@ -144,6 +144,7 @@ export default class OptionItem extends React.Component<OptionProps> {
144
144
  disabled={disabled}
145
145
  onClick={this.handleClick}
146
146
  role={role}
147
+ tabIndex={0}
147
148
  >
148
149
  {(state, childrenProps) => {
149
150
  const {pressed, hovered, focused} = state;
@@ -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
- };