@leafygreen-ui/combobox 12.2.1 → 12.4.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 (38) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/esm/index.js +1 -1
  3. package/dist/esm/index.js.map +1 -1
  4. package/dist/types/Combobox/Combobox.d.ts.map +1 -1
  5. package/dist/types/Combobox/Combobox.styles.d.ts +1 -1
  6. package/dist/types/Combobox/Combobox.types.d.ts +2 -2
  7. package/dist/types/Combobox/Combobox.types.d.ts.map +1 -1
  8. package/dist/types/Combobox/index.d.ts +1 -1
  9. package/dist/types/Combobox/index.d.ts.map +1 -1
  10. package/dist/types/ComboboxOption/ComboboxOption.d.ts.map +1 -1
  11. package/dist/types/ComboboxOption/ComboboxOption.styles.d.ts +6 -1
  12. package/dist/types/ComboboxOption/ComboboxOption.styles.d.ts.map +1 -1
  13. package/dist/types/ComboboxOption/ComboboxOption.types.d.ts +2 -2
  14. package/dist/types/ComboboxOption/ComboboxOption.types.d.ts.map +1 -1
  15. package/dist/types/index.d.ts +1 -1
  16. package/dist/types/index.d.ts.map +1 -1
  17. package/dist/types/utils/OptionObjectUtils.d.ts +2 -1
  18. package/dist/types/utils/OptionObjectUtils.d.ts.map +1 -1
  19. package/dist/types/utils/getNameAndValue.d.ts.map +1 -1
  20. package/dist/umd/index.js +1 -1
  21. package/dist/umd/index.js.map +1 -1
  22. package/package.json +14 -13
  23. package/src/Combobox/Combobox.spec.tsx +53 -3
  24. package/src/Combobox/Combobox.tsx +11 -5
  25. package/src/Combobox/Combobox.types.ts +3 -1
  26. package/src/Combobox/index.ts +1 -0
  27. package/src/Combobox.stories.tsx +8 -7
  28. package/src/ComboboxOption/ComboboxOption.stories.tsx +35 -1
  29. package/src/ComboboxOption/ComboboxOption.styles.ts +34 -3
  30. package/src/ComboboxOption/ComboboxOption.tsx +16 -20
  31. package/src/ComboboxOption/ComboboxOption.types.ts +2 -2
  32. package/src/index.ts +1 -1
  33. package/src/utils/ComboboxUtils.spec.tsx +107 -1
  34. package/src/utils/OptionObjectUtils.ts +3 -1
  35. package/src/utils/getNameAndValue.ts +6 -2
  36. package/stories.js +2 -2
  37. package/tsconfig.json +6 -0
  38. package/tsdoc.json +34 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leafygreen-ui/combobox",
3
- "version": "12.2.1",
3
+ "version": "12.4.0",
4
4
  "description": "leafyGreen UI Kit Combobox",
5
5
  "main": "./dist/umd/index.js",
6
6
  "module": "./dist/esm/index.js",
@@ -13,26 +13,27 @@
13
13
  "chalk": "^4.1.2",
14
14
  "lodash": "^4.17.21",
15
15
  "polished": "^4.2.2",
16
- "@leafygreen-ui/emotion": "^5.1.0",
17
- "@leafygreen-ui/chip": "^4.0.9",
18
- "@leafygreen-ui/form-field": "^4.0.7",
16
+ "@leafygreen-ui/checkbox": "^18.1.4",
17
+ "@leafygreen-ui/chip": "^4.2.0",
18
+ "@leafygreen-ui/emotion": "^5.2.0",
19
+ "@leafygreen-ui/form-field": "^4.0.8",
19
20
  "@leafygreen-ui/hooks": "^9.3.0",
