@leafygreen-ui/combobox 12.4.2 → 12.5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leafygreen-ui/combobox",
3
- "version": "12.4.2",
3
+ "version": "12.5.0",
4
4
  "description": "leafyGreen UI Kit Combobox",
5
5
  "main": "./dist/umd/index.js",
6
6
  "module": "./dist/esm/index.js",
@@ -14,14 +14,14 @@
14
14
  "lodash": "^4.17.21",
15
15
  "polished": "^4.2.2",
16
16
  "@leafygreen-ui/checkbox": "^18.1.5",
17
+ "@leafygreen-ui/emotion": "^5.2.0",
18
+ "@leafygreen-ui/hooks": "^9.3.1",
17
19
  "@leafygreen-ui/chip": "^4.2.1",
18
20
  "@leafygreen-ui/form-field": "^4.0.9",
19
- "@leafygreen-ui/hooks": "^9.3.1",
20
- "@leafygreen-ui/emotion": "^5.2.0",
21
21
  "@leafygreen-ui/icon": "^14.8.0",
22
22
  "@leafygreen-ui/icon-button": "^17.1.5",
23
- "@leafygreen-ui/input-option": "^4.1.5",
24
23
  "@leafygreen-ui/lib": "^15.7.0",
24
+ "@leafygreen-ui/input-option": "^4.1.5",
25
25
  "@leafygreen-ui/palette": "^5.0.2",
26
26
  "@leafygreen-ui/popover": "^14.3.2",
27
27
  "@leafygreen-ui/tokens": "^4.2.2",
@@ -13,6 +13,7 @@ import { axe } from 'jest-axe';
13
13
  import flatten from 'lodash/flatten';
14
14
  import isUndefined from 'lodash/isUndefined';
15
15
 
16
+ import { Badge } from '@leafygreen-ui/badge';
16
17
  import { RenderMode } from '@leafygreen-ui/popover';
17
18
  import { eventContainingTargetValue } from '@leafygreen-ui/testing-lib';
18
19
 
@@ -279,15 +280,11 @@ describe('packages/combobox', () => {
279
280
  expect(optionEl).toHaveTextContent('abc-def');
280
281
  });
281
282
 
