@leafygreen-ui/combobox 1.0.2 → 1.2.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 +65 -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 +3 -4
- 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 +22 -12
- package/src/Chip.tsx +16 -9
- package/src/Combobox.spec.tsx +336 -164
- package/src/Combobox.story.tsx +274 -248
- package/src/Combobox.styles.ts +94 -24
- package/src/Combobox.tsx +456 -279
- package/src/Combobox.types.ts +46 -8
- package/src/ComboboxContext.tsx +2 -2
- package/src/ComboboxOption.tsx +36 -11
- 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 -3977
- 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
|
|
|
@@ -16,12 +36,13 @@ export const TrunctationLocation = {
|
|
|
16
36
|
end: 'end',
|
|
17
37
|
none: 'none',
|
|
18
38
|
} as const;
|
|
19
|
-
export type TrunctationLocation =
|
|
39
|
+
export type TrunctationLocation =
|
|
40
|
+
typeof TrunctationLocation[keyof typeof TrunctationLocation];
|
|
20
41
|
|
|
21
42
|
export const Overflow = {
|
|
22
43
|
expandY: 'expand-y',
|
|
23
44
|
expandX: 'expand-x',
|
|
24
|
-
|
|
45
|
+
scrollX: 'scroll-x',
|
|
25
46
|
} as const;
|
|
26
47
|
export type Overflow = typeof Overflow[keyof typeof Overflow];
|
|
27
48
|
|
|
@@ -55,7 +76,7 @@ export function getNullSelection<M extends boolean>(
|
|
|
55
76
|
multiselect: M,
|
|
56
77
|
): SelectValueType<M> {
|
|
57
78
|
if (multiselect) {
|
|
58
|
-
return
|
|
79
|
+
return [] as Array<string> as SelectValueType<M>;
|
|
59
80
|
} else {
|
|
60
81
|
return null as SelectValueType<M>;
|
|
61
82
|
}
|
|
@@ -73,8 +94,8 @@ export interface ComboboxMultiselectProps<M extends boolean> {
|
|
|
73
94
|
multiselect?: M;
|
|
74
95
|
/**
|
|
75
96
|
* The initial selection.
|
|
76
|
-
* Must be a string
|
|
77
|
-
* 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.
|
|
78
99
|
*/
|
|
79
100
|
initialValue?: SelectValueType<M>;
|
|
80
101
|
/**
|
|
@@ -84,10 +105,12 @@ export interface ComboboxMultiselectProps<M extends boolean> {
|
|
|
84
105
|
onChange?: onChangeType<M>;
|
|
85
106
|
/**
|
|
86
107
|
* The controlled value of the Combobox.
|
|
87
|
-
* Must be a string
|
|
88
|
-
* 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.
|
|
89
111
|
*/
|
|
90
112
|
value?: SelectValueType<M>;
|
|
113
|
+
|
|
91
114
|
/**
|
|
92
115
|
* Defines the overflow behavior of a multiselect combobox.
|
|
93
116
|
*
|
|
@@ -248,6 +271,7 @@ export type ComboboxProps<M extends boolean> = Either<
|
|
|
248
271
|
/**
|
|
249
272
|
* Combobox Option Props
|
|
250
273
|
*/
|
|
274
|
+
|
|
251
275
|
interface BaseComboboxOptionProps {
|
|
252
276
|
/**
|
|
253
277
|
* The internal value of the option. Used as the identifier in Combobox `initialValue`, value and filteredOptions.
|
|
@@ -266,6 +290,12 @@ interface BaseComboboxOptionProps {
|
|
|
266
290
|
*/
|
|
267
291
|
glyph?: ReactElement;
|
|
268
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
|
+
|
|
269
299
|
/**
|
|
270
300
|
* Styling Prop
|
|
271
301
|
*/
|
|
@@ -277,12 +307,20 @@ export type ComboboxOptionProps = Either<
|
|
|
277
307
|
'value' | 'displayName'
|
|
278
308
|
>;
|
|
279
309
|
|
|
310
|
+
export interface OptionObject {
|
|
311
|
+
value: string;
|
|
312
|
+
displayName: string;
|
|
313
|
+
isDisabled: boolean;
|
|
314
|
+
hasGlyph?: boolean;
|
|
315
|
+
}
|
|
316
|
+
|
|
280
317
|
export interface InternalComboboxOptionProps {
|
|
281
318
|
value: string;
|
|
282
319
|
displayName: string;
|
|
283
320
|
isSelected: boolean;
|
|
284
321
|
isFocused: boolean;
|
|
285
322
|
setSelected: () => void;
|
|
323
|
+
disabled?: boolean;
|
|
286
324
|
glyph?: ReactElement;
|
|
287
325
|
className?: string;
|
|
288
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,31 +99,35 @@ const InternalComboboxOption = React.forwardRef<
|
|
|
86
99
|
glyph,
|
|
87
100
|
isSelected,
|
|
88
101
|
isFocused,
|
|
102
|
+
disabled,
|
|
89
103
|
setSelected,
|
|
90
104
|
className,
|
|
91
105
|
}: InternalComboboxOptionProps,
|
|
92
106
|
forwardedRef,
|
|
93
107
|
) => {
|
|
94
|
-
const { multiselect, darkMode, withIcons, inputValue } =
|
|
95
|
-
ComboboxContext
|
|
96
|
-
);
|
|
108
|
+
const { multiselect, darkMode, withIcons, inputValue } =
|
|
109
|
+
useContext(ComboboxContext);
|
|
97
110
|
const optionTextId = useIdAllocator({ prefix: 'combobox-option-text' });
|
|
98
111
|
const optionRef = useForwardedRef(forwardedRef, null);
|
|
99
112
|
|
|
100
113
|
const handleOptionClick = useCallback(
|
|
101
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
|
|
102
118
|
e.stopPropagation();
|
|
103
119
|
|
|
104
120
|
// If user clicked on the option, or the checkbox
|
|
105
121
|
// Ignore extra click events on the checkbox wrapper
|
|
106
122
|
if (
|
|
107
|
-
|
|
108
|
-
(e.target
|
|
123
|
+
!disabled &&
|
|
124
|
+
(e.target === optionRef.current ||
|
|
125
|
+
(e.target as Element).tagName === 'INPUT')
|
|
109
126
|
) {
|
|
110
127
|
setSelected();
|
|
111
128
|
}
|
|
112
129
|
},
|
|
113
|
-
[optionRef, setSelected],
|
|
130
|
+
[disabled, optionRef, setSelected],
|
|
114
131
|
);
|
|
115
132
|
|
|
116
133
|
const renderedIcon = useMemo(() => {
|
|
@@ -136,6 +153,7 @@ const InternalComboboxOption = React.forwardRef<
|
|
|
136
153
|
checked={isSelected}
|
|
137
154
|
tabIndex={-1}
|
|
138
155
|
animate={false}
|
|
156
|
+
disabled={disabled}
|
|
139
157
|
/>
|
|
140
158
|
);
|
|
141
159
|
|
|
@@ -177,6 +195,7 @@ const InternalComboboxOption = React.forwardRef<
|
|
|
177
195
|
inputValue,
|
|
178
196
|
darkMode,
|
|
179
197
|
optionTextId,
|
|
198
|
+
disabled,
|
|
180
199
|
withIcons,
|
|
181
200
|
]);
|
|
182
201
|
|
|
@@ -187,9 +206,15 @@ const InternalComboboxOption = React.forwardRef<
|
|
|
187
206
|
aria-selected={isFocused}
|
|
188
207
|
aria-label={displayName}
|
|
189
208
|
tabIndex={-1}
|
|
190
|
-
className={cx(
|
|
209
|
+
className={cx(
|
|
210
|
+
comboboxOptionStyle,
|
|
211
|
+
{
|
|
212
|
+
[comboboxOptionDisabledStyle]: disabled,
|
|
213
|
+
},
|
|
214
|
+
className,
|
|
215
|
+
)}
|
|
191
216
|
onClick={handleOptionClick}
|
|
192
|
-
|
|
217
|
+
onKeyDown={handleOptionClick}
|
|
193
218
|
>
|
|
194
219
|
{renderedChildren}
|
|
195
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
|
+
};
|