@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.
- package/CHANGELOG.md +59 -0
- package/README.md +2 -2
- package/dist/Chip.d.ts.map +1 -1
- package/dist/Combobox.d.ts +7 -1
- package/dist/Combobox.d.ts.map +1 -1
- package/dist/Combobox.styles.d.ts +7 -3
- package/dist/Combobox.styles.d.ts.map +1 -1
- package/dist/Combobox.types.d.ts +33 -6
- package/dist/Combobox.types.d.ts.map +1 -1
- package/dist/ComboboxContext.d.ts +1 -1
- package/dist/ComboboxContext.d.ts.map +1 -1
- package/dist/ComboboxOption.d.ts.map +1 -1
- package/dist/ComboboxTestUtils.d.ts +1 -2
- package/dist/ComboboxTestUtils.d.ts.map +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/utils/OptionObjectUtils.d.ts +5 -0
- package/dist/utils/OptionObjectUtils.d.ts.map +1 -0
- package/dist/utils/flattenChildren.d.ts +11 -0
- package/dist/utils/flattenChildren.d.ts.map +1 -0
- package/dist/utils/getNameAndValue.d.ts +14 -0
- package/dist/utils/getNameAndValue.d.ts.map +1 -0
- package/dist/utils/index.d.ts +5 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/wrapJSX.d.ts +14 -0
- package/dist/utils/wrapJSX.d.ts.map +1 -0
- package/package.json +20 -10
- package/src/Chip.tsx +16 -9
- package/src/Combobox.spec.tsx +322 -139
- package/src/Combobox.story.tsx +274 -248
- package/src/Combobox.styles.ts +94 -24
- package/src/Combobox.tsx +446 -266
- package/src/Combobox.types.ts +43 -6
- package/src/ComboboxContext.tsx +2 -2
- package/src/ComboboxOption.tsx +34 -8
- package/src/ComboboxTestUtils.tsx +22 -8
- package/src/utils/ComboboxUtils.spec.tsx +227 -0
- package/src/utils/OptionObjectUtils.ts +26 -0
- package/src/utils/flattenChildren.tsx +47 -0
- package/src/utils/getNameAndValue.ts +23 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/wrapJSX.tsx +54 -0
- package/tsconfig.json +3 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/util.d.ts +0 -53
- package/dist/util.d.ts.map +0 -1
- package/src/util.tsx +0 -117
package/src/Combobox.types.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
89
|
-
* Changing value after initial render
|
|
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;
|
package/src/ComboboxContext.tsx
CHANGED
|
@@ -4,7 +4,7 @@ import { ComboboxSize, TrunctationLocation } from './Combobox.types';
|
|
|
4
4
|
interface ComboboxData {
|
|
5
5
|
multiselect: boolean;
|
|
6
6
|
darkMode: boolean;
|
|
7
|
-
size:
|
|
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:
|
|
18
|
+
size: ComboboxSize.Default,
|
|
19
19
|
withIcons: false,
|
|
20
20
|
disabled: false,
|
|
21
21
|
});
|
package/src/ComboboxOption.tsx
CHANGED
|
@@ -10,13 +10,14 @@ import {
|
|
|
10
10
|
InternalComboboxOptionProps,
|
|
11
11
|
} from './Combobox.types';
|
|
12
12
|
import { ComboboxContext } from './ComboboxContext';
|
|
13
|
-
import { wrapJSX } from './
|
|
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 =
|
|
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
|
-
|
|
107
|
-
(e.target
|
|
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(
|
|
209
|
+
className={cx(
|
|
210
|
+
comboboxOptionStyle,
|
|
211
|
+
{
|
|
212
|
+
[comboboxOptionDisabledStyle]: disabled,
|
|
213
|
+
},
|
|
214
|
+
className,
|
|
215
|
+
)}
|
|
190
216
|
onClick={handleOptionClick}
|
|
191
|
-
|
|
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 {
|
|
12
|
-
|
|
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
|
|
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
|
|
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';
|