282
- test('Option aria-label falls back to displayName text content', () => {
283
+ test('Option aria-label falls back to displayName', () => {
283
284
  const options: Array<OptionObject> = [
284
285
  {
285
286
  value: 'react-node-option',
286
- displayName: (
287
- <span>
288
- <strong>Bold</strong> and <em>italic</em> text
289
- </span>
290
- ),
287
+ displayName: 'Bold and italic text',
291
288
  isDisabled: false,
292
289
  },
293
290
  ];
@@ -297,6 +294,100 @@ describe('packages/combobox', () => {
297
294
  expect(optionEl).toHaveAttribute('aria-label', 'Bold and italic text');
298
295
  });
299
296
 
297
+ test('displayName renders correctly', () => {
298
+ const options: Array<OptionObject> = [
299
+ {
300
+ value: 'legacy-option',
301
+ displayName: 'Legacy String Display Name',
302
+ isDisabled: false,
303
+ },
304
+ ];
305
+ const { openMenu } = renderCombobox(select, { options });
306
+ const { optionElements } = openMenu();
307
+ const [optionEl] = Array.from(optionElements!);
308
+
309
+ // Should render the string displayName
310
+ expect(optionEl).toHaveTextContent('Legacy String Display Name');
311
+ expect(optionEl).toHaveAttribute(
312
+ 'aria-label',
313
+ 'Legacy String Display Name',
314
+ );
315
+ });
316
+
317
+ test('customContent with Badge component renders correctly', () => {
318
+ const { openMenu } = renderCombobox(select, {
319
+ children: (
320
+ <ComboboxOption
321
+ value="new-feature"
322
+ displayName="New Feature"
323
+ customContent={
324
+ <>
325
+ Custom Content Component
326
+ <Badge variant="blue" data-testid="custom-badge">
327
+ New
328
+ </Badge>
329
+ </>
330
+ }
331
+ />
332
+ ),
333
+ });
334
+ const { optionElements } = openMenu();
335
+ const [optionEl] = Array.from(optionElements!) as Array<Element>;
336
+
337
+ // Should render the custom content with Badge (not the displayName)
338
+ expect(optionEl).toHaveTextContent('Custom Content Component');
339
+ expect(optionEl).not.toHaveTextContent('New Feature');
340
+
341
+ // Should render the Badge component
342
+ const badgeEl = optionEl.querySelector('[data-testid="custom-badge"]');
343
+ expect(badgeEl).toBeInTheDocument();
344
+
345
+ // aria-label should still use the string displayName
346
+ expect(optionEl).toHaveAttribute('aria-label', 'New Feature');
347
+ });
348
+
349
+ test('option with customContent can be selected', async () => {
350
+ const onChange = jest.fn();
351
+ const { openMenu, inputEl, queryByTestId } = renderCombobox(select, {
352
+ onChange,
353
+ children: (
354
+ <ComboboxOption
355
+ value="new-feature"
356
+ displayName="New Feature"
357
+ customContent={
358
+ <>
359
+ Custom Content
360
+ <Badge variant="blue">New</Badge>
361
+ </>
362
+ }
363
+ />
364
+ ),
365
+ });
366
+ const { optionElements } = openMenu();
367
+ const [optionEl] = Array.from(optionElements!) as Array<Element>;
368
+
369
+ userEvent.click(optionEl as Element);
370
+
371
+ if (select === 'single') {
372
+ expect(onChange).toHaveBeenCalledWith('new-feature');
373
+ // Only displayName should be rendered in the input, not customContent
374
+ expect(inputEl).toHaveValue('New Feature');
375
+ expect(inputEl).not.toHaveValue('Custom Content');
376
+ } else {
377
+ expect(onChange).toHaveBeenCalledWith(
378
+ ['new-feature'],
379
+ expect.anything(),
380
+ );
381
+ // Only displayName should be rendered in the chip, not customContent
382
+ await waitFor(() => {
383
+ const chip = queryByTestId('lg-combobox-chip');
384
+ expect(chip).toBeInTheDocument();
385
+ expect(chip).toHaveTextContent('New Feature');
386
+ expect(chip).not.toHaveTextContent('Custom Content');
387
+ });
388
+ }
389
+ });
390
+
300
391
  test('Option aria-label falls back to value when displayName is not provided', () => {
301
392
  const options = [{ value: 'fallback-value' }];
302
393
  /// @ts-expect-error `options` will not match the expected type
@@ -8,6 +8,7 @@ import {
8
8
  import { StoryContext, StoryFn } from '@storybook/react';
9
9
  import { userEvent, within } from '@storybook/test';
10
10
 
11
+ import { Badge } from '@leafygreen-ui/badge';
11
12
  import { Button } from '@leafygreen-ui/button';
12
13
  import { css } from '@leafygreen-ui/emotion';
13
14
 
@@ -237,6 +238,57 @@ ExternalFilter.parameters = {
237
238
  chromatic: { disableSnapshot: true },
238
239
  };
239
240
 
241
+ /**
242
+ * Example showing the `customContent` prop for rendering custom components in dropdown options.
243
+ * The `customContent` prop accepts any ReactNode, allowing you to add badges, icons, or other
244
+ * custom components to your options. The `displayName` is still used for filtering and chips.
245
+ */
246
+ export const WithCustomContent = () => {
247
+ return (
248
+ <Combobox
249
+ label="Choose a feature"
250
+ description="Some features are new!"
251
+ placeholder="Select a feature"
252
+ multiselect={false}
253
+ >
254
+ <ComboboxOption
255
+ value="feature-a"
256
+ displayName="Feature A"
257
+ customContent={
258
+ <>
259
+ <span>Feature A</span>
260
+ <Badge variant="blue">New</Badge>
261
+ </>
262
+ }
263
+ />
264
+ <ComboboxOption
265
+ value="feature-b"
266
+ displayName="Feature B"
267
+ customContent={
268
+ <>
269
+ <span>Feature B</span>
270
+ <Badge variant="green">Beta</Badge>
271
+ </>
272
+ }
273
+ />
274
+ <ComboboxOption value="feature-c" displayName="Feature C" />
275
+ <ComboboxOption
276
+ value="feature-d"
277
+ displayName="Feature D"
278
+ customContent={
279
+ <>
280
+ <span>Feature D</span>
281
+ <Badge variant="red">Deprecated</Badge>
282
+ </>
283
+ }
284
+ />
285
+ </Combobox>
286
+ );
287
+ };
288
+ WithCustomContent.parameters = {
289
+ chromatic: { disableSnapshot: true },
290
+ };
291
+
240
292
  export const SingleSelect: StoryType<typeof Combobox> = () => <></>;
241
293
  SingleSelect.args = {
242
294
  multiselect: false,
@@ -81,7 +81,8 @@ export const WithIconsAndCustomDisplayName: StoryType<
81
81
  WithIconsAndCustomDisplayName.parameters = {
82
82
  generate: {
83
83
  args: {
84
- displayName: (
84
+ displayName: 'Option',
85
+ customContent: (
85
86
  <div
86
87
  className={css`
87
88
  display: flex;
@@ -30,6 +30,7 @@ export const InternalComboboxOption = React.forwardRef<
30
30
  glyph,
31
31
  isSelected,
32
32
  displayName,
33
+ customContent,
33
34
  isFocused,
34
35
  setSelected,
35
36
  className,
@@ -94,6 +95,9 @@ export const InternalComboboxOption = React.forwardRef<
94
95
  // When multiselect and withoutIcons the Checkbox is aligned to the top instead of centered.
95
96
  const multiSelectWithoutIcons = multiselect && !withIcons;
96
97
 
98
+ // Convert displayName ReactNode to string for aria-label and wrapJSX
99
+ const displayNameStr = getNodeTextContent(displayName);
100
+
97
101
  return (
98
102
  <InputOption
99
103
  {...rest}
@@ -101,7 +105,7 @@ export const InternalComboboxOption = React.forwardRef<
101
105
  ref={optionRef}
102
106
  highlighted={isFocused}
103
107
  disabled={disabled}
104
- aria-label={ariaLabel || getNodeTextContent(displayName) || value}
108
+ aria-label={ariaLabel || displayNameStr || value}
105
109
  darkMode={darkMode}
106
110
  className={getInputOptionStyles({
107
111
  size,
@@ -117,12 +121,12 @@ export const InternalComboboxOption = React.forwardRef<
117
121
  rightGlyph={rightGlyph}
118
122
  description={description}
119
123
  >
120
- {typeof displayName === 'string' ? (
124
+ {customContent ? (
125
+ customContent
126
+ ) : (
121
127
  <span id={optionTextId}>
122
- {wrapJSX(displayName, inputValue, 'strong')}
128
+ {wrapJSX(displayNameStr, inputValue, 'strong')}
123
129
  </span>
124
- ) : (
125
- displayName
126
130
  )}
127
131
  </InputOptionContent>
128
132
  </InputOption>
@@ -21,6 +21,12 @@ interface SharedComboboxOptionProps {
21
21
  */
22
22
  displayName?: ReactNode;
23
23
 
24
+ /**
25
+ * Optional custom content to render for the option.
26
+ * When provided, this ReactNode will be rendered in the option menu
27
+ */
28
+ customContent?: ReactNode;
29
+
24
30
  /**
25
31
  * The icon to display to the left of the option in the menu.
26
32
  */
@@ -1,5 +1,6 @@
1
1
  export { ComboboxOption, InternalComboboxOption } from './ComboboxOption';
2
2
  export {
3
3
  type ComboboxOptionProps,
4
+ type InternalComboboxOptionProps,
4
5
  type OptionObject,
5
6
  } from './ComboboxOption.types';
package/src/index.ts CHANGED
@@ -1,6 +1,10 @@
1
1
  export { Combobox, type ComboboxProps, RenderMode } from './Combobox';
2
2
  export { ComboboxGroup, type ComboboxGroupProps } from './ComboboxGroup';
3
- export { ComboboxOption, type ComboboxOptionProps } from './ComboboxOption';
3
+ export {
4
+ ComboboxOption,
5
+ type ComboboxOptionProps,
6
+ type InternalComboboxOptionProps,
7
+ } from './ComboboxOption';
4
8
  export {
5
9
  ComboboxSize,
6
10
  DropdownWidthBasis,
@@ -121,6 +121,7 @@ export const getComboboxJSX = (props?: renderComboboxProps) => {
121
121
 
122
122
  const label = props?.label ?? 'Some label';
123
123
  const options = props?.options ?? defaultOptions;
124
+ const children = props?.children;
124
125
  return (
125
126
  <LeafyGreenProvider>
126
127
  <Combobox
@@ -129,7 +130,7 @@ export const getComboboxJSX = (props?: renderComboboxProps) => {
129
130
  multiselect={props?.multiselect ?? false}
130
131
  {...props}
131
132
  >
132
- {options.map(renderOption)}
133
+ {children ?? options.map(renderOption)}
133
134
  </Combobox>
134
135
  </LeafyGreenProvider>
135
136
  );
@@ -202,21 +202,16 @@ describe('packages/combobox/utils', () => {
202
202
  expect(result).toBe('test');
203
203
  });
204
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 = [
205
+ test('Returns string displayName when option has string displayName', () => {
206
+ const optionsWithString = [
212
207
  {
213
- value: 'node-option',
214
- displayName: nodeDisplayName,
208
+ value: 'string-option',
209
+ displayName: 'Bold text',
215
210
  isDisabled: false,
216
211
  },
217
212
  ];
218
- const result = getDisplayNameForValue('node-option', optionsWithNode);
219
- expect(result).toBe(nodeDisplayName);
213
+ const result = getDisplayNameForValue('string-option', optionsWithString);
214
+ expect(result).toBe('Bold text');
220
215
  });
221
216
  });
222
217
 
@@ -280,12 +275,13 @@ describe('packages/combobox/utils', () => {
280
275
  ]);
281
276
  });
282
277
 
283
- test('flattens options with node displayName', () => {
278
+ test('flattens options with customContent', () => {
284
279
  const children = [
285
280
  <ComboboxOption
286
281
  key="test"
287
282
  value="test"
288
- displayName={
283
+ displayName="Testing New"
284
+ customContent={
289
285
  <div>
290
286
  <span>Testing</span>
291
287
  <span>New</span>