@jobber/components 8.3.0 → 8.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/ActivityIndicator.d.ts +1 -0
- package/ActivityIndicator.js +17 -0
- package/dist/ActivityIndicator/ActivityIndicator.d.ts +38 -0
- package/dist/ActivityIndicator/index.cjs +9 -0
- package/dist/ActivityIndicator/index.d.ts +2 -0
- package/dist/ActivityIndicator/index.mjs +3 -0
- package/dist/ActivityIndicator-cjs.js +27 -0
- package/dist/ActivityIndicator-es.js +25 -0
- package/dist/Autocomplete/Autocomplete.d.ts +6 -4
- package/dist/Autocomplete/Autocomplete.types.d.ts +3 -137
- package/dist/Autocomplete/components/FloatingMenu.d.ts +6 -6
- package/dist/Autocomplete/components/MenuList.d.ts +4 -4
- package/dist/Autocomplete/components/PersistentRegion.d.ts +3 -3
- package/dist/Autocomplete/index.cjs +25 -1658
- package/dist/Autocomplete/index.d.ts +3 -30
- package/dist/Autocomplete/index.mjs +25 -1647
- package/dist/Autocomplete/tests/Autocomplete.setup.d.ts +15 -15
- package/dist/Autocomplete/useAutocomplete.d.ts +2 -2
- package/dist/Autocomplete-cjs.js +1306 -0
- package/dist/Autocomplete-es.js +1304 -0
- package/dist/Menu-es.js +1 -1
- package/dist/MenuSubmenuTrigger-es.js +1 -1
- package/dist/Spinner/Spinner.d.ts +6 -0
- package/dist/Spinner-cjs.js +3 -0
- package/dist/Spinner-es.js +3 -0
- package/dist/docs/ActivityIndicator/ActivityIndicator.md +67 -0
- package/dist/docs/Autocomplete/{AutocompleteV2.md → Autocomplete.md} +13 -107
- package/dist/docs/Banner/Banner.md +3 -3
- package/dist/docs/Box/Box.md +2 -2
- package/dist/docs/Chips/Chips.md +1 -1
- package/dist/docs/DataList/DataList.md +1 -1
- package/dist/docs/Icon/Icon.md +1 -1
- package/dist/docs/InputGroup/InputGroup.md +1 -1
- package/dist/docs/Menu/Menu.md +1 -1
- package/dist/docs/Modal/Modal.md +3 -3
- package/dist/docs/MultiSelect/MultiSelect.md +1 -1
- package/dist/docs/Spinner/Spinner.md +9 -1
- package/dist/docs/index.md +2 -2
- package/dist/docs/usage-guidelines/usage-guidelines.md +8 -10
- package/dist/floating-ui.react-dom-es.js +1 -1
- package/dist/floating-ui.react-es.js +1 -1
- package/dist/index.cjs +5 -3
- package/dist/index.d.mts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.esm-es.js +1 -1
- package/dist/index.mjs +3 -2
- package/dist/styles.css +308 -153
- package/dist/utils/meta/meta.json +1 -0
- package/package.json +8 -3
- package/dist/Autocomplete/Autocomplete.rebuilt.d.ts +0 -8
- package/dist/Autocomplete/Autocomplete.utils.d.ts +0 -8
- package/dist/Autocomplete/Menu/DefaultMenu.d.ts +0 -16
- package/dist/Autocomplete/Menu/Menu.d.ts +0 -3
- package/dist/Autocomplete/Menu/MenuWrapper.d.ts +0 -23
- package/dist/Autocomplete/Option.d.ts +0 -78
- package/dist/Autocomplete/V1.docgen.d.ts +0 -4
- package/dist/Autocomplete/V2.docgen.d.ts +0 -6
- package/dist/Autocomplete/useKeyboardNavigation.d.ts +0 -35
- package/dist/Autocomplete/useRepositionMenu.d.ts +0 -10
- package/dist/docs/Autocomplete/AutocompleteV1.md +0 -1328
|
@@ -1,1328 +0,0 @@
|
|
|
1
|
-
# Autocomplete
|
|
2
|
-
|
|
3
|
-
An Autocomplete component allows a user to quickly pick a preset value from a
|
|
4
|
-
larger list of possible options.
|
|
5
|
-
|
|
6
|
-
## Design & usage guidelines
|
|
7
|
-
|
|
8
|
-
The options in an Autocomplete component should be a list of possible values
|
|
9
|
-
that the user can select. These options should be presented in a way that makes
|
|
10
|
-
it easy for the user to find the value they are looking for. There are a few
|
|
11
|
-
ways to achieve this.
|
|
12
|
-
|
|
13
|
-
[Details](/storybook/web/?path=/story/components-forms-and-inputs-autocomplete--with-details)
|
|
14
|
-
can be added to an option, which will either appear below the option's label or
|
|
15
|
-
on the bottom right of the option. This can be used to provide more information
|
|
16
|
-
about the option, such as a description or a count.
|
|
17
|
-
|
|
18
|
-
[Section headings](/storybook/web/?path=/story/components-forms-and-inputs-autocomplete--section-heading)
|
|
19
|
-
can be added to an Autocomplete to break up the options into groups. This can be
|
|
20
|
-
useful if there are a lot of options or if the options are related to different
|
|
21
|
-
things.
|
|
22
|
-
|
|
23
|
-
You can
|
|
24
|
-
[set a value](/storybook/web/?path=/story/components-forms-and-inputs-autocomplete--set-a-value)
|
|
25
|
-
to the Autocomplete component by passing an active option. This will set the
|
|
26
|
-
`value` of the input.
|
|
27
|
-
|
|
28
|
-
Using the `getOptions` prop, you can pass a function that will be called when
|
|
29
|
-
the user types in the input. This async request should return a list of possible
|
|
30
|
-
options based on the user's input. This can be achieved by using either promises
|
|
31
|
-
or `await`.
|
|
32
|
-
|
|
33
|
-
## Related components
|
|
34
|
-
|
|
35
|
-
* If you want to present a list of predefined options without text input, or the
|
|
36
|
-
number of options is smaller, use a [Select](../Select/Select.md)
|
|
37
|
-
* If autocompleted results are not required for the text input, use
|
|
38
|
-
[InputText](../InputText/InputText.md)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
## Component customization
|
|
42
|
-
|
|
43
|
-
### Custom rendering
|
|
44
|
-
|
|
45
|
-
If you need more control over the rendering of content in the menu, you can
|
|
46
|
-
provide a **custom render function** using `customRenderMenu`
|
|
47
|
-
|
|
48
|
-
This is useful if the default rendering of the Autocomplete Menu doesn’t meet
|
|
49
|
-
your requirements. If a custom render function is provided you will be able to
|
|
50
|
-
completely control how the menu is rendered.
|
|
51
|
-
|
|
52
|
-
This is achieved by providing a function to the `customRenderMenu` prop. This
|
|
53
|
-
function will receive the following props:
|
|
54
|
-
|
|
55
|
-
* `MenuWrapper` is the wrapper of the Autocomplete Menu, this is the element
|
|
56
|
-
that will be visible when the menu is open. It handles the positioning of the
|
|
57
|
-
Autocomplete Menu
|
|
58
|
-
* `inputFocused` is a boolean that indicates if the input is focused.
|
|
59
|
-
* `onOptionSelect` is a function that is called when an option is selected for
|
|
60
|
-
updating the value of the Autocomplete.
|
|
61
|
-
* `options` is the list of options to render in the Autocomplete Menu. This is
|
|
62
|
-
the options that are returned from the `getOptions` function.
|
|
63
|
-
* `selectedOption` is the option that is currently selected.
|
|
64
|
-
* `menuRef` is a ref that is passed to the Autocomplete Menu this is used by the
|
|
65
|
-
`useKeyboardNavigation` hook to handle the keyboard navigation of the options.
|
|
66
|
-
* `inputRef` is a ref that is passed to the Autocomplete Menu to provide a ref
|
|
67
|
-
to the `InputText` component.
|
|
68
|
-
|
|
69
|
-
There are also some helper functions that are used to handle the keyboard
|
|
70
|
-
navigation of the options and the base styling of the options.
|
|
71
|
-
|
|
72
|
-
* `useKeyboardNavigation` is used to handle the default keyboard navigation of
|
|
73
|
-
the options. Use this hook if you are using only using MenuOption or
|
|
74
|
-
BaseMenuOption components in the menu.
|
|
75
|
-
* `useCustomKeyboardNavigation` is used to handle the keyboard navigation of the
|
|
76
|
-
options. Use this hook if you are using components in the menu that aren't
|
|
77
|
-
MenuOption or BaseMenuOption such as custom elements. See the Advanced
|
|
78
|
-
examples below for more information.
|
|
79
|
-
* `MenuOption` is the default rendering of the options in the Autocomplete Menu.
|
|
80
|
-
The `UNSAFE_style` prop is used to add a custom styling to the options.
|
|
81
|
-
* `isOptionGroup` is a helper function that is used to determine if the option
|
|
82
|
-
is a group.
|
|
83
|
-
* `isOptionSelected` is a helper function that is used to determine if the
|
|
84
|
-
option is selected.
|
|
85
|
-
* `MenuOptionContent` is the default rendering of the content of the options in
|
|
86
|
-
the Autocomplete Menu.
|
|
87
|
-
* `BaseMenuOption` is the base component for the options in the Autocomplete
|
|
88
|
-
Menu. This component provides the base styling and functionality for the
|
|
89
|
-
options such as the separators and the highlighting. This is used in the
|
|
90
|
-
advanced example below.
|
|
91
|
-
|
|
92
|
-
### Basic Override
|
|
93
|
-
|
|
94
|
-
In this example we will override the default rendering of the Autocomplete Menu to add a custom styling to the options while keeping the default keyboard navigation.
|
|
95
|
-
|
|
96
|
-
```tsx
|
|
97
|
-
<AutocompleteBasicCustomRenderExample />
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
**Full Example (Click to Reveal Code)**
|
|
101
|
-
|
|
102
|
-
```tsx
|
|
103
|
-
export const BasicCustomTemplate = () => {
|
|
104
|
-
const [detailsValue, setDetailsValue] = useState<Option | undefined>();
|
|
105
|
-
const planetSection = SectionHeadingOptions.find(
|
|
106
|
-
({ label }) => label === "Planets",
|
|
107
|
-
)?.options;
|
|
108
|
-
|
|
109
|
-
return (
|
|
110
|
-
<Content>
|
|
111
|
-
<Autocomplete
|
|
112
|
-
placeholder="Search for an option with section headings"
|
|
113
|
-
initialOptions={SectionHeadingOptions}
|
|
114
|
-
value={detailsValue}
|
|
115
|
-
onChange={newValue => setDetailsValue(newValue)}
|
|
116
|
-
customRenderMenu={({
|
|
117
|
-
MenuWrapper,
|
|
118
|
-
inputFocused,
|
|
119
|
-
onOptionSelect,
|
|
120
|
-
options,
|
|
121
|
-
selectedOption,
|
|
122
|
-
menuRef,
|
|
123
|
-
}) => {
|
|
124
|
-
const { highlightedIndex } = useKeyboardNavigation({
|
|
125
|
-
visible: inputFocused,
|
|
126
|
-
options,
|
|
127
|
-
onOptionSelect,
|
|
128
|
-
menuRef,
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
return (
|
|
132
|
-
<MenuWrapper visible={inputFocused}>
|
|
133
|
-
{options.map((option, index) => {
|
|
134
|
-
const optionStyle = getOptionStyling({
|
|
135
|
-
option,
|
|
136
|
-
highlightedIndex,
|
|
137
|
-
options,
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
return (
|
|
141
|
-
<MenuOption
|
|
142
|
-
key={option.label}
|
|
143
|
-
option={option}
|
|
144
|
-
addSeparators={isOptionGroup(option)}
|
|
145
|
-
isHighlighted={index === highlightedIndex}
|
|
146
|
-
onOptionSelect={onOptionSelect}
|
|
147
|
-
isSelected={isOptionSelected(selectedOption, option)}
|
|
148
|
-
UNSAFE_style={optionStyle}
|
|
149
|
-
/>
|
|
150
|
-
);
|
|
151
|
-
})}
|
|
152
|
-
</MenuWrapper>
|
|
153
|
-
);
|
|
154
|
-
}}
|
|
155
|
-
getOptions={getHeadingsOption}
|
|
156
|
-
/>
|
|
157
|
-
</Content>
|
|
158
|
-
);
|
|
159
|
-
|
|
160
|
-
// eslint-disable-next-line max-statements
|
|
161
|
-
function getOptionStyling({
|
|
162
|
-
option,
|
|
163
|
-
highlightedIndex,
|
|
164
|
-
options,
|
|
165
|
-
}: {
|
|
166
|
-
option: AnyOption;
|
|
167
|
-
highlightedIndex: number;
|
|
168
|
-
options: AnyOption[];
|
|
169
|
-
}) {
|
|
170
|
-
const isSectionLabel = "options" in option;
|
|
171
|
-
const isPlanetsLabel = option.label === "Planets";
|
|
172
|
-
|
|
173
|
-
const inPlanetSection = planetSection?.find?.(
|
|
174
|
-
({ label }) => label === option.label,
|
|
175
|
-
);
|
|
176
|
-
const activeOption = options[highlightedIndex];
|
|
177
|
-
|
|
178
|
-
const activeOptionInSection =
|
|
179
|
-
isSectionLabel &&
|
|
180
|
-
option?.options?.find?.(({ label }) => label === activeOption?.label);
|
|
181
|
-
let optionColor = "var(--color-success--surface)";
|
|
182
|
-
let groupColor = "var(--color-success)";
|
|
183
|
-
|
|
184
|
-
if (isSectionLabel && activeOptionInSection) {
|
|
185
|
-
groupColor = "var(--color-informative)";
|
|
186
|
-
optionColor = "var(--color-informative--surface)";
|
|
187
|
-
} else if (!isSectionLabel && inPlanetSection) {
|
|
188
|
-
groupColor = "var(--color-warning)";
|
|
189
|
-
optionColor = "var(--color-warning--surface)";
|
|
190
|
-
} else if (isSectionLabel && isPlanetsLabel) {
|
|
191
|
-
groupColor = "var(--color-critical)";
|
|
192
|
-
optionColor = "var(--color-critical--surface)";
|
|
193
|
-
}
|
|
194
|
-
const isActive = activeOption === option ? 0.5 : 1;
|
|
195
|
-
|
|
196
|
-
return {
|
|
197
|
-
groupOption: {
|
|
198
|
-
heading: {
|
|
199
|
-
backgroundColor: groupColor,
|
|
200
|
-
},
|
|
201
|
-
},
|
|
202
|
-
option: {
|
|
203
|
-
backgroundColor: optionColor,
|
|
204
|
-
opacity: isActive,
|
|
205
|
-
},
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
function getHeadingsOption(text: string) {
|
|
210
|
-
if (text === "") {
|
|
211
|
-
return SectionHeadingOptions;
|
|
212
|
-
}
|
|
213
|
-
const filterRegex = new RegExp(text, "i");
|
|
214
|
-
|
|
215
|
-
return SectionHeadingOptions.map(section => ({
|
|
216
|
-
...section,
|
|
217
|
-
options: section.options.filter(option =>
|
|
218
|
-
option.label.match(filterRegex),
|
|
219
|
-
),
|
|
220
|
-
}));
|
|
221
|
-
}
|
|
222
|
-
};
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
### Advanced Menu Override
|
|
226
|
-
|
|
227
|
-
In this example we will override the rendering of the menu to render the Options
|
|
228
|
-
in a completely custom way. This example will also include a footer that is
|
|
229
|
-
rendered at the bottom of the menu with sticky positioning.
|
|
230
|
-
|
|
231
|
-
**Note:** Due to the complexity of the example, we will not be using the
|
|
232
|
-
`useKeyboardNavigation` hook to handle the keyboard navigation of the options.
|
|
233
|
-
|
|
234
|
-
Instead we will be handling the keyboard navigation manually with the
|
|
235
|
-
`useCustomKeyboardNavigation` hook. This requires us to manually keep track of
|
|
236
|
-
the highlighted option index and update it when the user navigates the menu.
|
|
237
|
-
|
|
238
|
-
```tsx
|
|
239
|
-
<AutocompleteAdvancedCustomRenderExample />
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
**Full Example (Click to Reveal Code)**
|
|
243
|
-
|
|
244
|
-
```tsx
|
|
245
|
-
interface CustomOption {
|
|
246
|
-
value: number;
|
|
247
|
-
label: string;
|
|
248
|
-
address: string;
|
|
249
|
-
contact: string;
|
|
250
|
-
status: string;
|
|
251
|
-
}
|
|
252
|
-
const AdvancedOptions: CustomOption[] = [
|
|
253
|
-
{
|
|
254
|
-
value: 1,
|
|
255
|
-
label: "Nostromo",
|
|
256
|
-
address: "4517 Washington Ave. Manchester, Kentucky 39495",
|
|
257
|
-
contact: "(406) 555-4145 · darlene.r@gmail.com",
|
|
258
|
-
status: "Lead",
|
|
259
|
-
},
|
|
260
|
-
{
|
|
261
|
-
value: 2,
|
|
262
|
-
label: "Rodger Young",
|
|
263
|
-
address: "8502 Preston Rd. Inglewood, Maine 98380",
|
|
264
|
-
contact: "(406) 555-0120 · marvin.m@gmail.com",
|
|
265
|
-
status: "Active",
|
|
266
|
-
},
|
|
267
|
-
{
|
|
268
|
-
value: 3,
|
|
269
|
-
label: "Serenity",
|
|
270
|
-
address: "3 Properties",
|
|
271
|
-
contact: "(406) 555-0120 · seemore.b@gmail.com",
|
|
272
|
-
status: "Active",
|
|
273
|
-
},
|
|
274
|
-
{
|
|
275
|
-
value: 4,
|
|
276
|
-
label: "Sleeper Service",
|
|
277
|
-
address: "3891 Ranchview Dr. Richardson, California 62639",
|
|
278
|
-
contact: "(406) 555-0120",
|
|
279
|
-
status: "Active",
|
|
280
|
-
},
|
|
281
|
-
];
|
|
282
|
-
|
|
283
|
-
export const AdvancedCustomTemplate = () => {
|
|
284
|
-
const [value, setValue] = useState<CustomOption | undefined>();
|
|
285
|
-
|
|
286
|
-
function getOptions(text: string) {
|
|
287
|
-
if (text === "") {
|
|
288
|
-
return AdvancedOptions;
|
|
289
|
-
}
|
|
290
|
-
const filterRegex = new RegExp(text, "i");
|
|
291
|
-
|
|
292
|
-
return AdvancedOptions.filter(
|
|
293
|
-
option =>
|
|
294
|
-
option.label.match(filterRegex) ||
|
|
295
|
-
option.address.match(filterRegex) ||
|
|
296
|
-
option.contact.match(filterRegex),
|
|
297
|
-
);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
return (
|
|
301
|
-
<Autocomplete
|
|
302
|
-
placeholder="Search for something"
|
|
303
|
-
initialOptions={AdvancedOptions}
|
|
304
|
-
value={value}
|
|
305
|
-
onChange={newValue => setValue(newValue)}
|
|
306
|
-
customRenderMenu={props => <CustomMenuContent {...props} />}
|
|
307
|
-
getOptions={getOptions}
|
|
308
|
-
/>
|
|
309
|
-
);
|
|
310
|
-
};
|
|
311
|
-
|
|
312
|
-
function CustomMenuContent({
|
|
313
|
-
options,
|
|
314
|
-
selectedOption,
|
|
315
|
-
onOptionSelect,
|
|
316
|
-
inputFocused,
|
|
317
|
-
inputRef,
|
|
318
|
-
MenuWrapper,
|
|
319
|
-
menuRef,
|
|
320
|
-
}: CustomOptionsMenuProp<CustomOption, CustomOption>) {
|
|
321
|
-
// Set to -1 to account for the footer being the first option when options are being initialized
|
|
322
|
-
const INITIAL_HIGHLIGHTED_OPTION_INDEX = -1;
|
|
323
|
-
|
|
324
|
-
const [highlightedOptionIndex, setHighlightedOptionIndex] = useState(
|
|
325
|
-
INITIAL_HIGHLIGHTED_OPTION_INDEX,
|
|
326
|
-
);
|
|
327
|
-
// Length of options + 1 to account for the footer
|
|
328
|
-
const maxIndex = options.length - 1 + 1;
|
|
329
|
-
|
|
330
|
-
// We need to track the footer focus state because it can be focused instead of just the Input
|
|
331
|
-
const [footerFocused, setFooterFocused] = useState(false);
|
|
332
|
-
const footerElement = document.querySelector(
|
|
333
|
-
"#footerElement",
|
|
334
|
-
) as HTMLElement;
|
|
335
|
-
|
|
336
|
-
const footerFocusedCallback = useCallbackRef(() => {
|
|
337
|
-
setFooterFocused(true);
|
|
338
|
-
});
|
|
339
|
-
const footerBlurCallback = useCallbackRef(() => {
|
|
340
|
-
setFooterFocused(false);
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
useEffect(() => {
|
|
344
|
-
footerElement?.addEventListener("focus", footerFocusedCallback);
|
|
345
|
-
footerElement?.addEventListener("blur", footerBlurCallback);
|
|
346
|
-
|
|
347
|
-
return () => {
|
|
348
|
-
footerElement?.removeEventListener("focus", footerFocusedCallback);
|
|
349
|
-
footerElement?.removeEventListener("blur", footerBlurCallback);
|
|
350
|
-
};
|
|
351
|
-
}, [footerElement, footerFocusedCallback, footerBlurCallback]);
|
|
352
|
-
|
|
353
|
-
const menuVisible = useMemo(
|
|
354
|
-
() => inputFocused || footerFocused,
|
|
355
|
-
[inputFocused, footerFocused],
|
|
356
|
-
);
|
|
357
|
-
|
|
358
|
-
const onRequestHighlightChange = useCallback(
|
|
359
|
-
(event: KeyboardEvent, direction: KeyboardAction) => {
|
|
360
|
-
const indexChange = getRequestedIndexChange({
|
|
361
|
-
event,
|
|
362
|
-
options,
|
|
363
|
-
direction,
|
|
364
|
-
highlightedIndex: highlightedOptionIndex,
|
|
365
|
-
});
|
|
366
|
-
const newPreviousIndex = Math.max(
|
|
367
|
-
0,
|
|
368
|
-
highlightedOptionIndex + indexChange,
|
|
369
|
-
);
|
|
370
|
-
const newNextIndex = Math.min(
|
|
371
|
-
maxIndex,
|
|
372
|
-
highlightedOptionIndex + indexChange,
|
|
373
|
-
);
|
|
374
|
-
if (!menuVisible) return;
|
|
375
|
-
|
|
376
|
-
switch (direction) {
|
|
377
|
-
case KeyboardAction.Previous:
|
|
378
|
-
// If the footer is focused, focus the input
|
|
379
|
-
if (highlightedOptionIndex === maxIndex) {
|
|
380
|
-
inputRef?.current?.focus();
|
|
381
|
-
}
|
|
382
|
-
setHighlightedOptionIndex(newPreviousIndex);
|
|
383
|
-
menuRef?.children[newPreviousIndex]?.scrollIntoView?.({
|
|
384
|
-
behavior: "smooth",
|
|
385
|
-
block: "nearest",
|
|
386
|
-
inline: "start",
|
|
387
|
-
});
|
|
388
|
-
|
|
389
|
-
break;
|
|
390
|
-
case KeyboardAction.Next: {
|
|
391
|
-
setHighlightedOptionIndex(newNextIndex);
|
|
392
|
-
const nextElement = menuRef?.children[newNextIndex];
|
|
393
|
-
const footerHeight = footerElement?.offsetHeight || 0;
|
|
394
|
-
|
|
395
|
-
if (nextElement) {
|
|
396
|
-
const rect = nextElement.getBoundingClientRect();
|
|
397
|
-
const menuRect = menuRef.getBoundingClientRect();
|
|
398
|
-
|
|
399
|
-
// If element is hidden behind footer
|
|
400
|
-
if (rect.bottom > menuRect.bottom - footerHeight) {
|
|
401
|
-
// Calculate exact scroll position needed
|
|
402
|
-
const scrollOffset =
|
|
403
|
-
rect.bottom - (menuRect.bottom - footerHeight);
|
|
404
|
-
menuRef.scrollTop += scrollOffset;
|
|
405
|
-
} else {
|
|
406
|
-
nextElement.scrollIntoView({
|
|
407
|
-
behavior: "smooth",
|
|
408
|
-
block: "nearest",
|
|
409
|
-
inline: "start",
|
|
410
|
-
});
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
if (newNextIndex === maxIndex) {
|
|
415
|
-
footerElement?.focus();
|
|
416
|
-
}
|
|
417
|
-
break;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
case KeyboardAction.Select:
|
|
421
|
-
// Don't select the footer
|
|
422
|
-
highlightedOptionIndex < maxIndex &&
|
|
423
|
-
onOptionSelect(options[highlightedOptionIndex]);
|
|
424
|
-
|
|
425
|
-
menuRef?.children[highlightedOptionIndex]?.scrollIntoView?.({
|
|
426
|
-
behavior: "smooth",
|
|
427
|
-
block: "nearest",
|
|
428
|
-
inline: "start",
|
|
429
|
-
});
|
|
430
|
-
if (highlightedOptionIndex === maxIndex) {
|
|
431
|
-
footerElement?.click();
|
|
432
|
-
}
|
|
433
|
-
break;
|
|
434
|
-
}
|
|
435
|
-
},
|
|
436
|
-
[highlightedOptionIndex, options, onOptionSelect],
|
|
437
|
-
);
|
|
438
|
-
useCustomKeyboardNavigation({ onRequestHighlightChange });
|
|
439
|
-
|
|
440
|
-
return (
|
|
441
|
-
<MenuWrapper visible={menuVisible}>
|
|
442
|
-
{options.map((option, index) => {
|
|
443
|
-
const label = option.status;
|
|
444
|
-
const status = option.status === "Active" ? "success" : "informative";
|
|
445
|
-
return (
|
|
446
|
-
<BaseMenuOption
|
|
447
|
-
addSeparators={true}
|
|
448
|
-
isHighlighted={index === highlightedOptionIndex}
|
|
449
|
-
onOptionSelect={onOptionSelect}
|
|
450
|
-
option={option}
|
|
451
|
-
key={option.label}
|
|
452
|
-
>
|
|
453
|
-
<Flex template={["grow", "shrink"]}>
|
|
454
|
-
<Flex align="start" template={["shrink", "grow"]}>
|
|
455
|
-
<Content spacing="minuscule">
|
|
456
|
-
{selectedOption === option && (
|
|
457
|
-
<Icon name="checkmark" size="small" />
|
|
458
|
-
)}
|
|
459
|
-
<Text>{option.label}</Text>
|
|
460
|
-
<Text variation="subdued">{option.address}</Text>
|
|
461
|
-
<Text variation="subdued">{option.contact}</Text>
|
|
462
|
-
</Content>
|
|
463
|
-
</Flex>
|
|
464
|
-
<StatusLabel status={status} label={label} />
|
|
465
|
-
</Flex>
|
|
466
|
-
</BaseMenuOption>
|
|
467
|
-
);
|
|
468
|
-
})}
|
|
469
|
-
{footer}
|
|
470
|
-
</MenuWrapper>
|
|
471
|
-
);
|
|
472
|
-
}
|
|
473
|
-
```
|
|
474
|
-
|
|
475
|
-
#### Rendering the Menu Content
|
|
476
|
-
|
|
477
|
-
The content in the Menu is wrapped by the `<MenuWrapper>` component which
|
|
478
|
-
handles attaching the menu to the input. Our custom options are wrapped by the
|
|
479
|
-
`BaseMenuOption` component which handles the base styling and functionality for
|
|
480
|
-
the options. Inside the `BaseMenuOption` component the content that we want to
|
|
481
|
-
render is provided as children.
|
|
482
|
-
|
|
483
|
-
**Note:** For the footer to be sticky at the bottom of the menu without being
|
|
484
|
-
scrollable, the footer needs to be wrapped in a `div` with `position: "sticky"`
|
|
485
|
-
and `bottom: 0`.
|
|
486
|
-
|
|
487
|
-
**Menu Option Content (Click to Reveal Code)**
|
|
488
|
-
|
|
489
|
-
```tsx
|
|
490
|
-
function addNewClient() {
|
|
491
|
-
window.alert("Add new client");
|
|
492
|
-
}
|
|
493
|
-
const footer = (
|
|
494
|
-
<div style={{ position: "sticky", bottom: 0 }}>
|
|
495
|
-
<Button
|
|
496
|
-
label="+ Add new client"
|
|
497
|
-
onClick={addNewClient}
|
|
498
|
-
id="footerElement"
|
|
499
|
-
size="small"
|
|
500
|
-
fullWidth
|
|
501
|
-
type="tertiary"
|
|
502
|
-
/>
|
|
503
|
-
</div>
|
|
504
|
-
);
|
|
505
|
-
<MenuWrapper visible={menuVisible}>
|
|
506
|
-
{options.map((option, index) => {
|
|
507
|
-
const label = option.status;
|
|
508
|
-
const status = option.status === "Active" ? "success" : "informative";
|
|
509
|
-
|
|
510
|
-
return (
|
|
511
|
-
<BaseMenuOption
|
|
512
|
-
addSeparators={true}
|
|
513
|
-
isHighlighted={index === highlightedOptionIndex}
|
|
514
|
-
onOptionSelect={onOptionSelect}
|
|
515
|
-
option={option}
|
|
516
|
-
key={option.label}
|
|
517
|
-
>
|
|
518
|
-
<Flex template={["grow", "shrink"]}>
|
|
519
|
-
<Flex align="start" template={["shrink", "grow"]}>
|
|
520
|
-
<Content spacing="minuscule">
|
|
521
|
-
{selectedOption === option && (
|
|
522
|
-
<Icon name="checkmark" size="small" />
|
|
523
|
-
)}
|
|
524
|
-
<Text>{option.label}</Text>
|
|
525
|
-
<Text variation="subdued">{option.address}</Text>
|
|
526
|
-
<Text variation="subdued">{option.contact}</Text>
|
|
527
|
-
</Content>
|
|
528
|
-
</Flex>
|
|
529
|
-
<StatusLabel status={status} label={label} />
|
|
530
|
-
</Flex>
|
|
531
|
-
</BaseMenuOption>
|
|
532
|
-
);
|
|
533
|
-
})}
|
|
534
|
-
{footer}
|
|
535
|
-
</MenuWrapper>
|
|
536
|
-
```
|
|
537
|
-
|
|
538
|
-
#### Keyboard Navigation
|
|
539
|
-
|
|
540
|
-
The following code is the keyboard navigation logic for the menu. It is used to
|
|
541
|
-
handle the keyboard navigation of the options and the footer.
|
|
542
|
-
|
|
543
|
-
* `highlightedOptionIndex` is the index of the option that is currently
|
|
544
|
-
highlighted. It is initially set to -1 to account for the footer being the
|
|
545
|
-
first option when options are being initialized.
|
|
546
|
-
* `maxIndex` is the index of the footer.
|
|
547
|
-
* `footerFocused` is a boolean that indicates if the footer is focused. We need
|
|
548
|
-
to track this because it can be focused instead of just the Input and if we
|
|
549
|
-
don't then it is possible for the Menu to remain opened
|
|
550
|
-
* `menuVisible` is a boolean that indicates if the menu is visible.
|
|
551
|
-
* `onRequestHighlightChange` is a function that is called when the user
|
|
552
|
-
navigates the menu.
|
|
553
|
-
* `getRequestedIndexChange` is a helper function that is used to get the index
|
|
554
|
-
change based on the keyboard event, direction and if the option is a group.
|
|
555
|
-
* `KeyboardAction.Next` case should calculate the exact scroll position needed
|
|
556
|
-
with the footer height offset in mind, when the next element is hidden behind
|
|
557
|
-
the footer.
|
|
558
|
-
|
|
559
|
-
**Keyboard Navigation (Click to Reveal Code)**
|
|
560
|
-
|
|
561
|
-
```tsx
|
|
562
|
-
// Set to -1 to account for the footer being the first option when options are being initialized
|
|
563
|
-
const INITIAL_HIGHLIGHTED_OPTION_INDEX = -1;
|
|
564
|
-
|
|
565
|
-
const [highlightedOptionIndex, setHighlightedOptionIndex] = useState(
|
|
566
|
-
INITIAL_HIGHLIGHTED_OPTION_INDEX,
|
|
567
|
-
);
|
|
568
|
-
// Length of options + 1 to account for the footer
|
|
569
|
-
const maxIndex = options.length - 1 + 1;
|
|
570
|
-
|
|
571
|
-
// We need to track the footer focus state because it can be focused instead of just the Input
|
|
572
|
-
const [footerFocused, setFooterFocused] = useState(false);
|
|
573
|
-
const footerElement = document.querySelector(
|
|
574
|
-
"#footerElement",
|
|
575
|
-
) as HTMLElement;
|
|
576
|
-
|
|
577
|
-
const footerFocusedCallback = useCallbackRef(() => {
|
|
578
|
-
setFooterFocused(true);
|
|
579
|
-
});
|
|
580
|
-
const footerBlurCallback = useCallbackRef(() => {
|
|
581
|
-
setFooterFocused(false);
|
|
582
|
-
});
|
|
583
|
-
|
|
584
|
-
useEffect(() => {
|
|
585
|
-
footerElement?.addEventListener("focus", footerFocusedCallback);
|
|
586
|
-
footerElement?.addEventListener("blur", footerBlurCallback);
|
|
587
|
-
|
|
588
|
-
return () => {
|
|
589
|
-
footerElement?.removeEventListener("focus", footerFocusedCallback);
|
|
590
|
-
footerElement?.removeEventListener("blur", footerBlurCallback);
|
|
591
|
-
};
|
|
592
|
-
}, [footerElement, footerFocusedCallback, footerBlurCallback]);
|
|
593
|
-
|
|
594
|
-
const menuVisible = useMemo(
|
|
595
|
-
() => inputFocused || footerFocused,
|
|
596
|
-
[inputFocused, footerFocused],
|
|
597
|
-
);
|
|
598
|
-
|
|
599
|
-
const onRequestHighlightChange = useCallback(
|
|
600
|
-
(event: KeyboardEvent, direction: KeyboardAction) => {
|
|
601
|
-
const indexChange = getRequestedIndexChange({
|
|
602
|
-
event,
|
|
603
|
-
options,
|
|
604
|
-
direction,
|
|
605
|
-
highlightedIndex: highlightedOptionIndex,
|
|
606
|
-
});
|
|
607
|
-
const newPreviousIndex = Math.max(
|
|
608
|
-
0,
|
|
609
|
-
highlightedOptionIndex + indexChange,
|
|
610
|
-
);
|
|
611
|
-
const newNextIndex = Math.min(
|
|
612
|
-
maxIndex,
|
|
613
|
-
highlightedOptionIndex + indexChange,
|
|
614
|
-
);
|
|
615
|
-
if (!menuVisible) return;
|
|
616
|
-
|
|
617
|
-
switch (direction) {
|
|
618
|
-
case KeyboardAction.Previous:
|
|
619
|
-
// If the footer is focused, focus the input
|
|
620
|
-
if (highlightedOptionIndex === maxIndex) {
|
|
621
|
-
inputRef?.current?.focus();
|
|
622
|
-
}
|
|
623
|
-
setHighlightedOptionIndex(newPreviousIndex);
|
|
624
|
-
menuRef?.children[newPreviousIndex]?.scrollIntoView?.({
|
|
625
|
-
behavior: "smooth",
|
|
626
|
-
block: "nearest",
|
|
627
|
-
inline: "start",
|
|
628
|
-
});
|
|
629
|
-
|
|
630
|
-
break;
|
|
631
|
-
case KeyboardAction.Next: {
|
|
632
|
-
setHighlightedOptionIndex(newNextIndex);
|
|
633
|
-
const nextElement = menuRef?.children[newNextIndex];
|
|
634
|
-
const footerHeight = footerElement?.offsetHeight || 0;
|
|
635
|
-
|
|
636
|
-
if (nextElement) {
|
|
637
|
-
const rect = nextElement.getBoundingClientRect();
|
|
638
|
-
const menuRect = menuRef.getBoundingClientRect();
|
|
639
|
-
|
|
640
|
-
// If element is hidden behind footer
|
|
641
|
-
if (rect.bottom > menuRect.bottom - footerHeight) {
|
|
642
|
-
// Calculate exact scroll position needed
|
|
643
|
-
const scrollOffset =
|
|
644
|
-
rect.bottom - (menuRect.bottom - footerHeight);
|
|
645
|
-
menuRef.scrollTop += scrollOffset;
|
|
646
|
-
} else {
|
|
647
|
-
nextElement.scrollIntoView({
|
|
648
|
-
behavior: "smooth",
|
|
649
|
-
block: "nearest",
|
|
650
|
-
inline: "start",
|
|
651
|
-
});
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
if (newNextIndex === maxIndex) {
|
|
656
|
-
footerElement?.focus();
|
|
657
|
-
}
|
|
658
|
-
break;
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
case KeyboardAction.Select:
|
|
662
|
-
// Don't select the footer
|
|
663
|
-
highlightedOptionIndex < maxIndex &&
|
|
664
|
-
onOptionSelect(options[highlightedOptionIndex]);
|
|
665
|
-
|
|
666
|
-
menuRef?.children[highlightedOptionIndex]?.scrollIntoView?.({
|
|
667
|
-
behavior: "smooth",
|
|
668
|
-
block: "nearest",
|
|
669
|
-
inline: "start",
|
|
670
|
-
});
|
|
671
|
-
if (highlightedOptionIndex === maxIndex) {
|
|
672
|
-
footerElement?.click();
|
|
673
|
-
}
|
|
674
|
-
break;
|
|
675
|
-
}
|
|
676
|
-
},
|
|
677
|
-
[highlightedOptionIndex, options, onOptionSelect],
|
|
678
|
-
);
|
|
679
|
-
useCustomKeyboardNavigation({ onRequestHighlightChange });
|
|
680
|
-
```
|
|
681
|
-
|
|
682
|
-
### Advanced Custom Section Headings
|
|
683
|
-
|
|
684
|
-
In this example we will override the rendering of the menu to render the an
|
|
685
|
-
Autocomplete with Section Headings and Custom Elements in the Section Headings.
|
|
686
|
-
|
|
687
|
-
**Note:** Similar to the Advanced Custom Menu Override, due to the complexity of
|
|
688
|
-
the example, we will not be using the `useKeyboardNavigation` hook to handle the
|
|
689
|
-
keyboard navigation of the options.
|
|
690
|
-
|
|
691
|
-
```tsx
|
|
692
|
-
<AutocompleteAdvancedSectionHeadingExample />
|
|
693
|
-
```
|
|
694
|
-
|
|
695
|
-
**Full Example (Click to Reveal Code)**
|
|
696
|
-
|
|
697
|
-
```tsx
|
|
698
|
-
interface CustomOptionForGroup {
|
|
699
|
-
value: number;
|
|
700
|
-
label: string;
|
|
701
|
-
status: string;
|
|
702
|
-
}
|
|
703
|
-
interface CustomGroupOption {
|
|
704
|
-
label: string;
|
|
705
|
-
icon: IconNames;
|
|
706
|
-
options: CustomOptionForGroup[];
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
const SectionHeadingOptionsCustom: CustomGroupOption[] = [
|
|
710
|
-
{
|
|
711
|
-
label: "Quotes",
|
|
712
|
-
icon: "quote",
|
|
713
|
-
options: [
|
|
714
|
-
{ value: 1, label: "Quote for Acme Corp", status: "Draft" },
|
|
715
|
-
{ value: 2, label: "Quote for Stark Industries", status: "Approved" },
|
|
716
|
-
{
|
|
717
|
-
value: 3,
|
|
718
|
-
label: "Quote for Wayne Enterprises",
|
|
719
|
-
status: "Awaiting approval",
|
|
720
|
-
},
|
|
721
|
-
{ value: 4, label: "Quote for Umbrella Corp", status: "Paid" },
|
|
722
|
-
],
|
|
723
|
-
},
|
|
724
|
-
{
|
|
725
|
-
icon: "invoice",
|
|
726
|
-
label: "Invoices",
|
|
727
|
-
options: [
|
|
728
|
-
{ value: 5, label: "Invoice for Acme Corp", status: "Draft" },
|
|
729
|
-
{
|
|
730
|
-
value: 6,
|
|
731
|
-
label: "Invoice for Stark Industries",
|
|
732
|
-
status: "Awaiting payment",
|
|
733
|
-
},
|
|
734
|
-
{
|
|
735
|
-
value: 7,
|
|
736
|
-
label: "Invoice for Wayne Enterprises",
|
|
737
|
-
status: "Awaiting payment",
|
|
738
|
-
},
|
|
739
|
-
{ value: 8, label: "Invoice for Umbrella Corp", status: "Paid" },
|
|
740
|
-
],
|
|
741
|
-
},
|
|
742
|
-
];
|
|
743
|
-
|
|
744
|
-
interface CustomElementOption {
|
|
745
|
-
sectionLabel: string;
|
|
746
|
-
indexToInsertAfter: number;
|
|
747
|
-
}
|
|
748
|
-
type OptionsWithExtraElements =
|
|
749
|
-
| CustomGroupOption
|
|
750
|
-
| CustomOptionForGroup
|
|
751
|
-
| CustomElementOption;
|
|
752
|
-
|
|
753
|
-
export function AdvancedSectionHeadingTemplate() {
|
|
754
|
-
const [value, setValue] = useState<CustomOptionForGroup | undefined>();
|
|
755
|
-
|
|
756
|
-
return (
|
|
757
|
-
<Autocomplete
|
|
758
|
-
placeholder="Search for something under a section heading"
|
|
759
|
-
initialOptions={SectionHeadingOptionsCustom}
|
|
760
|
-
value={value}
|
|
761
|
-
onChange={newValue => setValue(newValue)}
|
|
762
|
-
getOptions={() => SectionHeadingOptionsCustom}
|
|
763
|
-
customRenderMenu={props => (
|
|
764
|
-
<AdvancedSectionHeadingCustomMenuContent {...props} />
|
|
765
|
-
)}
|
|
766
|
-
/>
|
|
767
|
-
);
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
function AdvancedSectionHeadingCustomMenuContent({
|
|
771
|
-
options,
|
|
772
|
-
selectedOption,
|
|
773
|
-
onOptionSelect,
|
|
774
|
-
inputFocused,
|
|
775
|
-
inputRef,
|
|
776
|
-
MenuWrapper,
|
|
777
|
-
menuRef,
|
|
778
|
-
}: CustomOptionsMenuProp<CustomGroupOption, CustomOptionForGroup>) {
|
|
779
|
-
const { optionsWithExtraElements, extraElementFocused } =
|
|
780
|
-
useCustomSectionOptions(options);
|
|
781
|
-
// Set to 1 to account for the first option being the section heading
|
|
782
|
-
const initialHighlight = 1
|
|
783
|
-
|
|
784
|
-
const [highlightedOptionIndex, setHighlightedOptionIndex] =
|
|
785
|
-
useState(initialHighlight);
|
|
786
|
-
// Length of options -1 and the number of extra elements
|
|
787
|
-
|
|
788
|
-
const maxIndex = optionsWithExtraElements.length - 1;
|
|
789
|
-
|
|
790
|
-
const menuVisible = useMemo(
|
|
791
|
-
() => extraElementFocused || inputFocused,
|
|
792
|
-
[extraElementFocused, inputFocused],
|
|
793
|
-
);
|
|
794
|
-
const onRequestHighlightChange = useCallback(
|
|
795
|
-
// eslint-disable-next-line max-statements
|
|
796
|
-
(_event: KeyboardEvent, direction: KeyboardAction) => {
|
|
797
|
-
const requestedIndex =
|
|
798
|
-
optionsWithExtraElements[highlightedOptionIndex + direction];
|
|
799
|
-
// Default index change to the direction. If the requested index is not an extra element,
|
|
800
|
-
// then we need to adjust the index change to account for the section heading
|
|
801
|
-
let indexChange = direction;
|
|
802
|
-
|
|
803
|
-
if (requestedIndex && !isExtraElement(requestedIndex)) {
|
|
804
|
-
indexChange =
|
|
805
|
-
requestedIndex && isOptionGroup(requestedIndex)
|
|
806
|
-
? direction + direction
|
|
807
|
-
: direction;
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
const newPreviousIndex = Math.max(
|
|
811
|
-
0,
|
|
812
|
-
highlightedOptionIndex + indexChange,
|
|
813
|
-
);
|
|
814
|
-
const newNextIndex = Math.min(
|
|
815
|
-
maxIndex,
|
|
816
|
-
highlightedOptionIndex + indexChange,
|
|
817
|
-
);
|
|
818
|
-
|
|
819
|
-
if (!menuVisible) return;
|
|
820
|
-
|
|
821
|
-
switch (direction) {
|
|
822
|
-
case KeyboardAction.Previous:
|
|
823
|
-
// If the option is an extra element, focus the extra element. Else focus the input
|
|
824
|
-
if (isExtraElement(optionsWithExtraElements[newPreviousIndex])) {
|
|
825
|
-
const element = document.getElementById(
|
|
826
|
-
optionsWithExtraElements[newPreviousIndex].sectionLabel,
|
|
827
|
-
) as HTMLElement;
|
|
828
|
-
element?.focus();
|
|
829
|
-
} else {
|
|
830
|
-
inputRef?.current?.focus();
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
setHighlightedOptionIndex(newPreviousIndex);
|
|
834
|
-
menuRef?.children[newPreviousIndex]?.scrollIntoView?.({
|
|
835
|
-
behavior: "smooth",
|
|
836
|
-
block: "nearest",
|
|
837
|
-
inline: "start",
|
|
838
|
-
});
|
|
839
|
-
|
|
840
|
-
break;
|
|
841
|
-
case KeyboardAction.Next:
|
|
842
|
-
setHighlightedOptionIndex(newNextIndex);
|
|
843
|
-
menuRef?.children[newNextIndex]?.scrollIntoView?.({
|
|
844
|
-
behavior: "smooth",
|
|
845
|
-
block: "nearest",
|
|
846
|
-
inline: "start",
|
|
847
|
-
});
|
|
848
|
-
|
|
849
|
-
// If the option is an extra element, focus the extra element. Else focus the input
|
|
850
|
-
if (isExtraElement(optionsWithExtraElements[newNextIndex])) {
|
|
851
|
-
const element = document.getElementById(
|
|
852
|
-
optionsWithExtraElements[newNextIndex].sectionLabel,
|
|
853
|
-
) as HTMLElement;
|
|
854
|
-
element?.focus();
|
|
855
|
-
} else {
|
|
856
|
-
inputRef?.current?.focus();
|
|
857
|
-
}
|
|
858
|
-
break;
|
|
859
|
-
|
|
860
|
-
case KeyboardAction.Select:
|
|
861
|
-
menuRef?.children[highlightedOptionIndex]?.scrollIntoView?.({
|
|
862
|
-
behavior: "smooth",
|
|
863
|
-
block: "nearest",
|
|
864
|
-
inline: "start",
|
|
865
|
-
});
|
|
866
|
-
|
|
867
|
-
// If the option is an extra element, click the extra element. Else check it is not an option group and select the option
|
|
868
|
-
if (
|
|
869
|
-
isExtraElement(optionsWithExtraElements[highlightedOptionIndex])
|
|
870
|
-
) {
|
|
871
|
-
const element = document.getElementById(
|
|
872
|
-
optionsWithExtraElements[highlightedOptionIndex].sectionLabel,
|
|
873
|
-
) as HTMLElement;
|
|
874
|
-
element?.click();
|
|
875
|
-
} else if (
|
|
876
|
-
!isOptionGroup(optionsWithExtraElements[highlightedOptionIndex])
|
|
877
|
-
) {
|
|
878
|
-
onOptionSelect(optionsWithExtraElements[highlightedOptionIndex]);
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
break;
|
|
882
|
-
}
|
|
883
|
-
},
|
|
884
|
-
[
|
|
885
|
-
highlightedOptionIndex,
|
|
886
|
-
options,
|
|
887
|
-
onOptionSelect,
|
|
888
|
-
extraElementFocused,
|
|
889
|
-
maxIndex,
|
|
890
|
-
optionsWithExtraElements,
|
|
891
|
-
],
|
|
892
|
-
);
|
|
893
|
-
useCustomKeyboardNavigation({ onRequestHighlightChange });
|
|
894
|
-
|
|
895
|
-
${AdvancedSectionMenuRenderingCode}
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
function AdvancedSectionFooterButton({
|
|
899
|
-
label,
|
|
900
|
-
id,
|
|
901
|
-
}: {
|
|
902
|
-
readonly label: string;
|
|
903
|
-
readonly id: string;
|
|
904
|
-
}) {
|
|
905
|
-
return (
|
|
906
|
-
<Button
|
|
907
|
-
label={\`+ Add new \${label}\`}
|
|
908
|
-
onClick={() => alert(\`Add new \${label}\`)}
|
|
909
|
-
size="small"
|
|
910
|
-
id={id}
|
|
911
|
-
fullWidth
|
|
912
|
-
type="tertiary"
|
|
913
|
-
/>
|
|
914
|
-
);
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
${AdvancedSectionSelectableOptionCode}
|
|
918
|
-
${useCustomSectionOptionsCode}
|
|
919
|
-
${isExtraElementCode}
|
|
920
|
-
```
|
|
921
|
-
|
|
922
|
-
#### Rendering the Menu Content
|
|
923
|
-
|
|
924
|
-
The content in the Menu is wrapped by the `<MenuWrapper>` component which
|
|
925
|
-
handles attaching the menu to the input. We use the `optionsWithExtraElements`
|
|
926
|
-
to render the options. The `optionsWithExtraElements` is the options with the
|
|
927
|
-
extra elements inserted in the correct positions. This array is generated using
|
|
928
|
-
a custom hook called `useCustomSectionOptions`. (explanation of the hook below).
|
|
929
|
-
|
|
930
|
-
**Menu Content Rendering (Click to Reveal Code)**
|
|
931
|
-
|
|
932
|
-
```tsx
|
|
933
|
-
<MenuWrapper visible={menuVisible}>
|
|
934
|
-
{optionsWithExtraElements.map((option, index) => {
|
|
935
|
-
if (isExtraElement(option)) {
|
|
936
|
-
return (
|
|
937
|
-
<AdvancedSectionFooterButton
|
|
938
|
-
label={option.sectionLabel}
|
|
939
|
-
id={option.sectionLabel}
|
|
940
|
-
key={\`\${option.sectionLabel}-create-new\`}
|
|
941
|
-
/>
|
|
942
|
-
);
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
if (isOptionGroup(option)) {
|
|
946
|
-
return (
|
|
947
|
-
<BaseMenuGroupOption key={option.label}>
|
|
948
|
-
<Flex template={["grow", "shrink"]}>
|
|
949
|
-
<Heading level={5}>{option.label}</Heading>
|
|
950
|
-
<Icon name={option.icon} />
|
|
951
|
-
</Flex>
|
|
952
|
-
</BaseMenuGroupOption>
|
|
953
|
-
);
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
return (
|
|
957
|
-
<SelectableOption
|
|
958
|
-
isSelected={isOptionSelected(selectedOption, option)}
|
|
959
|
-
key={option.value}
|
|
960
|
-
option={option}
|
|
961
|
-
index={index}
|
|
962
|
-
highlightedOptionIndex={highlightedOptionIndex}
|
|
963
|
-
onOptionSelect={onOptionSelect}
|
|
964
|
-
/>
|
|
965
|
-
);
|
|
966
|
-
})}
|
|
967
|
-
</MenuWrapper>
|
|
968
|
-
```
|
|
969
|
-
|
|
970
|
-
#### Utilities used for Custom Rendering
|
|
971
|
-
|
|
972
|
-
The following utilities were created in this example to help with custom
|
|
973
|
-
rendering:
|
|
974
|
-
|
|
975
|
-
`useCustomSectionOptions` determines the indices of the options that should have
|
|
976
|
-
extra elements inserted after them. It then inserts the extra elements into an
|
|
977
|
-
array and returns it with the extra elements inserted. This is used when the
|
|
978
|
-
options are being rendered.
|
|
979
|
-
|
|
980
|
-
**useCustomSectionOptions (Click to Reveal Code)**
|
|
981
|
-
|
|
982
|
-
```tsx
|
|
983
|
-
/**
|
|
984
|
-
* Get the indices of the options that should have extra elements inserted after them
|
|
985
|
-
* @param options - The options to get the extra element indices for
|
|
986
|
-
* @returns The indices of the options that should have extra elements inserted after them
|
|
987
|
-
*/
|
|
988
|
-
function getExtraElementIndices(
|
|
989
|
-
options: (CustomOptionForGroup | CustomGroupOption)[],
|
|
990
|
-
) {
|
|
991
|
-
const sectionsOptions = options.filter(isOptionGroup);
|
|
992
|
-
|
|
993
|
-
return sectionsOptions.map(({ label, options: optionsInSection }) => {
|
|
994
|
-
const indexToInsertAfter = options.findIndex(
|
|
995
|
-
opt => opt === optionsInSection[optionsInSection.length - 1],
|
|
996
|
-
);
|
|
997
|
-
|
|
998
|
-
return {
|
|
999
|
-
sectionLabel: label,
|
|
1000
|
-
indexToInsertAfter,
|
|
1001
|
-
};
|
|
1002
|
-
});
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
/**
|
|
1006
|
-
* Creates a new array with the extra elements inserted into the options array
|
|
1007
|
-
* @param options - The options to insert the extra elements into
|
|
1008
|
-
* @param extraElements - The extra elements to insert into the options
|
|
1009
|
-
* @returns The options with the extra elements inserted
|
|
1010
|
-
*/
|
|
1011
|
-
function insertExtraElements(
|
|
1012
|
-
options: (CustomOptionForGroup | CustomGroupOption)[],
|
|
1013
|
-
extraElements: CustomElementOption[],
|
|
1014
|
-
) {
|
|
1015
|
-
return options.reduce<Array<OptionsWithExtraElements>>(
|
|
1016
|
-
(acc, option, index) => {
|
|
1017
|
-
const shouldInsertExtraElement = extraElements.find(
|
|
1018
|
-
({ indexToInsertAfter }) => indexToInsertAfter === index,
|
|
1019
|
-
);
|
|
1020
|
-
|
|
1021
|
-
if (shouldInsertExtraElement) {
|
|
1022
|
-
return [
|
|
1023
|
-
...acc,
|
|
1024
|
-
option,
|
|
1025
|
-
{
|
|
1026
|
-
sectionLabel: shouldInsertExtraElement.sectionLabel,
|
|
1027
|
-
indexToInsertAfter: shouldInsertExtraElement.indexToInsertAfter,
|
|
1028
|
-
},
|
|
1029
|
-
];
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
return [...acc, option];
|
|
1033
|
-
},
|
|
1034
|
-
[],
|
|
1035
|
-
);
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
/**
|
|
1039
|
-
* Custom hook to handle the section options with extra elements
|
|
1040
|
-
* @param options - The options to handle
|
|
1041
|
-
* @returns The options with the extra elements inserted and the focus state for the extra elements
|
|
1042
|
-
*/
|
|
1043
|
-
function useCustomSectionOptions(
|
|
1044
|
-
options: (CustomGroupOption | CustomOptionForGroup)[],
|
|
1045
|
-
) {
|
|
1046
|
-
const extraElements = useMemo(
|
|
1047
|
-
() => getExtraElementIndices(options),
|
|
1048
|
-
[options],
|
|
1049
|
-
);
|
|
1050
|
-
const optionsWithExtraElements = useMemo(
|
|
1051
|
-
() => insertExtraElements(options, extraElements),
|
|
1052
|
-
[options, extraElements],
|
|
1053
|
-
);
|
|
1054
|
-
// We need to track the focus state for the extra elements because it can be focused instead of just the Input
|
|
1055
|
-
const [extraElementFocused, setExtraElementFocused] = useState(false);
|
|
1056
|
-
|
|
1057
|
-
const extraElementFocusCallback = useCallbackRef(() => {
|
|
1058
|
-
setExtraElementFocused(true);
|
|
1059
|
-
});
|
|
1060
|
-
const extraElementBlurCallback = useCallbackRef(() => {
|
|
1061
|
-
setExtraElementFocused(false);
|
|
1062
|
-
});
|
|
1063
|
-
// Add event listeners to the extra elements for focus and blur
|
|
1064
|
-
useEffect(() => {
|
|
1065
|
-
extraElements.forEach(({ sectionLabel }) => {
|
|
1066
|
-
const element = document.getElementById(sectionLabel) as HTMLElement;
|
|
1067
|
-
element?.addEventListener("focus", extraElementFocusCallback);
|
|
1068
|
-
element?.addEventListener("blur", extraElementBlurCallback);
|
|
1069
|
-
});
|
|
1070
|
-
|
|
1071
|
-
return () => {
|
|
1072
|
-
extraElements.forEach(({ sectionLabel }) => {
|
|
1073
|
-
const element = document.getElementById(sectionLabel) as HTMLElement;
|
|
1074
|
-
element?.removeEventListener("focus", extraElementFocusCallback);
|
|
1075
|
-
element?.removeEventListener("blur", extraElementBlurCallback);
|
|
1076
|
-
});
|
|
1077
|
-
};
|
|
1078
|
-
}, [extraElements]);
|
|
1079
|
-
|
|
1080
|
-
return {
|
|
1081
|
-
optionsWithExtraElements,
|
|
1082
|
-
extraElementFocused,
|
|
1083
|
-
};
|
|
1084
|
-
}
|
|
1085
|
-
```
|
|
1086
|
-
|
|
1087
|
-
`isExtraElement` is a helper function that is used to determine if the option is
|
|
1088
|
-
an extra element.
|
|
1089
|
-
|
|
1090
|
-
**isExtraElement (Click to Reveal Code)**
|
|
1091
|
-
|
|
1092
|
-
```tsx
|
|
1093
|
-
/**
|
|
1094
|
-
* Checks if the option is an extra element
|
|
1095
|
-
* @param option - The option to check
|
|
1096
|
-
* @returns Whether the option is an extra element
|
|
1097
|
-
*/
|
|
1098
|
-
function isExtraElement(option: object): option is CustomElementOption {
|
|
1099
|
-
return option && "sectionLabel" in option && "indexToInsertAfter" in option;
|
|
1100
|
-
}
|
|
1101
|
-
```
|
|
1102
|
-
|
|
1103
|
-
`SelectableOption` is a component that is used to render the selectable options
|
|
1104
|
-
in the menu.
|
|
1105
|
-
|
|
1106
|
-
**SelectableOption (Click to Reveal Code)**
|
|
1107
|
-
|
|
1108
|
-
```tsx
|
|
1109
|
-
/**
|
|
1110
|
-
* Renders the selectable option
|
|
1111
|
-
* @param option - The option to render
|
|
1112
|
-
* @param index - The index of the option
|
|
1113
|
-
* @param highlightedOptionIndex - The index of the highlighted option
|
|
1114
|
-
* @param onOptionSelect - The function to call when the option is selected
|
|
1115
|
-
* @param isSelected - Whether the option is selected
|
|
1116
|
-
*/
|
|
1117
|
-
function SelectableOption({
|
|
1118
|
-
option,
|
|
1119
|
-
index,
|
|
1120
|
-
highlightedOptionIndex,
|
|
1121
|
-
onOptionSelect,
|
|
1122
|
-
isSelected,
|
|
1123
|
-
}: {
|
|
1124
|
-
readonly option: CustomOptionForGroup;
|
|
1125
|
-
readonly index: number;
|
|
1126
|
-
readonly highlightedOptionIndex: number;
|
|
1127
|
-
readonly onOptionSelect: (option?: CustomOptionForGroup) => void;
|
|
1128
|
-
readonly isSelected: boolean;
|
|
1129
|
-
}) {
|
|
1130
|
-
let status: StatusIndicatorType = "informative";
|
|
1131
|
-
|
|
1132
|
-
switch (option.status) {
|
|
1133
|
-
case "Paid":
|
|
1134
|
-
case "Approved":
|
|
1135
|
-
status = "success";
|
|
1136
|
-
break;
|
|
1137
|
-
case "Awaiting payment":
|
|
1138
|
-
case "Awaiting approval":
|
|
1139
|
-
status = "warning";
|
|
1140
|
-
break;
|
|
1141
|
-
case "Draft":
|
|
1142
|
-
status = "inactive";
|
|
1143
|
-
break;
|
|
1144
|
-
}
|
|
1145
|
-
|
|
1146
|
-
return (
|
|
1147
|
-
<BaseMenuOption
|
|
1148
|
-
addSeparators={true}
|
|
1149
|
-
isHighlighted={highlightedOptionIndex === index}
|
|
1150
|
-
onOptionSelect={onOptionSelect}
|
|
1151
|
-
option={option}
|
|
1152
|
-
>
|
|
1153
|
-
<Flex template={["grow", "shrink"]}>
|
|
1154
|
-
<Text>
|
|
1155
|
-
{isSelected && <Icon name="checkmark" size="small" />} {option.label}
|
|
1156
|
-
</Text>
|
|
1157
|
-
<StatusLabel label={option.status} status={status} />
|
|
1158
|
-
</Flex>
|
|
1159
|
-
</BaseMenuOption>
|
|
1160
|
-
);
|
|
1161
|
-
}
|
|
1162
|
-
```
|
|
1163
|
-
|
|
1164
|
-
#### Keyboard Navigation
|
|
1165
|
-
|
|
1166
|
-
The following code is the keyboard navigation logic for the menu. It is used to
|
|
1167
|
-
handle the keyboard navigation of the options and the footers in each of the
|
|
1168
|
-
sections.
|
|
1169
|
-
|
|
1170
|
-
The keyboard navigation logic is similar to the Advanced Custom Menu Override.
|
|
1171
|
-
However, we need to account for the extra elements in each of the sections
|
|
1172
|
-
instead of just a single footer.
|
|
1173
|
-
|
|
1174
|
-
`useCustomSectionOptions` returns the options with the extra elements inserted
|
|
1175
|
-
and the `extraElementFocused` state. It provides event listeners for keeping
|
|
1176
|
-
track of the focused state for if an extra element is focused. There are
|
|
1177
|
-
additional checks to see if the requested index is an extra element and if it
|
|
1178
|
-
is, focus the extra element and if not it will focus the input.
|
|
1179
|
-
|
|
1180
|
-
**Keyboard Navigation (Click to Reveal Code)**
|
|
1181
|
-
|
|
1182
|
-
```tsx
|
|
1183
|
-
...
|
|
1184
|
-
// Other code
|
|
1185
|
-
const { optionsWithExtraElements, extraElementFocused } =
|
|
1186
|
-
useCustomSectionOptions(options);
|
|
1187
|
-
// Set to 1 to account for the first option being the section heading
|
|
1188
|
-
const initialHighlight = 1
|
|
1189
|
-
|
|
1190
|
-
const [highlightedOptionIndex, setHighlightedOptionIndex] =
|
|
1191
|
-
useState(initialHighlight);
|
|
1192
|
-
// Length of options -1 and the number of extra elements
|
|
1193
|
-
|
|
1194
|
-
const maxIndex = optionsWithExtraElements.length - 1;
|
|
1195
|
-
|
|
1196
|
-
const menuVisible = useMemo(
|
|
1197
|
-
() => extraElementFocused || inputFocused,
|
|
1198
|
-
[extraElementFocused, inputFocused],
|
|
1199
|
-
);
|
|
1200
|
-
const onRequestHighlightChange = useCallback(
|
|
1201
|
-
// eslint-disable-next-line max-statements
|
|
1202
|
-
(_event: KeyboardEvent, direction: KeyboardAction) => {
|
|
1203
|
-
const requestedIndex =
|
|
1204
|
-
optionsWithExtraElements[highlightedOptionIndex + direction];
|
|
1205
|
-
// Default index change to the direction. If the requested index is not an extra element,
|
|
1206
|
-
// then we need to adjust the index change to account for the section heading
|
|
1207
|
-
let indexChange = direction;
|
|
1208
|
-
|
|
1209
|
-
if (requestedIndex && !isExtraElement(requestedIndex)) {
|
|
1210
|
-
indexChange =
|
|
1211
|
-
requestedIndex && isOptionGroup(requestedIndex)
|
|
1212
|
-
? direction + direction
|
|
1213
|
-
: direction;
|
|
1214
|
-
}
|
|
1215
|
-
|
|
1216
|
-
const newPreviousIndex = Math.max(
|
|
1217
|
-
0,
|
|
1218
|
-
highlightedOptionIndex + indexChange,
|
|
1219
|
-
);
|
|
1220
|
-
const newNextIndex = Math.min(
|
|
1221
|
-
maxIndex,
|
|
1222
|
-
highlightedOptionIndex + indexChange,
|
|
1223
|
-
);
|
|
1224
|
-
|
|
1225
|
-
if (!menuVisible) return;
|
|
1226
|
-
|
|
1227
|
-
switch (direction) {
|
|
1228
|
-
case KeyboardAction.Previous:
|
|
1229
|
-
// If the option is an extra element, focus the extra element. Else focus the input
|
|
1230
|
-
if (isExtraElement(optionsWithExtraElements[newPreviousIndex])) {
|
|
1231
|
-
const element = document.getElementById(
|
|
1232
|
-
optionsWithExtraElements[newPreviousIndex].sectionLabel,
|
|
1233
|
-
) as HTMLElement;
|
|
1234
|
-
element?.focus();
|
|
1235
|
-
} else {
|
|
1236
|
-
inputRef?.current?.focus();
|
|
1237
|
-
}
|
|
1238
|
-
|
|
1239
|
-
setHighlightedOptionIndex(newPreviousIndex);
|
|
1240
|
-
menuRef?.children[newPreviousIndex]?.scrollIntoView?.({
|
|
1241
|
-
behavior: "smooth",
|
|
1242
|
-
block: "nearest",
|
|
1243
|
-
inline: "start",
|
|
1244
|
-
});
|
|
1245
|
-
|
|
1246
|
-
break;
|
|
1247
|
-
case KeyboardAction.Next:
|
|
1248
|
-
setHighlightedOptionIndex(newNextIndex);
|
|
1249
|
-
menuRef?.children[newNextIndex]?.scrollIntoView?.({
|
|
1250
|
-
behavior: "smooth",
|
|
1251
|
-
block: "nearest",
|
|
1252
|
-
inline: "start",
|
|
1253
|
-
});
|
|
1254
|
-
|
|
1255
|
-
// If the option is an extra element, focus the extra element. Else focus the input
|
|
1256
|
-
if (isExtraElement(optionsWithExtraElements[newNextIndex])) {
|
|
1257
|
-
const element = document.getElementById(
|
|
1258
|
-
optionsWithExtraElements[newNextIndex].sectionLabel,
|
|
1259
|
-
) as HTMLElement;
|
|
1260
|
-
element?.focus();
|
|
1261
|
-
} else {
|
|
1262
|
-
inputRef?.current?.focus();
|
|
1263
|
-
}
|
|
1264
|
-
break;
|
|
1265
|
-
|
|
1266
|
-
case KeyboardAction.Select:
|
|
1267
|
-
menuRef?.children[highlightedOptionIndex]?.scrollIntoView?.({
|
|
1268
|
-
behavior: "smooth",
|
|
1269
|
-
block: "nearest",
|
|
1270
|
-
inline: "start",
|
|
1271
|
-
});
|
|
1272
|
-
|
|
1273
|
-
// If the option is an extra element, click the extra element. Else check it is not an option group and select the option
|
|
1274
|
-
if (
|
|
1275
|
-
isExtraElement(optionsWithExtraElements[highlightedOptionIndex])
|
|
1276
|
-
) {
|
|
1277
|
-
const element = document.getElementById(
|
|
1278
|
-
optionsWithExtraElements[highlightedOptionIndex].sectionLabel,
|
|
1279
|
-
) as HTMLElement;
|
|
1280
|
-
element?.click();
|
|
1281
|
-
} else if (
|
|
1282
|
-
!isOptionGroup(optionsWithExtraElements[highlightedOptionIndex])
|
|
1283
|
-
) {
|
|
1284
|
-
onOptionSelect(optionsWithExtraElements[highlightedOptionIndex]);
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
break;
|
|
1288
|
-
}
|
|
1289
|
-
},
|
|
1290
|
-
[
|
|
1291
|
-
highlightedOptionIndex,
|
|
1292
|
-
options,
|
|
1293
|
-
onOptionSelect,
|
|
1294
|
-
extraElementFocused,
|
|
1295
|
-
maxIndex,
|
|
1296
|
-
optionsWithExtraElements,
|
|
1297
|
-
],
|
|
1298
|
-
);
|
|
1299
|
-
useCustomKeyboardNavigation({ onRequestHighlightChange });
|
|
1300
|
-
```
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
## Props
|
|
1304
|
-
|
|
1305
|
-
### Web
|
|
1306
|
-
|
|
1307
|
-
| Prop | Type | Required | Default | Description |
|
|
1308
|
-
|------|------|----------|---------|-------------|
|
|
1309
|
-
| `getOptions` | `(newInputText: string) => (GenericOption | GenericGetOptionsValue)[] | Promise<(GenericOption | GenericGetOptionsValue)[]>` | Yes | — | Called as the user types in the input. The autocomplete will display what is returned from this method to the user as... |
|
|
1310
|
-
| `onChange` | `(newValue?: GenericOptionValue) => void` | Yes | — | Simplified onChange handler that only provides the new value. @param newValue |
|
|
1311
|
-
| `placeholder` | `string` | Yes | — | Hint text that goes above the value once the form is filled out. |
|
|
1312
|
-
| `value` | `Option` | Yes | — | Set Autocomplete value. |
|
|
1313
|
-
| `allowFreeForm` | `boolean` | No | `true` | Allow the autocomplete to use values not from the drop down menu. |
|
|
1314
|
-
| `clearable` | `Clearable` | No | — | Add a clear action on the input that clears the value. You should always use `while-editing` if you want the input t... |
|
|
1315
|
-
| `customRenderMenu` | `(props: CustomOptionsMenuProp<GenericOption | GenericGetOptionsValue, GenericOptionValue>) => ReactElement<...>` | No | — | Override the content rendered in the menu. |
|
|
1316
|
-
| `debounce` | `number` | No | `300` | Debounce in milliseconds for getOptions |
|
|
1317
|
-
| `description` | `ReactNode` | No | — | Further description of the input, can be used for a hint. |
|
|
1318
|
-
| `initialOptions` | `GenericOption[]` | No | — | Initial options to display in the autocomplete. |
|
|
1319
|
-
| `inputRef` | `RefObject<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>` | No | — | @deprecated Use `ref` instead. |
|
|
1320
|
-
| `invalid` | `boolean` | No | — | Highlights the field red to indicate an error. |
|
|
1321
|
-
| `name` | `string` | No | — | Name of the input. |
|
|
1322
|
-
| `onBlur` | `(event?: FocusEvent<Element, Element>) => void` | No | — | Blur callback. |
|
|
1323
|
-
| `onFocus` | `(event?: FocusEvent<Element, Element>) => void` | No | — | Focus callback. |
|
|
1324
|
-
| `prefix` | `Affix` | No | — | Adds a prefix label and icon to the field |
|
|
1325
|
-
| `ref` | `Ref<HTMLInputElement>` | No | — | Allows getting a ref to the component instance. Once the component unmounts, React will set `ref.current` to `null` (... |
|
|
1326
|
-
| `size` | `"large" | "small"` | No | — | Adjusts the interface to either have small or large spacing. |
|
|
1327
|
-
| `suffix` | `{ onClick: () => void; readonly ariaLabel: string; readonly icon: IconNames; readonly label?: string; } | { onClick?: never; ariaLabel?: never; readonly label?: string; readonly icon?: IconNames; }` | No | — | Adds a suffix label and icon with an optional action to the field |
|
|
1328
|
-
| `version` | `1` | No | `1` | Version of the component to use. |
|