@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.
- package/CHANGELOG.md +27 -0
- package/dist/esm/index.js +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/types/Combobox/Combobox.d.ts.map +1 -1
- package/dist/types/Combobox/Combobox.styles.d.ts +1 -1
- package/dist/types/Combobox/Combobox.types.d.ts +2 -2
- package/dist/types/Combobox/Combobox.types.d.ts.map +1 -1
- package/dist/types/Combobox/index.d.ts +1 -1
- package/dist/types/Combobox/index.d.ts.map +1 -1
- package/dist/types/ComboboxOption/ComboboxOption.d.ts.map +1 -1
- package/dist/types/ComboboxOption/ComboboxOption.styles.d.ts +6 -1
- package/dist/types/ComboboxOption/ComboboxOption.styles.d.ts.map +1 -1
- package/dist/types/ComboboxOption/ComboboxOption.types.d.ts +2 -2
- package/dist/types/ComboboxOption/ComboboxOption.types.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/OptionObjectUtils.d.ts +2 -1
- package/dist/types/utils/OptionObjectUtils.d.ts.map +1 -1
- package/dist/types/utils/getNameAndValue.d.ts.map +1 -1
- package/dist/umd/index.js +1 -1
- package/dist/umd/index.js.map +1 -1
- package/package.json +14 -13
- package/src/Combobox/Combobox.spec.tsx +53 -3
- package/src/Combobox/Combobox.tsx +11 -5
- package/src/Combobox/Combobox.types.ts +3 -1
- package/src/Combobox/index.ts +1 -0
- package/src/Combobox.stories.tsx +8 -7
- package/src/ComboboxOption/ComboboxOption.stories.tsx +35 -1
- package/src/ComboboxOption/ComboboxOption.styles.ts +34 -3
- package/src/ComboboxOption/ComboboxOption.tsx +16 -20
- package/src/ComboboxOption/ComboboxOption.types.ts +2 -2
- package/src/index.ts +1 -1
- package/src/utils/ComboboxUtils.spec.tsx +107 -1
- package/src/utils/OptionObjectUtils.ts +3 -1
- package/src/utils/getNameAndValue.ts +6 -2
- package/stories.js +2 -2
- package/tsconfig.json +6 -0
- package/tsdoc.json +34 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@leafygreen-ui/combobox",
|
|
3
|
-
"version": "12.
|
|
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/
|
|
17
|
-
"@leafygreen-ui/chip": "^4.0
|
|
18
|
-
"@leafygreen-ui/
|
|
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
|
|
21
|
-
"@leafygreen-ui/
|
|
22
|
-
"@leafygreen-ui/
|
|
23
|
-
"@leafygreen-ui/
|
|
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.
|
|
27
|
-
"@leafygreen-ui/
|
|
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
|
-
"@
|
|
35
|
-
"@leafygreen-ui/button": "^25.
|
|
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(
|
|
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'
|
|
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 {
|
|
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={
|
|
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
|
*/
|
package/src/Combobox/index.ts
CHANGED
package/src/Combobox.stories.tsx
CHANGED
|
@@ -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
|
-
<
|
|
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: [
|
|
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
|
-
|
|
54
|
-
|
|
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={
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
129
|
-
{
|
|
130
|
-
|
|
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?:
|
|
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 {
|
|
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
|
-
):
|
|
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(
|
|
23
|
-
displayName:
|
|
26
|
+
value: valProp ?? kebabCase(displayNameProps),
|
|
27
|
+
displayName: displayNameProps || valProp || '', // TODO consider adding a prop to customize displayName => startCase(valProp),
|
|
24
28
|
};
|
|
25
29
|
};
|