@leafygreen-ui/combobox 1.0.2 → 1.2.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +2 -2
  3. package/dist/Chip.d.ts.map +1 -1
  4. package/dist/Combobox.d.ts +7 -1
  5. package/dist/Combobox.d.ts.map +1 -1
  6. package/dist/Combobox.styles.d.ts +7 -3
  7. package/dist/Combobox.styles.d.ts.map +1 -1
  8. package/dist/Combobox.types.d.ts +33 -6
  9. package/dist/Combobox.types.d.ts.map +1 -1
  10. package/dist/ComboboxContext.d.ts +1 -1
  11. package/dist/ComboboxContext.d.ts.map +1 -1
  12. package/dist/ComboboxOption.d.ts.map +1 -1
  13. package/dist/ComboboxTestUtils.d.ts +3 -4
  14. package/dist/ComboboxTestUtils.d.ts.map +1 -1
  15. package/dist/esm/index.js +1 -1
  16. package/dist/esm/index.js.map +1 -1
  17. package/dist/index.js +1 -1
  18. package/dist/index.js.map +1 -1
  19. package/dist/utils/OptionObjectUtils.d.ts +5 -0
  20. package/dist/utils/OptionObjectUtils.d.ts.map +1 -0
  21. package/dist/utils/flattenChildren.d.ts +11 -0
  22. package/dist/utils/flattenChildren.d.ts.map +1 -0
  23. package/dist/utils/getNameAndValue.d.ts +14 -0
  24. package/dist/utils/getNameAndValue.d.ts.map +1 -0
  25. package/dist/utils/index.d.ts +5 -0
  26. package/dist/utils/index.d.ts.map +1 -0
  27. package/dist/utils/wrapJSX.d.ts +14 -0
  28. package/dist/utils/wrapJSX.d.ts.map +1 -0
  29. package/package.json +22 -12
  30. package/src/Chip.tsx +16 -9
  31. package/src/Combobox.spec.tsx +336 -164
  32. package/src/Combobox.story.tsx +274 -248
  33. package/src/Combobox.styles.ts +94 -24
  34. package/src/Combobox.tsx +456 -279
  35. package/src/Combobox.types.ts +46 -8
  36. package/src/ComboboxContext.tsx +2 -2
  37. package/src/ComboboxOption.tsx +36 -11
  38. package/src/ComboboxTestUtils.tsx +22 -8
  39. package/src/utils/ComboboxUtils.spec.tsx +227 -0
  40. package/src/utils/OptionObjectUtils.ts +26 -0
  41. package/src/utils/flattenChildren.tsx +47 -0
  42. package/src/utils/getNameAndValue.ts +23 -0
  43. package/src/utils/index.ts +8 -0
  44. package/src/utils/wrapJSX.tsx +54 -0
  45. package/tsconfig.json +3 -0
  46. package/tsconfig.tsbuildinfo +1 -3977
  47. package/dist/util.d.ts +0 -53
  48. package/dist/util.d.ts.map +0 -1
  49. package/src/util.tsx +0 -117
@@ -5,8 +5,28 @@ import { Either } from '@leafygreen-ui/lib';
5
5
  * Prop Enums & Types
6
6
  */
7
7
 
8
+ export const ComboboxElement = {
9
+ Input: 'Input',
10
+ ClearButton: 'ClearButton',
11
+ FirstChip: 'FirstChip',
12
+ LastChip: 'LastChip',
13
+ MiddleChip: 'MiddleChip',
14
+ Combobox: 'Combobox',
15
+ Menu: 'Menu',
16
+ } as const;
17
+ export type ComboboxElement =
18
+ typeof ComboboxElement[keyof typeof ComboboxElement];
19
+
20
+ /**
21
+ * Prop types
22
+ */
23
+
8
24
  export const ComboboxSize = {
9
- default: 'default',
25
+ // TODO: add XSmall & Small variants after the refresh
26
+ // XSmall: 'xsmall',
27
+ // Small: 'small',
28
+ Default: 'default',
29
+ Large: 'large',
10
30
  } as const;
11
31
  export type ComboboxSize = typeof ComboboxSize[keyof typeof ComboboxSize];
12
32
 
@@ -16,12 +36,13 @@ export const TrunctationLocation = {
16
36
  end: 'end',
17
37
  none: 'none',
18
38
  } as const;
