@leafygreen-ui/combobox 1.0.3 → 1.2.1

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 +59 -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 +1 -2
  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 +20 -10
  30. package/src/Chip.tsx +16 -9
  31. package/src/Combobox.spec.tsx +322 -139
  32. package/src/Combobox.story.tsx +274 -248
  33. package/src/Combobox.styles.ts +94 -24
  34. package/src/Combobox.tsx +446 -266
  35. package/src/Combobox.types.ts +43 -6
  36. package/src/ComboboxContext.tsx +2 -2
  37. package/src/ComboboxOption.tsx +34 -8
  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 -1
  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
 
@@ -22,7 +42,7 @@ export type TrunctationLocation =
22
42
  export const Overflow = {
23
43
  expandY: 'expand-y',
24
44
  expandX: 'expand-x',
25
- scrollY: 'scroll-x',
45
+ scrollX: 'scroll-x',
26
46
  } as const;
27
47
  export type Overflow = typeof Overflow[keyof typeof Overflow];
28
48
 
@@ -74,8 +94,8 @@ export interface ComboboxMultiselectProps<M extends boolean> {
74
94
  multiselect?: M;
75
95
  /**
76
96
  * The initial selection.
77
- * Must be a string for a single-select, or an array of strings for multiselect.
78
- * 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.
79
99
  */
80
100
  initialValue?: SelectValueType<M>;
81
101
  /**
@@ -85,10 +105,12 @@ export interface ComboboxMultiselectProps<M extends boolean> {
85
105
  onChange?: onChangeType<M>;
86
106
  /**
87
107
  * The controlled value of the Combobox.
88
- * Must be a string for a single-select, or an array of strings for multiselect.
89
- * 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.
90
111
  */
91
112
  value?: SelectValueType<M>;
113
+
92
114
  /**
93
115
  * Defines the overflow behavior of a multiselect combobox.
94
116
  *
@@ -249,6 +271,7 @@ export type ComboboxProps<M extends boolean> = Either<
249
271
  /**
250
272
  * Combobox Option Props
251
273
  */
274
+
252
275
  interface BaseComboboxOptionProps {
253
276
  /**
254
277
  * The internal value of the option. Used as the identifier in Combobox `initialValue`, value and filteredOptions.
@@ -267,6 +290,12 @@ interface BaseComboboxOptionProps {
267
290
  */
268
291
  glyph?: ReactElement;
269
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
+
270
299
  /**
271
300
  * Styling Prop
272
301
  */
@@ -278,12 +307,20 @@ export type ComboboxOptionProps = Either<
278
307
  'value' | 'displayName'
279
308
  >;
280
309
 
310
+ export interface OptionObject {
311
+ value: string;
312
+ displayName: string;
313
+ isDisabled: boolean;
314
+ hasGlyph?: boolean;
315
+ }
316
+
281
317
  export interface InternalComboboxOptionProps {
282
318
  value: string;
283
319
  displayName: string;
284
320
  isSelected: boolean;
285
321
  isFocused: boolean;
286
322
  setSelected: () => void;
323
+ disabled?: boolean;
287
324
  glyph?: ReactElement;
288
325
  className?: string;
289
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,6 +99,7 @@ const InternalComboboxOption = React.forwardRef<
86
99
  glyph,
87
100
  isSelected,
88
101
  isFocused,
102
+ disabled,
89
103
  setSelected,
90
104
  className,
91
105
  }: InternalComboboxOptionProps,
@@ -98,18 +112,22 @@ const InternalComboboxOption = React.forwardRef<
98
112
 
99
113
  const handleOptionClick = useCallback(
100
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
101
118
  e.stopPropagation();
102
119
 
103
120
  // If user clicked on the option, or the checkbox
104
121
  // Ignore extra click events on the checkbox wrapper
105
122
  if (
106
- e.target === optionRef.current ||
107
- (e.target as Element).tagName === 'INPUT'
123
+ !disabled &&
124
+ (e.target === optionRef.current ||
125
+ (e.target as Element).tagName === 'INPUT')
108
126
  ) {
109
127
  setSelected();
110
128
  }
111
129
  },
112
- [optionRef, setSelected],
130
+ [disabled, optionRef, setSelected],
113
131
  );
114
132
 
115
133
  const renderedIcon = useMemo(() => {
@@ -135,6 +153,7 @@ const InternalComboboxOption = React.forwardRef<
135
153
  checked={isSelected}
136
154
  tabIndex={-1}
137
155
  animate={false}
156
+ disabled={disabled}
138
157
  />
139
158
  );
140
159
 
@@ -176,6 +195,7 @@ const InternalComboboxOption = React.forwardRef<
176
195
  inputValue,
177
196
  darkMode,
178
197
  optionTextId,
198
+ disabled,
179
199
  withIcons,
180
200
  ]);
181
201
 
@@ -186,9 +206,15 @@ const InternalComboboxOption = React.forwardRef<
186
206
  aria-selected={isFocused}
187
207
  aria-label={displayName}
188
208
  tabIndex={-1}
189
- className={cx(comboboxOptionStyle(), className)}
209
+ className={cx(
210
+ comboboxOptionStyle,
211
+ {
212
+ [comboboxOptionDisabledStyle]: disabled,
213
+ },
214
+ className,
215
+ )}
190
216
  onClick={handleOptionClick}
191
- onKeyPress={handleOptionClick}
217
+ onKeyDown={handleOptionClick}
192
218
  >
193
219
  {renderedChildren}
194
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
+ };
@@ -0,0 +1,8 @@
1
+ export { wrapJSX } from './wrapJSX';
2
+ export { getNameAndValue } from './getNameAndValue';
3
+ export {
4
+ getOptionObjectFromValue,
5
+ getDisplayNameForValue,
6
+ getValueForDisplayName,
7
+ } from './OptionObjectUtils';
8
+ export { flattenChildren } from './flattenChildren';