20
- "@leafygreen-ui/icon-button": "^17.1.3",
21
- "@leafygreen-ui/checkbox": "^18.1.3",
22
- "@leafygreen-ui/icon": "^14.7.0",
23
- "@leafygreen-ui/input-option": "^4.1.3",
21
+ "@leafygreen-ui/icon": "^14.8.0",
22
+ "@leafygreen-ui/icon-button": "^17.1.4",
23
+ "@leafygreen-ui/input-option": "^4.1.4",
24
+ "@leafygreen-ui/lib": "^15.7.0",
24
25
  "@leafygreen-ui/palette": "^5.0.2",
25
26
  "@leafygreen-ui/popover": "^14.3.1",
26
- "@leafygreen-ui/tokens": "^4.0.0",
27
- "@leafygreen-ui/lib": "^15.6.2",
28
- "@leafygreen-ui/typography": "^22.2.2"
27
+ "@leafygreen-ui/tokens": "^4.2.1",
28
+ "@leafygreen-ui/typography": "^22.2.3"
29
29
  },
30
30
  "peerDependencies": {
31
31
  "@leafygreen-ui/leafygreen-provider": "^5.0.0 || ^4.0.0 || ^3.2.0"
32
32
  },
33
33
  "devDependencies": {
34
- "@lg-tools/build": "^0.8.3",
35
- "@leafygreen-ui/button": "^25.1.3"
34
+ "@leafygreen-ui/badge": "^10.2.3",
35
+ "@leafygreen-ui/button": "^25.2.0",
36
+ "@lg-tools/build": "^0.9.0"
36
37
  },
37
38
  "homepage": "https://github.com/mongodb/leafygreen-ui/tree/main/packages/combobox",
