@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/CHANGELOG.md +7 -0
- package/README.md +9 -8
- package/dist/esm/index.js +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/types/ComboboxOption/ComboboxOption.d.ts.map +1 -1
- package/dist/types/ComboboxOption/ComboboxOption.types.d.ts +5 -0
- package/dist/types/ComboboxOption/ComboboxOption.types.d.ts.map +1 -1
- package/dist/types/ComboboxOption/index.d.ts +1 -1
- package/dist/types/ComboboxOption/index.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/utils/ComboboxTestUtils.d.ts.map +1 -1
- package/dist/umd/index.js +1 -1
- package/dist/umd/index.js.map +1 -1
- package/package.json +4 -4
- package/src/Combobox/Combobox.spec.tsx +97 -6
- package/src/Combobox.stories.tsx +52 -0
- package/src/ComboboxOption/ComboboxOption.stories.tsx +2 -1
- package/src/ComboboxOption/ComboboxOption.tsx +9 -5
- package/src/ComboboxOption/ComboboxOption.types.ts +6 -0
- package/src/ComboboxOption/index.ts +1 -0
- package/src/index.ts +5 -1
- package/src/utils/ComboboxTestUtils.tsx +2 -1
- package/src/utils/ComboboxUtils.spec.tsx +9 -13
- package/stories.js +2 -2
- package/tsdoc.json +43 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@leafygreen-ui/combobox",
|
|
3
|
-
"version": "12.
|
|
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
|
|
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
|
package/src/Combobox.stories.tsx
CHANGED
|
@@ -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,
|
|
@@ -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 ||
|
|
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
|
-
{
|
|
124
|
+
{customContent ? (
|
|
125
|
+
customContent
|
|
126
|
+
) : (
|
|
121
127
|
<span id={optionTextId}>
|
|
122
|
-
{wrapJSX(
|
|
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
|
*/
|
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 {
|
|
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
|
|
206
|
-
const
|
|
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: '
|
|
214
|
-
displayName:
|
|
208
|
+
value: 'string-option',
|
|
209
|
+
displayName: 'Bold text',
|
|
215
210
|
isDisabled: false,
|
|
216
211
|
},
|
|
217
212
|
];
|
|
218
|
-
const result = getDisplayNameForValue('
|
|
219
|
-
expect(result).toBe(
|
|
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
|
|
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>
|