19
- export type TrunctationLocation = typeof TrunctationLocation[keyof typeof TrunctationLocation];
39
+ export type TrunctationLocation =
40
+ typeof TrunctationLocation[keyof typeof TrunctationLocation];
20
41
 
21
42
  export const Overflow = {
22
43
  expandY: 'expand-y',
23
44
  expandX: 'expand-x',
24
- scrollY: 'scroll-x',
45
+ scrollX: 'scroll-x',
25
46
  } as const;
26
47
  export type Overflow = typeof Overflow[keyof typeof Overflow];
27
48
 
@@ -55,7 +76,7 @@ export function getNullSelection<M extends boolean>(
55
76
  multiselect: M,
56
77
  ): SelectValueType<M> {
57
78
  if (multiselect) {
58
- return ([] as Array<string>) as SelectValueType<M>;
79
+ return [] as Array<string> as SelectValueType<M>;
59
80
  } else {
60
81
  return null as SelectValueType<M>;
61
82
  }
@@ -73,8 +94,8 @@ export interface ComboboxMultiselectProps<M extends boolean> {
73
94
  multiselect?: M;
74
95
  /**
75
96
  * The initial selection.
76
- * Must be a string for a single-select, or an array of strings for multiselect.
77
- * Changing the initialValue after initial render will not change the selection.
97
+ * Must be a string (or array of strings) that matches the `value` prop of a `ComboboxOption`.
98
+ * Changing the `initialValue` after initial render will not change the selection.
78
99
  */
79
100
  initialValue?: SelectValueType<M>;
80
101
  /**
@@ -84,10 +105,12 @@ export interface ComboboxMultiselectProps<M extends boolean> {
84
105
  onChange?: onChangeType<M>;
85
106
  /**
86
107
  * The controlled value of the Combobox.
87
- * Must be a string for a single-select, or an array of strings for multiselect.
88
- * Changing value after initial render will affect the selection.
108
+ * Must be a string (or array of strings) that matches the `value` prop of a `ComboboxOption`.
109
+ * Changing `value` after initial render _will_ affect the selection.
110
+ * `value` will always take precedence over `initialValue` if both are provided.
89
111
  */
90
112
  value?: SelectValueType<M>;
113
+
91
114
  /**
92
115
  * Defines the overflow behavior of a multiselect combobox.
93
116
  *
@@ -248,6 +271,7 @@ export type ComboboxProps<M extends boolean> = Either<
248
271
  /**
249
272
  * Combobox Option Props
250
273
  */
274
+
251
275
  interface BaseComboboxOptionProps {
252
276
  /**
253
277
  * The internal value of the option. Used as the identifier in Combobox `initialValue`, value and filteredOptions.
@@ -266,6 +290,12 @@ interface BaseComboboxOptionProps {
266
290
  */
267
291
  glyph?: ReactElement;
268
292
 
293
+ /**
294
+ * Defines whether the option is disabled.
295
+ * Node: disabled options are still rendered in the menu, but not selectable.
296
+ */
297
+ disabled?: boolean;
298
+
269
299
  /**
270
300
  * Styling Prop
271
301
  */
@@ -277,12 +307,20 @@ export type ComboboxOptionProps = Either<
277
307
  'value' | 'displayName'
278
308
  >;
279
309
 
310
+ export interface OptionObject {
311
+ value: string;
312
+ displayName: string;
313
+ isDisabled: boolean;
314
+ hasGlyph?: boolean;
315
+ }
316
+
280
317
  export interface InternalComboboxOptionProps {
281
318
  value: string;
282
319
  displayName: string;
283
320
  isSelected: boolean;
284
321
  isFocused: boolean;
285
322
  setSelected: () => void;
323
+ disabled?: boolean;
286
324
  glyph?: ReactElement;
287
325
  className?: string;
288
326
  index: number;
@@ -4,7 +4,7 @@ import { ComboboxSize, TrunctationLocation } from './Combobox.types';
4
4
  interface ComboboxData {
5
5
  multiselect: boolean;
6
6
  darkMode: boolean;
7
- size: keyof typeof ComboboxSize;
7
+ size: ComboboxSize;
8
8
  withIcons: boolean;
9
9
  disabled: boolean;
10
10
  chipTruncationLocation?: TrunctationLocation;
@@ -15,7 +15,7 @@ interface ComboboxData {
15
15
  export const ComboboxContext = createContext<ComboboxData>({
16
16
  multiselect: false,
17
17
  darkMode: false,
18
- size: 'default',
18
+ size: ComboboxSize.Default,
19
19
  withIcons: false,
20
20
  disabled: false,
21
21
  });
@@ -10,13 +10,14 @@ import {
10
10
  InternalComboboxOptionProps,
11
11
  } from './Combobox.types';
12
12
  import { ComboboxContext } from './ComboboxContext';
13
- import { wrapJSX } from './util';
13
+ import { wrapJSX } from './utils';
14
+ import { fontFamilies } from '@leafygreen-ui/tokens';
14
15
 
15
16
  /**
16
17
  * Styles
17
18
  */
18
19
 
19
- const comboboxOptionStyle = () => css`
20
+ const comboboxOptionStyle = css`
20
21
  position: relative;
21
22
  display: flex;
22
23
  align-items: center;
@@ -25,10 +26,12 @@ const comboboxOptionStyle = () => css`
25
26
  color: inherit;
26
27
  cursor: pointer;
27
28
  overflow: hidden;
29
+ font-family: ${fontFamilies.legacy};
28
30
  font-size: var(--lg-combobox-item-font-size);
29
31
  line-height: var(--lg-combobox-item-line-height);
30
32
  padding: var(--lg-combobox-item-padding-y) var(--lg-combobox-item-padding-x);
31
33
 
34
+ // Left wedge
32
35
  &:before {
33
36
  content: '';
34
37
  position: absolute;
@@ -58,11 +61,21 @@ const comboboxOptionStyle = () => css`
58
61
  }
59
62
  `;
60
63
 
64
+ const comboboxOptionDisabledStyle = css`
65
+ cursor: not-allowed;
66
+ color: ${uiColors.gray.base};
67
+
68
+ &:hover {
69
+ background-color: inherit;
70
+ }
71
+ `;
72
+
61
73
  const flexSpan = css`
62
74
  display: inline-flex;
63
75
  gap: 8px;
64
76
  justify-content: start;
65
77
  align-items: inherit;
78
+ overflow-wrap: anywhere;
66
79
  `;
67
80
 
68
81
  const disallowPointer = css`
@@ -72,10 +85,10 @@ const disallowPointer = css`
72
85
  const displayNameStyle = (isSelected: boolean) => css`
73
86
  font-weight: ${isSelected ? 'bold' : 'normal'};
74
87
  `;
88
+
75
89
  /**
76
90
  * Component
77
91
  */
78
-
79
92
  const InternalComboboxOption = React.forwardRef<
80
93
  HTMLLIElement,
81
94
  InternalComboboxOptionProps
@@ -86,31 +99,35 @@ const InternalComboboxOption = React.forwardRef<
86
99
  glyph,
87
100
  isSelected,
88
101
  isFocused,
102
+ disabled,
89
103
  setSelected,
90
104
  className,
91
105
  }: InternalComboboxOptionProps,
92
106
  forwardedRef,
93
107
  ) => {
94
- const { multiselect, darkMode, withIcons, inputValue } = useContext(
95
- ComboboxContext,
96
- );
108
+ const { multiselect, darkMode, withIcons, inputValue } =
109
+ useContext(ComboboxContext);
97
110
  const optionTextId = useIdAllocator({ prefix: 'combobox-option-text' });
98
111
  const optionRef = useForwardedRef(forwardedRef, null);
99
112
 
100
113
  const handleOptionClick = useCallback(
101
114
  (e: React.SyntheticEvent) => {
115
+ // stopPropagation will not stop the keyDown event (only click)
116
+ // since the option is never `focused`, only `aria-selected`
117
+ // the keyDown event does not actually fire on the option element
102
118
  e.stopPropagation();
103
119
 
104
120
  // If user clicked on the option, or the checkbox
105
121
  // Ignore extra click events on the checkbox wrapper
106
122
  if (
107
- e.target === optionRef.current ||
108
- (e.target as Element).tagName === 'INPUT'
123
+ !disabled &&
124
+ (e.target === optionRef.current ||
125
+ (e.target as Element).tagName === 'INPUT')
109
126
  ) {
110
127
  setSelected();
111
128
  }
112
129
  },
113
- [optionRef, setSelected],
130
+ [disabled, optionRef, setSelected],
114
131
  );
115
132
 
116
133
  const renderedIcon = useMemo(() => {
@@ -136,6 +153,7 @@ const InternalComboboxOption = React.forwardRef<
136
153
  checked={isSelected}
137
154
  tabIndex={-1}
138
155
  animate={false}
156
+ disabled={disabled}
139
157
  />
140
158
  );
141
159
 
@@ -177,6 +195,7 @@ const InternalComboboxOption = React.forwardRef<
177
195
  inputValue,
178
196
  darkMode,
179
197
  optionTextId,
198
+ disabled,
180
199
  withIcons,
181
200
  ]);
182
201
 
@@ -187,9 +206,15 @@ const InternalComboboxOption = React.forwardRef<
187
206
  aria-selected={isFocused}
188
207
  aria-label={displayName}
189
208
  tabIndex={-1}
190
- className={cx(comboboxOptionStyle(), className)}
209
+ className={cx(
210
+ comboboxOptionStyle,
211
+ {
212
+ [comboboxOptionDisabledStyle]: disabled,
213
+ },
214
+ className,
215
+ )}
191
216
  onClick={handleOptionClick}
192
- onKeyPress={handleOptionClick}
217
+ onKeyDown={handleOptionClick}
193
218
  >
194
219
  {renderedChildren}
195
220
  </li>
@@ -4,12 +4,16 @@ import {
4
4
  configure,
5
5
  queryByText,
6
6
  queryByAttribute,
7
+ queryAllByTestId,
7
8
  queryAllByAttribute,
8
9
  } from '@testing-library/react';
9
10
  import userEvent from '@testing-library/user-event';
10
11
  import { Combobox, ComboboxGroup, ComboboxOption } from '.';
11
- import { BaseComboboxProps, ComboboxMultiselectProps } from './Combobox.types';
12
- import { OptionObject } from './util';
12
+ import {
13
+ BaseComboboxProps,
14
+ ComboboxMultiselectProps,
15
+ OptionObject,
16
+ } from './Combobox.types';
13
17
  import { isArray, isNull } from 'lodash';
14
18
  import chalk from '@testing-library/jest-dom/node_modules/chalk';
15
19
 
@@ -28,14 +32,17 @@ export const defaultOptions: Array<OptionObject> = [
28
32
  {
29
33
  value: 'apple',
30
34
  displayName: 'Apple',
35
+ isDisabled: false,
31
36
  },
32
37
  {
33
38
  value: 'banana',
34
39
  displayName: 'Banana',
40
+ isDisabled: false,
35
41
  },
36
42
  {
37
43
  value: 'carrot',
38
44
  displayName: 'Carrot',
45
+ isDisabled: false,
39
46
  },
40
47
  ];
41
48
 
@@ -46,10 +53,12 @@ export const groupedOptions: Array<NestedObject> = [
46
53
  {
47
54
  value: 'apple',
48
55
  displayName: 'Apple',
56
+ isDisabled: false,
49
57
  },
50
58
  {
51
59
  value: 'banana',
52
60
  displayName: 'Banana',
61
+ isDisabled: false,
53
62
  },
54
63
  ],
55
64
  },
@@ -59,10 +68,12 @@ export const groupedOptions: Array<NestedObject> = [
59
68
  {
60
69
  value: 'carrot',
61
70
  displayName: 'Carrot',
71
+ isDisabled: false,
62
72
  },
63
73
  {
64
74
  value: 'eggplant',
65
75
  displayName: 'Eggplant',
76
+ isDisabled: false,
66
77
  },
67
78
  ],
68
79
  },
@@ -88,8 +99,15 @@ export const getComboboxJSX = (props?: renderComboboxProps) => {
88
99
  const displayName =
89
100
  typeof option === 'string' ? undefined : option.displayName;
90
101
 
102
+ const isDisabled = typeof option === 'string' ? false : option.isDisabled;
103
+
91
104
  return (
92
- <ComboboxOption key={value} value={value} displayName={displayName} />
105
+ <ComboboxOption
106
+ key={value}
107
+ value={value}
108
+ displayName={displayName}
109
+ disabled={isDisabled}
110
+ />
93
111
  );
94
112
  }
95
113
  };
@@ -177,11 +195,7 @@ export function renderCombobox<T extends Select>(
177
195
  * @returns all chip elements
178
196
  */
179
197
  function queryAllChips(): Array<HTMLElement> {
180
- return queryAllByAttribute(
181
- 'data-leafygreen-ui',
182
- containerEl,
183
- 'combobox-chip',
184
- );
198
+ return queryAllByTestId(containerEl, 'lg-combobox-chip');
185
199
  }
186
200
 
187
201
  /**
@@ -0,0 +1,227 @@
1
+ import { render } from '@testing-library/react';
2
+ import React from 'react';
3
+ import Icon from '@leafygreen-ui/icon';
4
+ import { wrapJSX, getNameAndValue, flattenChildren } from '.';
5
+ import ComboboxOption from '../ComboboxOption';
6
+ import ComboboxGroup from '../ComboboxGroup';
7
+
8
+ describe('packages/combobox/utils', () => {
9
+ describe('wrapJSX', () => {
10
+ test('Wraps the matched string in the provided element', () => {
11
+ const JSX = wrapJSX('Apple', 'pp', 'em');
12
+ const { container } = render(JSX);
13
+ const ems = container.querySelectorAll('em');
14
+ expect(ems).toHaveLength(1);
15
+ expect(ems[0]).toHaveTextContent('pp');
16
+ expect(container).toHaveTextContent('Apple');
17
+ });
18
+ test('Wraps the entire string when it matches', () => {
19
+ const JSX = wrapJSX('Apple', 'Apple', 'em');
20
+ const { container } = render(JSX);
21
+ const ems = container.querySelectorAll('em');
22
+ expect(ems).toHaveLength(1);
23
+ expect(ems[0]).toHaveTextContent('Apple');
24
+ expect(container).toHaveTextContent('Apple');
25
+ });
26
+ test('Keeps case consistent with source', () => {
27
+ const JSX = wrapJSX('Apple', 'aPPlE', 'em');
28
+ const { container } = render(JSX);
29
+ const ems = container.querySelectorAll('em');
30
+ expect(ems).toHaveLength(1);
31
+ expect(ems[0]).toHaveTextContent('Apple');
32
+ expect(container).toHaveTextContent('Apple');
33
+ });
34
+ // No match
35
+ test('Wraps nothing when there is no match', () => {
36
+ const JSX = wrapJSX('Apple', 'q', 'em');
37
+ const { container } = render(JSX);
38
+ const ems = container.querySelectorAll('em');
39
+ expect(ems).toHaveLength(0);
40
+ expect(container).toHaveTextContent('Apple');
41
+ });
42
+
43
+ // Multiple matches
44
+ test('wraps all instances of a match', () => {
45
+ const JSX = wrapJSX('Pepper', 'p', 'em');
46
+ const { container } = render(JSX);
47
+ const ems = container.querySelectorAll('em');
48
+ expect(ems).toHaveLength(3);
49
+ expect(ems[0]).toHaveTextContent('P');
50
+ expect(ems[1]).toHaveTextContent('p');
51
+ expect(ems[2]).toHaveTextContent('p');
52
+ expect(container).toHaveTextContent('Pepper');
53
+ });
54
+ test('wraps all instances of longer match', () => {
55
+ const JSX = wrapJSX('Pepper', 'pe', 'em');
56
+ const { container } = render(JSX);
57
+ const ems = container.querySelectorAll('em');
58
+ expect(ems).toHaveLength(2);
59
+ expect(ems[0]).toHaveTextContent('Pe');
60
+ expect(ems[1]).toHaveTextContent('pe');
61
+ expect(container).toHaveTextContent('Pepper');
62
+ });
63
+
64
+ // No `wrap`
65
+ test('Returns the input string when "wrap" is empty', () => {
66
+ const JSX = wrapJSX('Apple', '', 'em');
67
+ const { container } = render(JSX);
68
+ const ems = container.querySelectorAll('em');
69
+ expect(ems).toHaveLength(0);
70
+ expect(container).toHaveTextContent(`Apple`);
71
+ });
72
+ test('Returns the input string when "wrap" is `undefined`', () => {
73
+ const JSX = wrapJSX('Apple', undefined, 'em');
74
+ const { container } = render(JSX);
75
+ const ems = container.querySelectorAll('em');
76
+ expect(ems).toHaveLength(0);
77
+ expect(container).toHaveTextContent(`Apple`);
78
+ });
79
+
80
+ // No `element`
81
+ test('Returns the input string when "element" is empty', () => {
82
+ const JSX = wrapJSX('Apple', 'ap', '' as keyof HTMLElementTagNameMap);
83
+ const { container } = render(JSX);
84
+ const ems = container.querySelectorAll('em');
85
+ expect(ems).toHaveLength(0);
86
+ expect(container).toHaveTextContent(`Apple`);
87
+ });
88
+ test('Returns the input string when "element" is undefined', () => {
89
+ const JSX = wrapJSX('Apple', 'ap');
90
+ const { container } = render(JSX);
91
+ const ems = container.querySelectorAll('em');
92
+ expect(ems).toHaveLength(0);
93
+ expect(container).toHaveTextContent(`Apple`);
94
+ });
95
+
96
+ test('Updates after a second call', () => {
97
+ const JSX = wrapJSX('Apple', 'p', 'em');
98
+ const { container, rerender } = render(JSX);
99
+ let ems = container.querySelectorAll('em');
100
+ expect(ems).toHaveLength(2);
101
+ const JSX2 = wrapJSX('Apple', 'pp', 'em');
102
+ rerender(JSX2);
103
+ ems = container.querySelectorAll('em');
104
+ expect(ems).toHaveLength(1);
105
+ expect(ems[0]).toHaveTextContent('pp');
106
+ expect(container).toHaveTextContent(`Apple`);
107
+ });
108
+ });
109
+
110
+ describe('getNameAndValue', () => {
111
+ test('Returns both value and displayName as given', () => {
112
+ const result = getNameAndValue({
113
+ value: 'value',
114
+ displayName: 'Display Name',
115
+ });
116
+ expect(result).toEqual({ value: 'value', displayName: 'Display Name' });
117
+ });
118
+
119
+ test('Returns a generated displayName', () => {
120
+ const result = getNameAndValue({ value: 'value' });
121
+ expect(result).toEqual({ value: 'value', displayName: 'value' });
122
+ });
123
+
124
+ test('Returns a generated value', () => {
125
+ const result = getNameAndValue({ displayName: 'Display Name' });
126
+ expect(result).toEqual({
127
+ value: 'display-name',
128
+ displayName: 'Display Name',
129
+ });
130
+ });
131
+ });
132
+
133
+ describe('flattenChildren', () => {
134
+ test('returns a single option', () => {
135
+ const children = <ComboboxOption value="test" displayName="Test" />;
136
+ const flat = flattenChildren(children);
137
+ expect(flat).toEqual([
138
+ {
139
+ value: 'test',
140
+ displayName: 'Test',
141
+ hasGlyph: false,
142
+ isDisabled: false,
143
+ },
144
+ ]);
145
+ });
146
+
147
+ test('returns multiple options', () => {
148
+ const children = [
149
+ <ComboboxOption key="apple" value="apple" displayName="Apple" />,
150
+ <ComboboxOption key="banana" value="banana" displayName="Banana" />,
151
+ ];
152
+ const flat = flattenChildren(children);
153
+ expect(flat).toEqual([
154
+ {
155
+ value: 'apple',
156
+ displayName: 'Apple',
157
+ hasGlyph: false,
158
+ isDisabled: false,
159
+ },
160
+ {
161
+ value: 'banana',
162
+ displayName: 'Banana',
163
+ hasGlyph: false,
164
+ isDisabled: false,
165
+ },
166
+ ]);
167
+ });
168
+
169
+ test('returns hasGlyph and isDisabled', () => {
170
+ const children = (
171
+ <ComboboxOption
172
+ value="test"
173
+ displayName="Test"
174
+ glyph={<Icon glyph="Beaker" />}
175
+ disabled
176
+ />
177
+ );
178
+ const flat = flattenChildren(children);
179
+ expect(flat).toEqual([
180
+ {
181
+ value: 'test',
182
+ displayName: 'Test',
183
+ hasGlyph: true,
184
+ isDisabled: true,
185
+ },
186
+ ]);
187
+ });
188
+
189
+ test('flattens nested options', () => {
190
+ const children = [
191
+ <ComboboxOption key="apple" value="apple" displayName="Apple" />,
192
+ <ComboboxOption key="banana" value="banana" displayName="Banana" />,
193
+ <ComboboxGroup key="peppers" label="Peppers">
194
+ <ComboboxOption value="ghost" displayName="Ghost" />
195
+ <ComboboxOption value="habanero" displayName="Habanero" />
196
+ </ComboboxGroup>,
197
+ ];
198
+ const flat = flattenChildren(children);
199
+ expect(flat).toEqual([
200
+ {
201
+ value: 'apple',
202
+ displayName: 'Apple',
203
+ hasGlyph: false,
204
+ isDisabled: false,
205
+ },
206
+ {
207
+ value: 'banana',
208
+ displayName: 'Banana',
209
+ hasGlyph: false,
210
+ isDisabled: false,
211
+ },
212
+ {
213
+ value: 'ghost',
214
+ displayName: 'Ghost',
215
+ hasGlyph: false,
216
+ isDisabled: false,
217
+ },
218
+ {
219
+ value: 'habanero',
220
+ displayName: 'Habanero',
221
+ hasGlyph: false,
222
+ isDisabled: false,
223
+ },
224
+ ]);
225
+ });
226
+ });
227
+ });
@@ -0,0 +1,26 @@
1
+ import { OptionObject } from '../Combobox.types';
2
+
3
+ export const getOptionObjectFromValue = (
4
+ value: string | null,
5
+ options: Array<OptionObject>,
6
+ ): OptionObject | undefined => {
7
+ if (value) return options.find(opt => opt.value === value);
8
+ };
9
+
10
+ export const getDisplayNameForValue = (
11
+ value: string | null,
12
+ options: Array<OptionObject>,
13
+ ): string => {
14
+ return value
15
+ ? getOptionObjectFromValue(value, options)?.displayName ?? value
16
+ : '';
17
+ };
18
+
19
+ export const getValueForDisplayName = (
20
+ displayName: string | null,
21
+ options: Array<OptionObject>,
22
+ ): string => {
23
+ return displayName
24
+ ? options.find(opt => opt.displayName === displayName)?.value ?? displayName
25
+ : '';
26
+ };
@@ -0,0 +1,47 @@
1
+ import React from 'react';
2
+ import { isComponentType, keyMap as _keyMap } from '@leafygreen-ui/lib';
3
+ import { OptionObject } from '../Combobox.types';
4
+ import { getNameAndValue } from './getNameAndValue';
5
+
6
+ /**
7
+ *
8
+ * Flattens multiple nested ComboboxOptions into a 1D array
9
+ *
10
+ * @param _children
11
+ * @returns `Array<OptionObject>`
12
+ */
13
+ export const flattenChildren = (
14
+ _children: React.ReactNode,
15
+ ): Array<OptionObject> => {
16
+ // TS doesn't like .reduce
17
+ // @ts-expect-error
18
+ return React.Children.toArray(_children).reduce(
19
+ // @ts-expect-error
20
+ (
21
+ acc: Array<OptionObject>,
22
+ child: React.ReactNode,
23
+ ): Array<OptionObject> | undefined => {
24
+ if (isComponentType(child, 'ComboboxOption')) {
25
+ const { value, displayName } = getNameAndValue(child.props);
26
+ const { glyph, disabled } = child.props;
27
+
28
+ return [
29
+ ...acc,
30
+ {
31
+ value,
32
+ displayName,
33
+ isDisabled: !!disabled,
34
+ hasGlyph: !!glyph,
35
+ },
36
+ ];
37
+ } else if (isComponentType(child, 'ComboboxGroup')) {
38
+ const { children } = child.props;
39
+
40
+ if (children) {
41
+ return [...acc, ...flattenChildren(children)];
42
+ }
43
+ }
44
+ },
45
+ [] as Array<OptionObject>,
46
+ );
47
+ };
@@ -0,0 +1,23 @@
1
+ import kebabCase from 'lodash/kebabCase';
2
+ import { ComboboxOptionProps } from '../Combobox.types';
3
+
4
+ /**
5
+ *
6
+ * Returns an object with properties `value` & `displayName`
7
+ * based on the props provided
8
+ *
9
+ * @property value: string
10
+ * @property displayName: string
11
+ */
12
+ export const getNameAndValue = ({
13
+ value: valProp,
14
+ displayName: nameProp,
15
+ }: ComboboxOptionProps): {
16
+ value: string;
17
+ displayName: string;
18
+ } => {
19
+ return {
20
+ value: valProp ?? kebabCase(nameProp),
21
+ displayName: nameProp ?? valProp ?? '', // TODO consider adding a prop to customize displayName => startCase(valProp),
22
+ };
23
+ };