38
39
  "repository": {
@@ -1,9 +1,10 @@
1
1
  /* eslint-disable jest/no-standalone-expect */
2
2
  /* eslint jest/expect-expect: ["error", { "assertFunctionNames": ["expect", "expectSelection"] }] */
3
- import { createRef } from 'react';
3
+ import React, { createRef } from 'react';
4
4
  import {
5
5
  act,
6
6
  queryByText,
7
+ render,
7
8
  waitFor,
8
9
  waitForElementToBeRemoved,
9
10
  } from '@testing-library/react';
@@ -12,6 +13,7 @@ import { axe } from 'jest-axe';
12
13
  import flatten from 'lodash/flatten';
13
14
  import isUndefined from 'lodash/isUndefined';
14
15
 
16
+ import { Badge } from '@leafygreen-ui/badge';
15
17
  import { RenderMode } from '@leafygreen-ui/popover';
16
18
  import { eventContainingTargetValue } from '@leafygreen-ui/testing-lib';
17
19
 
@@ -25,6 +27,7 @@ import {
25
27
  Select,
26
28
  testif,
27
29
  } from '../utils/ComboboxTestUtils';
30
+ import { Combobox, ComboboxOption } from '..';
28
31
 
29
32
  /**
30
33
  * Tests
@@ -262,7 +265,9 @@ describe('packages/combobox', () => {
262
265
  const { optionElements } = openMenu();
263
266
  // Note on `foo!` operator https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-0.html#non-null-assertion-operator
264
267
  Array.from(optionElements!).forEach((optionEl, index) => {
265
- expect(optionEl).toHaveTextContent(defaultOptions[index].displayName);
268
+ expect(optionEl).toHaveTextContent(
269
+ defaultOptions[index].displayName as string,
270
+ );
266
271
  });
267
272
  });
268
273
 
@@ -275,6 +280,49 @@ describe('packages/combobox', () => {
275
280
  expect(optionEl).toHaveTextContent('abc-def');
276
281
  });
277
282
 
283
+ test('Option aria-label falls back to displayName text content', () => {
284
+ const options: Array<OptionObject> = [
285
+ {
286
+ value: 'react-node-option',
287
+ displayName: (
288
+ <span>
289
+ <strong>Bold</strong> and <em>italic</em> text
290
+ </span>
291
+ ),
292
+ isDisabled: false,
293
+ },
294
+ ];
295
+ const { openMenu } = renderCombobox(select, { options });
296
+ const { optionElements } = openMenu();
297
+ const [optionEl] = Array.from(optionElements!);
298
+ expect(optionEl).toHaveAttribute('aria-label', 'Bold and italic text');
299
+ });
300
+
301
+ test('Option aria-label falls back to value when displayName is not provided', () => {
302
+ const options = [{ value: 'fallback-value' }];
303
+ /// @ts-expect-error `options` will not match the expected type
304
+ const { openMenu } = renderCombobox(select, { options });
305
+ const { optionElements } = openMenu();
306
+ const [optionEl] = Array.from(optionElements!);
307
+ expect(optionEl).toHaveAttribute('aria-label', 'fallback-value');
308
+ });
309
+
310
+ test('Option uses explicit aria-label prop when provided', () => {
311
+ const { getByRole, queryByRole } = render(
312
+ <Combobox label="Test" multiselect={select === 'multiple'}>
313
+ <ComboboxOption
314
+ value="test-value"
315
+ displayName="Display Name"
316
+ aria-label="Custom aria label"
317
+ />
318
+ </Combobox>,
319
+ );
320
+ userEvent.click(getByRole('combobox'));
321
+ const listbox = queryByRole('listbox');
322
+ const optionEl = listbox?.getElementsByTagName('li')[0];
323
+ expect(optionEl).toHaveAttribute('aria-label', 'Custom aria label');
324
+ });
325
+
278
326
  test('Options with long names are rendered with the full text', () => {
279
327
  const displayName = `Donec id elit non mi porta gravida at eget metus. Aenean lacinia bibendum nulla sed consectetur.`;
280
328
  const options: Array<OptionObject> = [
@@ -367,7 +415,9 @@ describe('packages/combobox', () => {
367
415
  groupedOptions.map(({ children }: NestedObject) => children),
368
416
  ).forEach((option: OptionObject | string) => {
369
417
  const displayName =
370
- typeof option === 'string' ? option : option.displayName;
418
+ typeof option === 'string'
419
+ ? option
420
+ : (option.displayName as string);
371
421
  const optionEl = queryByText(menuContainerEl!, displayName);
372
422
  expect(optionEl).toBeInTheDocument();
373
423
  });
@@ -34,7 +34,12 @@ import LeafyGreenProvider, {
34
34
  PopoverPropsProvider,
35
35
  useDarkMode,
36
36
  } from '@leafygreen-ui/leafygreen-provider';
37
- import { consoleOnce, isComponentType, keyMap } from '@leafygreen-ui/lib';
37
+ import {
38
+ consoleOnce,
39
+ getNodeTextContent,
40
+ isComponentType,
41
+ keyMap,
42
+ } from '@leafygreen-ui/lib';
38
43
  import {
39
44
  DismissMode,
40
45
  getPopoverRenderModeProps,
@@ -324,7 +329,7 @@ export function Combobox<M extends boolean>({
324
329
  ? getDisplayNameForValue(value, allOptions)
325
330
  : option.displayName;
326
331
 
327
- const isValueInDisplayName = displayName
332
+ const isValueInDisplayName = getNodeTextContent(displayName)
328
333
  .toLowerCase()
329
334
  .includes(inputValue.toLowerCase());
330
335
 
@@ -718,6 +723,7 @@ export function Combobox<M extends boolean>({
718
723
  if (isMultiselect(selection)) {
719
724
  return selection.filter(isValueValid).map((value, index) => {
720
725
  const displayName = getDisplayNameForValue(value, allOptions);
726
+ const displayNameContent = getNodeTextContent(displayName);
721
727
  const isFocused = focusedChip === value;
722
728
  const chipRef = getChipRef(value);
723
729
  const isLastChip = index >= selection.length - 1;
@@ -740,7 +746,7 @@ export function Combobox<M extends boolean>({
740
746
  return (
741
747
  <ComboboxChip
742
748
  key={value}
743
- displayName={displayName}
749
+ displayName={displayNameContent}
744
750
  isFocused={isFocused}
745
751
  onRemove={onRemove}
746
752
  onFocus={onFocus}
@@ -793,7 +799,7 @@ export function Combobox<M extends boolean>({
793
799
  selection as SelectValueType<false>,
794
800
  allOptions,
795
801
  ) ?? prevSelection;
796
- updateInputValue(displayName);
802
+ updateInputValue(getNodeTextContent(displayName));
797
803
  }
798
804
  }
799
805
  }, [
@@ -821,7 +827,7 @@ export function Combobox<M extends boolean>({
821
827
  selection as SelectValueType<false>,
822
828
  allOptions,
823
829
  ) ?? '';
824
- updateInputValue(displayName);
830
+ updateInputValue(getNodeTextContent(displayName));
825
831
  closeMenu();
826
832
  }
827
833
  } else {
@@ -2,7 +2,7 @@ import React, { ReactNode } from 'react';
2
2
 
3
3
  import { type ChipProps } from '@leafygreen-ui/chip';
4
4
  import { Either } from '@leafygreen-ui/lib';
5
- import { PopoverProps } from '@leafygreen-ui/popover';
5
+ import { PopoverProps, RenderMode } from '@leafygreen-ui/popover';
6
6
 
7
7
  import {
8
8
  ComboboxSize,
@@ -14,6 +14,8 @@ import {
14
14
  State,
15
15
  } from '../types';
16
16
 
17
+ export { RenderMode };
18
+
17
19
  /**
18
20
  * Combobox Props
19
21
  */
@@ -3,4 +3,5 @@ export {
3
3
  type BaseComboboxProps,
4
4
  type ComboboxMultiselectProps,
5
5
  type ComboboxProps,
6
+ RenderMode,
6
7
  } from './Combobox.types';
@@ -5,7 +5,7 @@ import {
5
5
  type StoryMetaType,
6
6
  StoryType,
7
7
  } from '@lg-tools/storybook-utils';
8
- import { StoryFn } from '@storybook/react';
8
+ import { StoryContext, StoryFn } from '@storybook/react';
9
9
  import { userEvent, within } from '@storybook/test';
10
10
 
11
11
  import { Button } from '@leafygreen-ui/button';
@@ -150,15 +150,15 @@ const meta: StoryMetaType<typeof Combobox> = {
150
150
 
151
151
  export default meta;
152
152
 
153
- export const LiveExample: StoryFn<ComboboxProps<boolean>> = (
154
- args: ComboboxProps<boolean>,
155
- ) => {
153
+ export const LiveExample: StoryFn<ComboboxProps<boolean>> = args => {
156
154
  return (
157
155
  <>
158
156
  {/* Since Combobox doesn't fully refresh when `multiselect` changes, we need to explicitly render a different instance */}
159
157
  {args.multiselect ? (
158
+ // @ts-ignore - multiselect check ensures props match ComboboxProps<true>
160
159
  <Combobox key="multi" {...args} multiselect={true} />
161
160
  ) : (
161
+ // @ts-ignore - multiselect check ensures props match ComboboxProps<false>
162
162
  <Combobox key="single" {...args} multiselect={false} />
163
163
  )}
164
164
  </>
@@ -265,6 +265,7 @@ export const MultiSelectNoIcons: StoryFn<ComboboxProps<boolean>> = (
265
265
  args: ComboboxProps<boolean>,
266
266
  ) => {
267
267
  return (
268
+ // @ts-expect-error - args will have multiselect=true from storybook controls
268
269
  <Combobox {...args} multiselect={true}>
269
270
  {getComboboxOptions(false)}
270
271
  </Combobox>
@@ -299,20 +300,20 @@ export const InitialLongComboboxOpen = {
299
300
  </Combobox>
300
301
  );
301
302
  },
302
- play: async ctx => {
303
+ play: async (ctx: StoryContext) => {
303
304
  const { findByRole } = within(ctx.canvasElement.parentElement!);
304
305
  const trigger = await findByRole('combobox');
305
306
  userEvent.click(trigger);
306
307
  },
307
308
  decorators: [
308
- (StoryFn, _ctx) => (
309
+ (Story: StoryFn, _ctx: StoryContext) => (
309
310
  <div
310
311
  className={css`
311
312
  height: 100vh;
312
313
  padding: 0;
313
314
  `}
314
315
  >
315
- <StoryFn />
316
+ <Story />
316
317
  </div>
317
318
  ),
318
319
  ],
@@ -1,8 +1,11 @@
1
1
  import React from 'react';
2
2
  import { StoryMetaType, StoryType } from '@lg-tools/storybook-utils';
3
3
 
4
+ import { Badge } from '@leafygreen-ui/badge';
5
+ import { css } from '@leafygreen-ui/emotion';
4
6
  import { Icon } from '@leafygreen-ui/icon';
5
7
  import LeafyGreenProvider from '@leafygreen-ui/leafygreen-provider';
8
+ import { spacing } from '@leafygreen-ui/tokens';
6
9
 
7
10
  import { ComboboxContext, defaultContext } from '../ComboboxContext';
8
11
 
@@ -14,7 +17,12 @@ const meta: StoryMetaType<typeof InternalComboboxOption> = {
14
17
  parameters: {
15
18
  default: null,
16
19
  generate: {
17
- storyNames: ['WithIcons', 'WithoutIcons', 'WithoutIconsAndMultiStep'],
20
+ storyNames: [
21
+ 'WithIcons',
22
+ 'WithoutIcons',
23
+ 'WithoutIconsAndMultiStep',
24
+ 'WithIconsAndCustomDisplayName',
25
+ ],
18
26
  combineArgs: {
19
27
  darkMode: [false, true],
20
28
  description: [undefined, 'This is a description'],
@@ -67,6 +75,32 @@ WithIcons.parameters = {
67
75
  },
68
76
  };
69
77
 
78
+ export const WithIconsAndCustomDisplayName: StoryType<
79
+ typeof InternalComboboxOption
80
+ > = () => <></>;
81
+ WithIconsAndCustomDisplayName.parameters = {
82
+ generate: {
83
+ args: {
84
+ displayName: (
85
+ <div
86
+ className={css`
87
+ display: flex;
88
+ align-items: center;
89
+ gap: ${spacing[100]}px;
90
+ margin-bottom: ${spacing[100]}px;
91
+ `}
92
+ >
93
+ <span>Option</span>
94
+ <Badge variant="green">New</Badge>
95
+ </div>
96
+ ),
97
+ /// @ts-expect-error - withIcons is not a component prop
98
+ withIcons: true,
99
+ glyph: <Icon glyph="Cloud" />,
100
+ },
101
+ },
102
+ };
103
+
70
104
  export const WithoutIconsAndMultiStep: StoryType<
71
105
  typeof InternalComboboxOption
72
106
  > = () => <></>;
@@ -1,4 +1,4 @@
1
- import { css } from '@leafygreen-ui/emotion';
1
+ import { css, cx } from '@leafygreen-ui/emotion';
2
2
  import { leftGlyphClassName } from '@leafygreen-ui/input-option';
3
3
  import {
4
4
  descriptionClassName,
@@ -50,8 +50,16 @@ export const disallowPointer = css`
50
50
  pointer-events: none;
51
51
  `;
52
52
 
53
- export const displayNameStyle = (isSelected: boolean) => css`
54
- font-weight: ${isSelected ? fontWeights.semiBold : fontWeights.regular};
53
+ const inputOptionBaseStyles = css`
54
+ .${titleClassName} {
55
+ font-weight: ${fontWeights.regular};
56
+ }
57
+ `;
58
+
59
+ const selectedInputOptionStyles = css`
60
+ .${titleClassName} {
61
+ font-weight: ${fontWeights.semiBold};
62
+ }
55
63
  `;
56
64
 
57
65
  export const iconThemeStyles: Record<Theme, string> = {
@@ -113,3 +121,26 @@ export const multiselectIconLargePosition = css`
113
121
  top: 3px;
114
122
  }
115
123
  `;
124
+
125
+ export const getInputOptionStyles = ({
126
+ size,
127
+ isMultiselectWithoutIcons,
128
+ isSelected,
129
+ className,
130
+ }: {
131
+ size: ComboboxSize;
132
+ isMultiselectWithoutIcons: boolean;
133
+ isSelected: boolean;
134
+ className?: string;
135
+ }) =>
136
+ cx(
137
+ inputOptionBaseStyles,
138
+ {
139
+ [selectedInputOptionStyles]: isSelected,
140
+ [largeStyles]: size === ComboboxSize.Large,
141
+ [multiselectIconPosition]: isMultiselectWithoutIcons,
142
+ [multiselectIconLargePosition]:
143
+ isMultiselectWithoutIcons && size === ComboboxSize.Large,
144
+ },
145
+ className,
146
+ );
@@ -1,20 +1,14 @@
1
1
  import React, { useCallback, useContext, useMemo } from 'react';
2
2
 
3
- import { cx } from '@leafygreen-ui/emotion';
4
3
  import { useForwardedRef, useIdAllocator } from '@leafygreen-ui/hooks';
5
4
  import { InputOption, InputOptionContent } from '@leafygreen-ui/input-option';
6
5
  import { useDarkMode } from '@leafygreen-ui/leafygreen-provider';
6
+ import { getNodeTextContent } from '@leafygreen-ui/lib';
7
7
 
8
8
  import { ComboboxContext } from '../ComboboxContext';
9
- import { ComboboxSize } from '../types';
10
9
  import { wrapJSX } from '../utils';
11
10
 
12
- import {
13
- displayNameStyle,
14
- largeStyles,
15
- multiselectIconLargePosition,
16
- multiselectIconPosition,
17
- } from './ComboboxOption.styles';
11
+ import { getInputOptionStyles } from './ComboboxOption.styles';
18
12
  import {
19
13
  ComboboxOptionProps,
20
14
  InternalComboboxOptionProps,
@@ -43,6 +37,7 @@ export const InternalComboboxOption = React.forwardRef<
43
37
  value,
44
38
  onClick,
45
39
  disabled = false,
40
+ 'aria-label': ariaLabel,
46
41
  ...rest
47
42
  }: InternalComboboxOptionProps,
48
43
  forwardedRef,
@@ -106,17 +101,14 @@ export const InternalComboboxOption = React.forwardRef<
106
101
  ref={optionRef}
107
102
  highlighted={isFocused}
108
103
  disabled={disabled}
109
- aria-label={displayName}
104
+ aria-label={ariaLabel || getNodeTextContent(displayName) || value}
110
105
  darkMode={darkMode}
111
- className={cx(
112
- {
113
- [largeStyles]: size === ComboboxSize.Large,
114
- [multiselectIconPosition]: multiSelectWithoutIcons,
115
- [multiselectIconLargePosition]:
116
- multiSelectWithoutIcons && size === ComboboxSize.Large,
117
- },
106
+ className={getInputOptionStyles({
107
+ size,
108
+ isSelected,
109
+ isMultiselectWithoutIcons: multiSelectWithoutIcons,
118
110
  className,
119
- )}
111
+ })}
120
112
  onClick={handleOptionClick}
121
113
  onKeyDown={handleOptionClick}
122
114
  >
@@ -125,9 +117,13 @@ export const InternalComboboxOption = React.forwardRef<
125
117
  rightGlyph={rightGlyph}
126
118
  description={description}
127
119
  >
128
- <span id={optionTextId} className={displayNameStyle(isSelected)}>
129
- {wrapJSX(displayName, inputValue, 'strong')}
130
- </span>
120
+ {typeof displayName === 'string' ? (
121
+ <span id={optionTextId}>
122
+ {wrapJSX(displayName, inputValue, 'strong')}
123
+ </span>
124
+ ) : (
125
+ displayName
126
+ )}
131
127
  </InputOptionContent>
132
128
  </InputOption>
133
129
  );
@@ -1,4 +1,4 @@
1
- import { ComponentPropsWithoutRef, ReactElement } from 'react';
1
+ import { ComponentPropsWithoutRef, ReactElement, ReactNode } from 'react';
2
2
 
3
3
  import { Either } from '@leafygreen-ui/lib';
4
4
 
@@ -19,7 +19,7 @@ interface SharedComboboxOptionProps {
19
19
  * The display value of the option. Used as the rendered string within the menu and chips.
20
20
  * When undefined, this is set to `value`
21
21
  */
22
- displayName?: string;
22
+ displayName?: ReactNode;
23
23
 
24
24
  /**
25
25
  * The icon to display to the left of the option in the menu.
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { Combobox, type ComboboxProps } from './Combobox';
1
+ export { Combobox, type ComboboxProps, RenderMode } from './Combobox';
2
2
  export { ComboboxGroup, type ComboboxGroupProps } from './ComboboxGroup';
3
3
  export { ComboboxOption, type ComboboxOptionProps } from './ComboboxOption';
4
4
  export {
@@ -5,7 +5,12 @@ import { Icon } from '@leafygreen-ui/icon';
5
5
 
6
6
  import { ComboboxGroup, ComboboxOption } from '..';
7
7
 
8
- import { flattenChildren, getNameAndValue, wrapJSX } from '.';
8
+ import {
9
+ flattenChildren,
10
+ getDisplayNameForValue,
11
+ getNameAndValue,
12
+ wrapJSX,
13
+ } from '.';
9
14
 
10
15
  describe('packages/combobox/utils', () => {
11
16
  describe('wrapJSX', () => {
@@ -155,6 +160,66 @@ describe('packages/combobox/utils', () => {
155
160
  });
156
161
  });
157
162
 
163
+ describe('getDisplayNameForValue', () => {
164
+ const options = [
165
+ { value: 'apple', displayName: 'Apple', isDisabled: false },
166
+ { value: 'banana', displayName: 'Banana', isDisabled: false },
167
+ { value: 'carrot', displayName: 'Carrot', isDisabled: true },
168
+ ];
169
+
170
+ test('Returns the displayName when a matching option is found', () => {
171
+ const result = getDisplayNameForValue('apple', options);
172
+ expect(result).toBe('Apple');
173
+ });
174
+
175
+ test('Returns the value when no matching option is found', () => {
176
+ const result = getDisplayNameForValue('unknown', options);
177
+ expect(result).toBe('unknown');
178
+ });
179
+
180
+ test('Returns empty string when value is null', () => {
181
+ const result = getDisplayNameForValue(null, options);
182
+ expect(result).toBe('');
183
+ });
184
+
185
+ test('Returns empty string when value is empty string', () => {
186
+ const result = getDisplayNameForValue('', options);
187
+ expect(result).toBe('');
188
+ });
189
+
190
+ test('Returns displayName for disabled option', () => {
191
+ const result = getDisplayNameForValue('carrot', options);
192
+ expect(result).toBe('Carrot');
193
+ });
194
+
195
+ test('Returns empty string when options array is empty and value is null', () => {
196
+ const result = getDisplayNameForValue(null, []);
197
+ expect(result).toBe('');
198
+ });
199
+
200
+ test('Returns value when options array is empty but value is provided', () => {
201
+ const result = getDisplayNameForValue('test', []);
202
+ expect(result).toBe('test');
203
+ });
204
+
205
+ test('Returns React node displayName when option has node displayName', () => {
206
+ const nodeDisplayName = (
207
+ <span>
208
+ <strong>Bold</strong> text
209
+ </span>
210
+ );
211
+ const optionsWithNode = [
212
+ {
213
+ value: 'node-option',
214
+ displayName: nodeDisplayName,
215
+ isDisabled: false,
216
+ },
217
+ ];
218
+ const result = getDisplayNameForValue('node-option', optionsWithNode);
219
+ expect(result).toBe(nodeDisplayName);
220
+ });
221
+ });
222
+
158
223
  describe('flattenChildren', () => {
159
224
  test('returns a single option', () => {
160
225
  const children = <ComboboxOption value="test" displayName="Test" />;
@@ -165,6 +230,7 @@ describe('packages/combobox/utils', () => {
165
230
  displayName: 'Test',
166
231
  hasGlyph: false,
167
232
  isDisabled: false,
233
+ badge: undefined,
168
234
  },
169
235
  ]);
170
236
  });
@@ -181,12 +247,14 @@ describe('packages/combobox/utils', () => {
181
247
  displayName: 'Apple',
182
248
  hasGlyph: false,
183
249
  isDisabled: false,
250
+ badge: undefined,
184
251
  },
185
252
  {
186
253
  value: 'banana',
187
254
  displayName: 'Banana',
188
255
  hasGlyph: false,
189
256
  isDisabled: false,
257
+ badge: undefined,
190
258
  },
191
259
  ]);
192
260
  });
@@ -202,6 +270,44 @@ describe('packages/combobox/utils', () => {
202
270
  );
203
271
  const flat = flattenChildren(children);
204
272
  expect(flat).toEqual([
273
+ {
274
+ value: 'test',
275
+ displayName: 'Test',
276
+ hasGlyph: true,
277
+ isDisabled: true,
278
+ badge: undefined,
279
+ },
280
+ ]);
281
+ });
282
+
283
+ test('flattens options with node displayName', () => {
284
+ const children = [
285
+ <ComboboxOption
286
+ key="test"
287
+ value="test"
288
+ displayName={
289
+ <div>
290
+ <span>Testing</span>
291
+ <span>New</span>
292
+ </div>
293
+ }
294
+ />,
295
+ <ComboboxOption
296
+ key="test2"
297
+ value="test"
298
+ displayName="Test"
299
+ glyph={<Icon glyph="Beaker" />}
300
+ disabled
301
+ />,
302
+ ];
303
+ const flat = flattenChildren(children);
304
+ expect(flat).toEqual([
305
+ {
306
+ value: 'test',
307
+ displayName: 'Testing New',
308
+ hasGlyph: false,
309
+ isDisabled: false,
310
+ },
205
311
  {
206
312
  value: 'test',
207
313
  displayName: 'Test',
@@ -1,3 +1,5 @@
1
+ import { ReactNode } from 'react';
2
+
1
3
  import { OptionObject } from '../ComboboxOption';
2
4
  /**
3
5
  *
@@ -21,7 +23,7 @@ export const getOptionObjectFromValue = (
21
23
  export const getDisplayNameForValue = (
22
24
  value: string | null,
23
25
  options: Array<OptionObject>,
24
- ): string => {
26
+ ): ReactNode => {
25
27
  return value
26
28
  ? getOptionObjectFromValue(value, options)?.displayName ?? value
27
29
  : '';
@@ -1,5 +1,7 @@
1
1
  import kebabCase from 'lodash/kebabCase';
2
2
 
3
+ import { getNodeTextContent } from '@leafygreen-ui/lib';
4
+
3
5
  import { ComboboxOptionProps } from '../ComboboxOption';
4
6
 
5
7
  /**
@@ -18,8 +20,10 @@ export const getNameAndValue = ({
18
20
  value: string;
19
21
  displayName: string;
20
22
  } => {
23
+ const displayNameProps = getNodeTextContent(nameProp);
24
+
21
25
  return {
22
- value: valProp ?? kebabCase(nameProp),
23
- displayName: nameProp ?? valProp ?? '', // TODO consider adding a prop to customize displayName => startCase(valProp),
26
+ value: valProp ?? kebabCase(displayNameProps),
27
+ displayName: displayNameProps || valProp || '', // TODO consider adding a prop to customize displayName => startCase(valProp),
24
28
  };
25
29
  };