@patternfly/react-core 5.4.0-prerelease.8 → 5.4.0-prerelease.9
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 +12 -0
- package/CONTRIBUTING.md +0 -2
- package/components/package.json +1 -1
- package/deprecated/package.json +1 -1
- package/dist/dynamic/components/AboutModal/package.json +1 -1
- package/dist/dynamic/components/Accordion/package.json +1 -1
- package/dist/dynamic/components/ActionList/package.json +1 -1
- package/dist/dynamic/components/Alert/package.json +1 -1
- package/dist/dynamic/components/Avatar/package.json +1 -1
- package/dist/dynamic/components/BackToTop/package.json +1 -1
- package/dist/dynamic/components/Backdrop/package.json +1 -1
- package/dist/dynamic/components/BackgroundImage/package.json +1 -1
- package/dist/dynamic/components/Badge/package.json +1 -1
- package/dist/dynamic/components/Banner/package.json +1 -1
- package/dist/dynamic/components/Brand/package.json +1 -1
- package/dist/dynamic/components/Breadcrumb/package.json +1 -1
- package/dist/dynamic/components/Button/package.json +1 -1
- package/dist/dynamic/components/CalendarMonth/package.json +1 -1
- package/dist/dynamic/components/Card/package.json +1 -1
- package/dist/dynamic/components/Checkbox/package.json +1 -1
- package/dist/dynamic/components/Chip/package.json +1 -1
- package/dist/dynamic/components/ClipboardCopy/package.json +1 -1
- package/dist/dynamic/components/CodeBlock/package.json +1 -1
- package/dist/dynamic/components/DataList/package.json +1 -1
- package/dist/dynamic/components/DatePicker/package.json +1 -1
- package/dist/dynamic/components/DescriptionList/package.json +1 -1
- package/dist/dynamic/components/Divider/package.json +1 -1
- package/dist/dynamic/components/DragDrop/package.json +1 -1
- package/dist/dynamic/components/Drawer/package.json +1 -1
- package/dist/dynamic/components/Dropdown/package.json +1 -1
- package/dist/dynamic/components/DualListSelector/package.json +1 -1
- package/dist/dynamic/components/EmptyState/package.json +1 -1
- package/dist/dynamic/components/ExpandableSection/package.json +1 -1
- package/dist/dynamic/components/FileUpload/package.json +1 -1
- package/dist/dynamic/components/Form/package.json +1 -1
- package/dist/dynamic/components/FormSelect/package.json +1 -1
- package/dist/dynamic/components/HelperText/package.json +1 -1
- package/dist/dynamic/components/Hint/package.json +1 -1
- package/dist/dynamic/components/Icon/package.json +1 -1
- package/dist/dynamic/components/InputGroup/package.json +1 -1
- package/dist/dynamic/components/JumpLinks/package.json +1 -1
- package/dist/dynamic/components/Label/package.json +1 -1
- package/dist/dynamic/components/List/package.json +1 -1
- package/dist/dynamic/components/LoginPage/package.json +1 -1
- package/dist/dynamic/components/Masthead/package.json +1 -1
- package/dist/dynamic/components/Menu/package.json +1 -1
- package/dist/dynamic/components/MenuToggle/package.json +1 -1
- package/dist/dynamic/components/Modal/package.json +1 -1
- package/dist/dynamic/components/MultipleFileUpload/package.json +1 -1
- package/dist/dynamic/components/Nav/package.json +1 -1
- package/dist/dynamic/components/NotificationBadge/package.json +1 -1
- package/dist/dynamic/components/NotificationDrawer/package.json +1 -1
- package/dist/dynamic/components/NumberInput/package.json +1 -1
- package/dist/dynamic/components/OverflowMenu/package.json +1 -1
- package/dist/dynamic/components/Page/package.json +1 -1
- package/dist/dynamic/components/Pagination/package.json +1 -1
- package/dist/dynamic/components/Panel/package.json +1 -1
- package/dist/dynamic/components/Popover/package.json +1 -1
- package/dist/dynamic/components/Progress/package.json +1 -1
- package/dist/dynamic/components/ProgressStepper/package.json +1 -1
- package/dist/dynamic/components/Radio/package.json +1 -1
- package/dist/dynamic/components/SearchInput/package.json +1 -1
- package/dist/dynamic/components/Select/package.json +1 -1
- package/dist/dynamic/components/Sidebar/package.json +1 -1
- package/dist/dynamic/components/SimpleList/package.json +1 -1
- package/dist/dynamic/components/Skeleton/package.json +1 -1
- package/dist/dynamic/components/SkipToContent/package.json +1 -1
- package/dist/dynamic/components/Slider/package.json +1 -1
- package/dist/dynamic/components/Spinner/package.json +1 -1
- package/dist/dynamic/components/Switch/package.json +1 -1
- package/dist/dynamic/components/Tabs/package.json +1 -1
- package/dist/dynamic/components/Text/package.json +1 -1
- package/dist/dynamic/components/TextArea/package.json +1 -1
- package/dist/dynamic/components/TextInput/package.json +1 -1
- package/dist/dynamic/components/TextInputGroup/package.json +1 -1
- package/dist/dynamic/components/Tile/package.json +1 -1
- package/dist/dynamic/components/TimePicker/package.json +1 -1
- package/dist/dynamic/components/Timestamp/package.json +1 -1
- package/dist/dynamic/components/Title/package.json +1 -1
- package/dist/dynamic/components/ToggleGroup/package.json +1 -1
- package/dist/dynamic/components/Toolbar/package.json +1 -1
- package/dist/dynamic/components/Tooltip/package.json +1 -1
- package/dist/dynamic/components/TreeView/package.json +1 -1
- package/dist/dynamic/components/Truncate/package.json +1 -1
- package/dist/dynamic/components/Wizard/hooks/package.json +1 -1
- package/dist/dynamic/components/Wizard/package.json +1 -1
- package/dist/dynamic/deprecated/components/ApplicationLauncher/package.json +1 -1
- package/dist/dynamic/deprecated/components/ContextSelector/package.json +1 -1
- package/dist/dynamic/deprecated/components/Dropdown/package.json +1 -1
- package/dist/dynamic/deprecated/components/OptionsMenu/package.json +1 -1
- package/dist/dynamic/deprecated/components/PageHeader/package.json +1 -1
- package/dist/dynamic/deprecated/components/Select/package.json +1 -1
- package/dist/dynamic/deprecated/components/Wizard/package.json +1 -1
- package/dist/dynamic/deprecated/components/package.json +1 -1
- package/dist/dynamic/helpers/FocusTrap/FocusTrap/package.json +1 -1
- package/dist/dynamic/helpers/GenerateId/GenerateId/package.json +1 -1
- package/dist/dynamic/helpers/KeyboardHandler/package.json +1 -1
- package/dist/dynamic/helpers/OUIA/ouia/package.json +1 -1
- package/dist/dynamic/helpers/Popper/Popper/package.json +1 -1
- package/dist/dynamic/helpers/constants/package.json +1 -1
- package/dist/dynamic/helpers/datetimeUtils/package.json +1 -1
- package/dist/dynamic/helpers/favorites/package.json +1 -1
- package/dist/dynamic/helpers/fileUtils/package.json +1 -1
- package/dist/dynamic/helpers/htmlConstants/package.json +1 -1
- package/dist/dynamic/helpers/package.json +1 -1
- package/dist/dynamic/helpers/resizeObserver/package.json +1 -1
- package/dist/dynamic/helpers/typeUtils/package.json +1 -1
- package/dist/dynamic/helpers/useInterval/package.json +1 -1
- package/dist/dynamic/helpers/useIsomorphicLayout/package.json +1 -1
- package/dist/dynamic/helpers/useUnmountEffect/package.json +1 -1
- package/dist/dynamic/helpers/util/package.json +1 -1
- package/dist/dynamic/layouts/Bullseye/package.json +1 -1
- package/dist/dynamic/layouts/Flex/package.json +1 -1
- package/dist/dynamic/layouts/Gallery/package.json +1 -1
- package/dist/dynamic/layouts/Grid/package.json +1 -1
- package/dist/dynamic/layouts/Level/package.json +1 -1
- package/dist/dynamic/layouts/Split/package.json +1 -1
- package/dist/dynamic/layouts/Stack/package.json +1 -1
- package/dist/dynamic/next/components/DualListSelector/package.json +1 -1
- package/dist/dynamic/next/components/Modal/package.json +1 -1
- package/dist/dynamic/next/components/package.json +1 -1
- package/dist/dynamic/styles/package.json +1 -1
- package/dist/esm/components/MenuToggle/MenuToggle.d.ts.map +1 -1
- package/dist/esm/components/MenuToggle/MenuToggle.js +1 -1
- package/dist/esm/components/MenuToggle/MenuToggle.js.map +1 -1
- package/dist/esm/components/Pagination/Pagination.d.ts.map +1 -1
- package/dist/esm/components/Pagination/Pagination.js +2 -1
- package/dist/esm/components/Pagination/Pagination.js.map +1 -1
- package/dist/esm/components/Pagination/PaginationOptionsMenu.d.ts +1 -0
- package/dist/esm/components/Pagination/PaginationOptionsMenu.d.ts.map +1 -1
- package/dist/esm/components/Pagination/PaginationOptionsMenu.js +2 -4
- package/dist/esm/components/Pagination/PaginationOptionsMenu.js.map +1 -1
- package/dist/esm/components/Select/Select.d.ts +2 -0
- package/dist/esm/components/Select/Select.d.ts.map +1 -1
- package/dist/esm/components/Select/Select.js +3 -3
- package/dist/esm/components/Select/Select.js.map +1 -1
- package/dist/esm/components/Tabs/Tabs.d.ts +4 -0
- package/dist/esm/components/Tabs/Tabs.d.ts.map +1 -1
- package/dist/esm/components/Tabs/Tabs.js +17 -0
- package/dist/esm/components/Tabs/Tabs.js.map +1 -1
- package/dist/esm/components/Truncate/Truncate.d.ts.map +1 -1
- package/dist/esm/components/Truncate/Truncate.js +54 -12
- package/dist/esm/components/Truncate/Truncate.js.map +1 -1
- package/dist/js/components/MenuToggle/MenuToggle.d.ts.map +1 -1
- package/dist/js/components/MenuToggle/MenuToggle.js +1 -1
- package/dist/js/components/MenuToggle/MenuToggle.js.map +1 -1
- package/dist/js/components/Pagination/Pagination.d.ts.map +1 -1
- package/dist/js/components/Pagination/Pagination.js +2 -1
- package/dist/js/components/Pagination/Pagination.js.map +1 -1
- package/dist/js/components/Pagination/PaginationOptionsMenu.d.ts +1 -0
- package/dist/js/components/Pagination/PaginationOptionsMenu.d.ts.map +1 -1
- package/dist/js/components/Pagination/PaginationOptionsMenu.js +2 -4
- package/dist/js/components/Pagination/PaginationOptionsMenu.js.map +1 -1
- package/dist/js/components/Select/Select.d.ts +2 -0
- package/dist/js/components/Select/Select.d.ts.map +1 -1
- package/dist/js/components/Select/Select.js +3 -3
- package/dist/js/components/Select/Select.js.map +1 -1
- package/dist/js/components/Tabs/Tabs.d.ts +4 -0
- package/dist/js/components/Tabs/Tabs.d.ts.map +1 -1
- package/dist/js/components/Tabs/Tabs.js +17 -0
- package/dist/js/components/Tabs/Tabs.js.map +1 -1
- package/dist/js/components/Truncate/Truncate.d.ts.map +1 -1
- package/dist/js/components/Truncate/Truncate.js +54 -12
- package/dist/js/components/Truncate/Truncate.js.map +1 -1
- package/dist/umd/react-core.min.js +2 -2
- package/helpers/package.json +1 -1
- package/layouts/package.json +1 -1
- package/next/package.json +1 -1
- package/package.json +6 -6
- package/src/components/CodeBlock/examples/CodeBlock.md +6 -0
- package/src/components/CodeBlock/examples/CodeBlockBasic.tsx +1 -1
- package/src/components/CodeBlock/examples/CodeBlockExpandable.tsx +3 -3
- package/src/components/MenuToggle/MenuToggle.tsx +1 -0
- package/src/components/Pagination/Pagination.tsx +2 -0
- package/src/components/Pagination/PaginationOptionsMenu.tsx +13 -14
- package/src/components/Pagination/__tests__/Generated/__snapshots__/PaginationOptionsMenu.test.tsx.snap +36 -38
- package/src/components/Pagination/__tests__/__snapshots__/Pagination.test.tsx.snap +714 -754
- package/src/components/Select/Select.tsx +5 -2
- package/src/components/Select/examples/SelectMultiTypeahead.tsx +111 -66
- package/src/components/Select/examples/SelectMultiTypeaheadCheckbox.tsx +103 -60
- package/src/components/Select/examples/SelectMultiTypeaheadCreatable.tsx +125 -79
- package/src/components/Select/examples/SelectTypeahead.tsx +113 -69
- package/src/components/Select/examples/SelectTypeaheadCreatable.tsx +115 -75
- package/src/components/Tabs/Tabs.tsx +21 -0
- package/src/components/ToggleGroup/examples/ToggleGroup.md +33 -8
- package/src/components/Truncate/Truncate.tsx +73 -7
- package/src/components/Truncate/__tests__/Truncate.test.tsx +6 -0
|
@@ -53,6 +53,8 @@ export interface SelectProps extends MenuProps, OUIAProps {
|
|
|
53
53
|
toggle: SelectToggleProps | ((toggleRef: React.RefObject<any>) => React.ReactNode);
|
|
54
54
|
/** Flag indicating the toggle should be focused after a selection. If this use case is too restrictive, the optional toggleRef property with a node toggle may be used to control focus. */
|
|
55
55
|
shouldFocusToggleOnSelect?: boolean;
|
|
56
|
+
/** @beta Flag indicating the first menu item should be focused after opening the menu. */
|
|
57
|
+
shouldFocusFirstItemOnOpen?: boolean;
|
|
56
58
|
/** Function callback when user selects an option. */
|
|
57
59
|
onSelect?: (event?: React.MouseEvent<Element, MouseEvent>, value?: string | number) => void;
|
|
58
60
|
/** Callback to allow the select component to change the open state of the menu.
|
|
@@ -86,6 +88,7 @@ const SelectBase: React.FunctionComponent<SelectProps & OUIAProps> = ({
|
|
|
86
88
|
selected,
|
|
87
89
|
toggle,
|
|
88
90
|
shouldFocusToggleOnSelect = false,
|
|
91
|
+
shouldFocusFirstItemOnOpen = true,
|
|
89
92
|
onOpenChange,
|
|
90
93
|
onOpenChangeKeys = ['Escape', 'Tab'],
|
|
91
94
|
isPlain,
|
|
@@ -125,7 +128,7 @@ const SelectBase: React.FunctionComponent<SelectProps & OUIAProps> = ({
|
|
|
125
128
|
|
|
126
129
|
const handleClick = (event: MouseEvent) => {
|
|
127
130
|
// toggle was opened, focus on first menu item
|
|
128
|
-
if (isOpen && toggleRef.current?.contains(event.target as Node)) {
|
|
131
|
+
if (isOpen && shouldFocusFirstItemOnOpen && toggleRef.current?.contains(event.target as Node)) {
|
|
129
132
|
setTimeout(() => {
|
|
130
133
|
const firstElement = menuRef?.current?.querySelector('li button:not(:disabled),li input:not(:disabled)');
|
|
131
134
|
firstElement && (firstElement as HTMLElement).focus();
|
|
@@ -160,7 +163,7 @@ const SelectBase: React.FunctionComponent<SelectProps & OUIAProps> = ({
|
|
|
160
163
|
}}
|
|
161
164
|
isPlain={isPlain}
|
|
162
165
|
selected={selected}
|
|
163
|
-
isScrollable={isScrollable}
|
|
166
|
+
isScrollable={isScrollable ?? (menuHeight !== undefined || maxMenuHeight !== undefined)}
|
|
164
167
|
{...getOUIAProps(
|
|
165
168
|
Select.displayName,
|
|
166
169
|
props.ouiaId !== undefined ? props.ouiaId : getDefaultOUIAId(Select.displayName),
|
|
@@ -30,9 +30,11 @@ export const SelectMultiTypeahead: React.FunctionComponent = () => {
|
|
|
30
30
|
const [selected, setSelected] = React.useState<string[]>([]);
|
|
31
31
|
const [selectOptions, setSelectOptions] = React.useState<SelectOptionProps[]>(initialSelectOptions);
|
|
32
32
|
const [focusedItemIndex, setFocusedItemIndex] = React.useState<number | null>(null);
|
|
33
|
-
const [
|
|
33
|
+
const [activeItemId, setActiveItemId] = React.useState<string | null>(null);
|
|
34
34
|
const textInputRef = React.useRef<HTMLInputElement>();
|
|
35
35
|
|
|
36
|
+
const NO_RESULTS = 'no results';
|
|
37
|
+
|
|
36
38
|
React.useEffect(() => {
|
|
37
39
|
let newSelectOptions: SelectOptionProps[] = initialSelectOptions;
|
|
38
40
|
|
|
@@ -45,7 +47,7 @@ export const SelectMultiTypeahead: React.FunctionComponent = () => {
|
|
|
45
47
|
// When no options are found after filtering, display 'No results found'
|
|
46
48
|
if (!newSelectOptions.length) {
|
|
47
49
|
newSelectOptions = [
|
|
48
|
-
{
|
|
50
|
+
{ isAriaDisabled: true, children: `No results found for "${inputValue}"`, value: NO_RESULTS }
|
|
49
51
|
];
|
|
50
52
|
}
|
|
51
53
|
|
|
@@ -56,56 +58,113 @@ export const SelectMultiTypeahead: React.FunctionComponent = () => {
|
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
setSelectOptions(newSelectOptions);
|
|
59
|
-
setFocusedItemIndex(null);
|
|
60
|
-
setActiveItem(null);
|
|
61
61
|
}, [inputValue]);
|
|
62
62
|
|
|
63
|
+
const createItemId = (value: any) => `select-multi-typeahead-${value.replace(' ', '-')}`;
|
|
64
|
+
|
|
65
|
+
const setActiveAndFocusedItem = (itemIndex: number) => {
|
|
66
|
+
setFocusedItemIndex(itemIndex);
|
|
67
|
+
const focusedItem = selectOptions[itemIndex];
|
|
68
|
+
setActiveItemId(createItemId(focusedItem.value));
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const resetActiveAndFocusedItem = () => {
|
|
72
|
+
setFocusedItemIndex(null);
|
|
73
|
+
setActiveItemId(null);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const closeMenu = () => {
|
|
77
|
+
setIsOpen(false);
|
|
78
|
+
resetActiveAndFocusedItem();
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const onInputClick = () => {
|
|
82
|
+
if (!isOpen) {
|
|
83
|
+
setIsOpen(true);
|
|
84
|
+
} else if (!inputValue) {
|
|
85
|
+
closeMenu();
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const onSelect = (value: string) => {
|
|
90
|
+
if (value && value !== NO_RESULTS) {
|
|
91
|
+
// eslint-disable-next-line no-console
|
|
92
|
+
console.log('selected', value);
|
|
93
|
+
|
|
94
|
+
setSelected(
|
|
95
|
+
selected.includes(value) ? selected.filter((selection) => selection !== value) : [...selected, value]
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
textInputRef.current?.focus();
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const onTextInputChange = (_event: React.FormEvent<HTMLInputElement>, value: string) => {
|
|
103
|
+
setInputValue(value);
|
|
104
|
+
resetActiveAndFocusedItem();
|
|
105
|
+
};
|
|
106
|
+
|
|
63
107
|
const handleMenuArrowKeys = (key: string) => {
|
|
64
|
-
let indexToFocus;
|
|
108
|
+
let indexToFocus = 0;
|
|
65
109
|
|
|
66
|
-
if (isOpen) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
110
|
+
if (!isOpen) {
|
|
111
|
+
setIsOpen(true);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (selectOptions.every((option) => option.isDisabled)) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (key === 'ArrowUp') {
|
|
119
|
+
// When no index is set or at the first index, focus to the last, otherwise decrement focus index
|
|
120
|
+
if (focusedItemIndex === null || focusedItemIndex === 0) {
|
|
121
|
+
indexToFocus = selectOptions.length - 1;
|
|
122
|
+
} else {
|
|
123
|
+
indexToFocus = focusedItemIndex - 1;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Skip disabled options
|
|
127
|
+
while (selectOptions[indexToFocus].isDisabled) {
|
|
128
|
+
indexToFocus--;
|
|
129
|
+
if (indexToFocus === -1) {
|
|
70
130
|
indexToFocus = selectOptions.length - 1;
|
|
71
|
-
} else {
|
|
72
|
-
indexToFocus = focusedItemIndex - 1;
|
|
73
131
|
}
|
|
74
132
|
}
|
|
133
|
+
}
|
|
75
134
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
135
|
+
if (key === 'ArrowDown') {
|
|
136
|
+
// When no index is set or at the last index, focus to the first, otherwise increment focus index
|
|
137
|
+
if (focusedItemIndex === null || focusedItemIndex === selectOptions.length - 1) {
|
|
138
|
+
indexToFocus = 0;
|
|
139
|
+
} else {
|
|
140
|
+
indexToFocus = focusedItemIndex + 1;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Skip disabled options
|
|
144
|
+
while (selectOptions[indexToFocus].isDisabled) {
|
|
145
|
+
indexToFocus++;
|
|
146
|
+
if (indexToFocus === selectOptions.length) {
|
|
79
147
|
indexToFocus = 0;
|
|
80
|
-
} else {
|
|
81
|
-
indexToFocus = focusedItemIndex + 1;
|
|
82
148
|
}
|
|
83
149
|
}
|
|
84
|
-
|
|
85
|
-
setFocusedItemIndex(indexToFocus);
|
|
86
|
-
const focusedItem = selectOptions.filter((option) => !option.isDisabled)[indexToFocus];
|
|
87
|
-
setActiveItem(`select-multi-typeahead-${focusedItem.value.replace(' ', '-')}`);
|
|
88
150
|
}
|
|
151
|
+
|
|
152
|
+
setActiveAndFocusedItem(indexToFocus);
|
|
89
153
|
};
|
|
90
154
|
|
|
91
155
|
const onInputKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
|
92
|
-
const
|
|
93
|
-
const [firstMenuItem] = enabledMenuItems;
|
|
94
|
-
const focusedItem = focusedItemIndex ? enabledMenuItems[focusedItemIndex] : firstMenuItem;
|
|
156
|
+
const focusedItem = focusedItemIndex !== null ? selectOptions[focusedItemIndex] : null;
|
|
95
157
|
|
|
96
158
|
switch (event.key) {
|
|
97
|
-
// Select the first available option
|
|
98
159
|
case 'Enter':
|
|
160
|
+
if (isOpen && focusedItem && focusedItem.value !== NO_RESULTS && !focusedItem.isAriaDisabled) {
|
|
161
|
+
onSelect(focusedItem.value);
|
|
162
|
+
}
|
|
163
|
+
|
|
99
164
|
if (!isOpen) {
|
|
100
|
-
setIsOpen(
|
|
101
|
-
} else if (isOpen && focusedItem.value !== 'no results') {
|
|
102
|
-
onSelect(focusedItem.value as string);
|
|
165
|
+
setIsOpen(true);
|
|
103
166
|
}
|
|
104
|
-
|
|
105
|
-
case 'Tab':
|
|
106
|
-
case 'Escape':
|
|
107
|
-
setIsOpen(false);
|
|
108
|
-
setActiveItem(null);
|
|
167
|
+
|
|
109
168
|
break;
|
|
110
169
|
case 'ArrowUp':
|
|
111
170
|
case 'ArrowDown':
|
|
@@ -117,24 +176,17 @@ export const SelectMultiTypeahead: React.FunctionComponent = () => {
|
|
|
117
176
|
|
|
118
177
|
const onToggleClick = () => {
|
|
119
178
|
setIsOpen(!isOpen);
|
|
179
|
+
textInputRef?.current?.focus();
|
|
120
180
|
};
|
|
121
181
|
|
|
122
|
-
const
|
|
123
|
-
|
|
182
|
+
const onClearButtonClick = () => {
|
|
183
|
+
setSelected([]);
|
|
184
|
+
setInputValue('');
|
|
185
|
+
resetActiveAndFocusedItem();
|
|
186
|
+
textInputRef?.current?.focus();
|
|
124
187
|
};
|
|
125
188
|
|
|
126
|
-
const
|
|
127
|
-
// eslint-disable-next-line no-console
|
|
128
|
-
console.log('selected', value);
|
|
129
|
-
|
|
130
|
-
if (value && value !== 'no results') {
|
|
131
|
-
setSelected(
|
|
132
|
-
selected.includes(value) ? selected.filter((selection) => selection !== value) : [...selected, value]
|
|
133
|
-
);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
textInputRef.current?.focus();
|
|
137
|
-
};
|
|
189
|
+
const getChildren = (value: string) => initialSelectOptions.find((option) => option.value === value)?.children;
|
|
138
190
|
|
|
139
191
|
const toggle = (toggleRef: React.Ref<MenuToggleElement>) => (
|
|
140
192
|
<MenuToggle
|
|
@@ -148,14 +200,14 @@ export const SelectMultiTypeahead: React.FunctionComponent = () => {
|
|
|
148
200
|
<TextInputGroup isPlain>
|
|
149
201
|
<TextInputGroupMain
|
|
150
202
|
value={inputValue}
|
|
151
|
-
onClick={
|
|
203
|
+
onClick={onInputClick}
|
|
152
204
|
onChange={onTextInputChange}
|
|
153
205
|
onKeyDown={onInputKeyDown}
|
|
154
206
|
id="multi-typeahead-select-input"
|
|
155
207
|
autoComplete="off"
|
|
156
208
|
innerRef={textInputRef}
|
|
157
209
|
placeholder="Select a state"
|
|
158
|
-
{...(
|
|
210
|
+
{...(activeItemId && { 'aria-activedescendant': activeItemId })}
|
|
159
211
|
role="combobox"
|
|
160
212
|
isExpanded={isOpen}
|
|
161
213
|
aria-controls="select-multi-typeahead-listbox"
|
|
@@ -169,25 +221,15 @@ export const SelectMultiTypeahead: React.FunctionComponent = () => {
|
|
|
169
221
|
onSelect(selection);
|
|
170
222
|
}}
|
|
171
223
|
>
|
|
172
|
-
{selection}
|
|
224
|
+
{getChildren(selection)}
|
|
173
225
|
</Chip>
|
|
174
226
|
))}
|
|
175
227
|
</ChipGroup>
|
|
176
228
|
</TextInputGroupMain>
|
|
177
|
-
<TextInputGroupUtilities>
|
|
178
|
-
{
|
|
179
|
-
<
|
|
180
|
-
|
|
181
|
-
onClick={() => {
|
|
182
|
-
setInputValue('');
|
|
183
|
-
setSelected([]);
|
|
184
|
-
textInputRef?.current?.focus();
|
|
185
|
-
}}
|
|
186
|
-
aria-label="Clear input value"
|
|
187
|
-
>
|
|
188
|
-
<TimesIcon aria-hidden />
|
|
189
|
-
</Button>
|
|
190
|
-
)}
|
|
229
|
+
<TextInputGroupUtilities {...(selected.length === 0 ? { style: { display: 'none' } } : {})}>
|
|
230
|
+
<Button variant="plain" onClick={onClearButtonClick} aria-label="Clear input value">
|
|
231
|
+
<TimesIcon aria-hidden />
|
|
232
|
+
</Button>
|
|
191
233
|
</TextInputGroupUtilities>
|
|
192
234
|
</TextInputGroup>
|
|
193
235
|
</MenuToggle>
|
|
@@ -198,9 +240,12 @@ export const SelectMultiTypeahead: React.FunctionComponent = () => {
|
|
|
198
240
|
id="multi-typeahead-select"
|
|
199
241
|
isOpen={isOpen}
|
|
200
242
|
selected={selected}
|
|
201
|
-
onSelect={(
|
|
202
|
-
onOpenChange={() =>
|
|
243
|
+
onSelect={(_event, selection) => onSelect(selection as string)}
|
|
244
|
+
onOpenChange={(isOpen) => {
|
|
245
|
+
!isOpen && closeMenu();
|
|
246
|
+
}}
|
|
203
247
|
toggle={toggle}
|
|
248
|
+
shouldFocusFirstItemOnOpen={false}
|
|
204
249
|
>
|
|
205
250
|
<SelectList isAriaMultiselectable id="select-multi-typeahead-listbox">
|
|
206
251
|
{selectOptions.map((option, index) => (
|
|
@@ -208,7 +253,7 @@ export const SelectMultiTypeahead: React.FunctionComponent = () => {
|
|
|
208
253
|
key={option.value || option.children}
|
|
209
254
|
isFocused={focusedItemIndex === index}
|
|
210
255
|
className={option.className}
|
|
211
|
-
id={
|
|
256
|
+
id={createItemId(option.value)}
|
|
212
257
|
{...option}
|
|
213
258
|
ref={null}
|
|
214
259
|
/>
|
|
@@ -28,10 +28,12 @@ export const SelectMultiTypeaheadCheckbox: React.FunctionComponent = () => {
|
|
|
28
28
|
const [selected, setSelected] = React.useState<string[]>([]);
|
|
29
29
|
const [selectOptions, setSelectOptions] = React.useState<SelectOptionProps[]>(initialSelectOptions);
|
|
30
30
|
const [focusedItemIndex, setFocusedItemIndex] = React.useState<number | null>(null);
|
|
31
|
-
const [
|
|
31
|
+
const [activeItemId, setActiveItemId] = React.useState<string | null>(null);
|
|
32
32
|
const [placeholder, setPlaceholder] = React.useState('0 items selected');
|
|
33
33
|
const textInputRef = React.useRef<HTMLInputElement>();
|
|
34
34
|
|
|
35
|
+
const NO_RESULTS = 'no results';
|
|
36
|
+
|
|
35
37
|
React.useEffect(() => {
|
|
36
38
|
let newSelectOptions: SelectOptionProps[] = initialSelectOptions;
|
|
37
39
|
|
|
@@ -45,9 +47,9 @@ export const SelectMultiTypeaheadCheckbox: React.FunctionComponent = () => {
|
|
|
45
47
|
if (!newSelectOptions.length) {
|
|
46
48
|
newSelectOptions = [
|
|
47
49
|
{
|
|
48
|
-
|
|
50
|
+
isAriaDisabled: true,
|
|
49
51
|
children: `No results found for "${inputValue}"`,
|
|
50
|
-
value:
|
|
52
|
+
value: NO_RESULTS,
|
|
51
53
|
hasCheckbox: false
|
|
52
54
|
}
|
|
53
55
|
];
|
|
@@ -60,56 +62,99 @@ export const SelectMultiTypeaheadCheckbox: React.FunctionComponent = () => {
|
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
setSelectOptions(newSelectOptions);
|
|
63
|
-
setFocusedItemIndex(null);
|
|
64
|
-
setActiveItem(null);
|
|
65
65
|
}, [inputValue]);
|
|
66
66
|
|
|
67
|
+
React.useEffect(() => {
|
|
68
|
+
setPlaceholder(`${selected.length} item${selected.length !== 1 ? 's' : ''} selected`);
|
|
69
|
+
}, [selected]);
|
|
70
|
+
|
|
71
|
+
const createItemId = (value: any) => `select-multi-typeahead-${value.replace(' ', '-')}`;
|
|
72
|
+
|
|
73
|
+
const setActiveAndFocusedItem = (itemIndex: number) => {
|
|
74
|
+
setFocusedItemIndex(itemIndex);
|
|
75
|
+
const focusedItem = selectOptions[itemIndex];
|
|
76
|
+
setActiveItemId(createItemId(focusedItem.value));
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const resetActiveAndFocusedItem = () => {
|
|
80
|
+
setFocusedItemIndex(null);
|
|
81
|
+
setActiveItemId(null);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const closeMenu = () => {
|
|
85
|
+
setIsOpen(false);
|
|
86
|
+
resetActiveAndFocusedItem();
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const onInputClick = () => {
|
|
90
|
+
if (!isOpen) {
|
|
91
|
+
setIsOpen(true);
|
|
92
|
+
} else if (!inputValue) {
|
|
93
|
+
closeMenu();
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
67
97
|
const handleMenuArrowKeys = (key: string) => {
|
|
68
|
-
let indexToFocus;
|
|
98
|
+
let indexToFocus = 0;
|
|
99
|
+
|
|
100
|
+
if (!isOpen) {
|
|
101
|
+
setIsOpen(true);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (selectOptions.every((option) => option.isDisabled)) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (key === 'ArrowUp') {
|
|
109
|
+
// When no index is set or at the first index, focus to the last, otherwise decrement focus index
|
|
110
|
+
if (focusedItemIndex === null || focusedItemIndex === 0) {
|
|
111
|
+
indexToFocus = selectOptions.length - 1;
|
|
112
|
+
} else {
|
|
113
|
+
indexToFocus = focusedItemIndex - 1;
|
|
114
|
+
}
|
|
69
115
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if (
|
|
116
|
+
// Skip disabled options
|
|
117
|
+
while (selectOptions[indexToFocus].isDisabled) {
|
|
118
|
+
indexToFocus--;
|
|
119
|
+
if (indexToFocus === -1) {
|
|
74
120
|
indexToFocus = selectOptions.length - 1;
|
|
75
|
-
} else {
|
|
76
|
-
indexToFocus = focusedItemIndex - 1;
|
|
77
121
|
}
|
|
78
122
|
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (key === 'ArrowDown') {
|
|
126
|
+
// When no index is set or at the last index, focus to the first, otherwise increment focus index
|
|
127
|
+
if (focusedItemIndex === null || focusedItemIndex === selectOptions.length - 1) {
|
|
128
|
+
indexToFocus = 0;
|
|
129
|
+
} else {
|
|
130
|
+
indexToFocus = focusedItemIndex + 1;
|
|
131
|
+
}
|
|
79
132
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
133
|
+
// Skip disabled options
|
|
134
|
+
while (selectOptions[indexToFocus].isDisabled) {
|
|
135
|
+
indexToFocus++;
|
|
136
|
+
if (indexToFocus === selectOptions.length) {
|
|
83
137
|
indexToFocus = 0;
|
|
84
|
-
} else {
|
|
85
|
-
indexToFocus = focusedItemIndex + 1;
|
|
86
138
|
}
|
|
87
139
|
}
|
|
88
|
-
|
|
89
|
-
setFocusedItemIndex(indexToFocus);
|
|
90
|
-
const focusedItem = selectOptions.filter((option) => !option.isDisabled)[indexToFocus];
|
|
91
|
-
setActiveItem(`select-multi-typeahead-checkbox-${focusedItem.value.replace(' ', '-')}`);
|
|
92
140
|
}
|
|
141
|
+
|
|
142
|
+
setActiveAndFocusedItem(indexToFocus);
|
|
93
143
|
};
|
|
94
144
|
|
|
95
145
|
const onInputKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
|
96
|
-
const
|
|
97
|
-
const [firstMenuItem] = enabledMenuItems;
|
|
98
|
-
const focusedItem = focusedItemIndex ? enabledMenuItems[focusedItemIndex] : firstMenuItem;
|
|
146
|
+
const focusedItem = focusedItemIndex !== null ? selectOptions[focusedItemIndex] : null;
|
|
99
147
|
|
|
100
148
|
switch (event.key) {
|
|
101
|
-
// Select the first available option
|
|
102
149
|
case 'Enter':
|
|
150
|
+
if (isOpen && focusedItem && focusedItem.value !== NO_RESULTS && !focusedItem.isAriaDisabled) {
|
|
151
|
+
onSelect(focusedItem.value);
|
|
152
|
+
}
|
|
153
|
+
|
|
103
154
|
if (!isOpen) {
|
|
104
|
-
setIsOpen(
|
|
105
|
-
} else if (isOpen && focusedItem.value !== 'no results') {
|
|
106
|
-
onSelect(focusedItem.value as string);
|
|
155
|
+
setIsOpen(true);
|
|
107
156
|
}
|
|
108
|
-
|
|
109
|
-
case 'Tab':
|
|
110
|
-
case 'Escape':
|
|
111
|
-
setIsOpen(false);
|
|
112
|
-
setActiveItem(null);
|
|
157
|
+
|
|
113
158
|
break;
|
|
114
159
|
case 'ArrowUp':
|
|
115
160
|
case 'ArrowDown':
|
|
@@ -121,17 +166,19 @@ export const SelectMultiTypeaheadCheckbox: React.FunctionComponent = () => {
|
|
|
121
166
|
|
|
122
167
|
const onToggleClick = () => {
|
|
123
168
|
setIsOpen(!isOpen);
|
|
169
|
+
textInputRef?.current?.focus();
|
|
124
170
|
};
|
|
125
171
|
|
|
126
172
|
const onTextInputChange = (_event: React.FormEvent<HTMLInputElement>, value: string) => {
|
|
127
173
|
setInputValue(value);
|
|
174
|
+
resetActiveAndFocusedItem();
|
|
128
175
|
};
|
|
129
176
|
|
|
130
177
|
const onSelect = (value: string) => {
|
|
131
|
-
|
|
132
|
-
|
|
178
|
+
if (value && value !== NO_RESULTS) {
|
|
179
|
+
// eslint-disable-next-line no-console
|
|
180
|
+
console.log('selected', value);
|
|
133
181
|
|
|
134
|
-
if (value && value !== 'no results') {
|
|
135
182
|
setSelected(
|
|
136
183
|
selected.includes(value) ? selected.filter((selection) => selection !== value) : [...selected, value]
|
|
137
184
|
);
|
|
@@ -140,9 +187,12 @@ export const SelectMultiTypeaheadCheckbox: React.FunctionComponent = () => {
|
|
|
140
187
|
textInputRef.current?.focus();
|
|
141
188
|
};
|
|
142
189
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
190
|
+
const onClearButtonClick = () => {
|
|
191
|
+
setSelected([]);
|
|
192
|
+
setInputValue('');
|
|
193
|
+
resetActiveAndFocusedItem();
|
|
194
|
+
textInputRef?.current?.focus();
|
|
195
|
+
};
|
|
146
196
|
|
|
147
197
|
const toggle = (toggleRef: React.Ref<MenuToggleElement>) => (
|
|
148
198
|
<MenuToggle
|
|
@@ -156,32 +206,22 @@ export const SelectMultiTypeaheadCheckbox: React.FunctionComponent = () => {
|
|
|
156
206
|
<TextInputGroup isPlain>
|
|
157
207
|
<TextInputGroupMain
|
|
158
208
|
value={inputValue}
|
|
159
|
-
onClick={
|
|
209
|
+
onClick={onInputClick}
|
|
160
210
|
onChange={onTextInputChange}
|
|
161
211
|
onKeyDown={onInputKeyDown}
|
|
162
212
|
id="multi-typeahead-select-checkbox-input"
|
|
163
213
|
autoComplete="off"
|
|
164
214
|
innerRef={textInputRef}
|
|
165
215
|
placeholder={placeholder}
|
|
166
|
-
{...(
|
|
216
|
+
{...(activeItemId && { 'aria-activedescendant': activeItemId })}
|
|
167
217
|
role="combobox"
|
|
168
218
|
isExpanded={isOpen}
|
|
169
219
|
aria-controls="select-multi-typeahead-checkbox-listbox"
|
|
170
220
|
/>
|
|
171
|
-
<TextInputGroupUtilities>
|
|
172
|
-
{
|
|
173
|
-
<
|
|
174
|
-
|
|
175
|
-
onClick={() => {
|
|
176
|
-
setInputValue('');
|
|
177
|
-
setSelected([]);
|
|
178
|
-
textInputRef?.current?.focus();
|
|
179
|
-
}}
|
|
180
|
-
aria-label="Clear input value"
|
|
181
|
-
>
|
|
182
|
-
<TimesIcon aria-hidden />
|
|
183
|
-
</Button>
|
|
184
|
-
)}
|
|
221
|
+
<TextInputGroupUtilities {...(selected.length === 0 ? { style: { display: 'none' } } : {})}>
|
|
222
|
+
<Button variant="plain" onClick={onClearButtonClick} aria-label="Clear input value">
|
|
223
|
+
<TimesIcon aria-hidden />
|
|
224
|
+
</Button>
|
|
185
225
|
</TextInputGroupUtilities>
|
|
186
226
|
</TextInputGroup>
|
|
187
227
|
</MenuToggle>
|
|
@@ -193,19 +233,22 @@ export const SelectMultiTypeaheadCheckbox: React.FunctionComponent = () => {
|
|
|
193
233
|
id="multi-typeahead-checkbox-select"
|
|
194
234
|
isOpen={isOpen}
|
|
195
235
|
selected={selected}
|
|
196
|
-
onSelect={(
|
|
197
|
-
onOpenChange={() =>
|
|
236
|
+
onSelect={(_event, selection) => onSelect(selection as string)}
|
|
237
|
+
onOpenChange={(isOpen) => {
|
|
238
|
+
!isOpen && closeMenu();
|
|
239
|
+
}}
|
|
198
240
|
toggle={toggle}
|
|
241
|
+
shouldFocusFirstItemOnOpen={false}
|
|
199
242
|
>
|
|
200
|
-
<SelectList id="select-multi-typeahead-checkbox-listbox">
|
|
243
|
+
<SelectList isAriaMultiselectable id="select-multi-typeahead-checkbox-listbox">
|
|
201
244
|
{selectOptions.map((option, index) => (
|
|
202
245
|
<SelectOption
|
|
203
|
-
{...(!option.isDisabled && { hasCheckbox: true })}
|
|
246
|
+
{...(!option.isDisabled && !option.isAriaDisabled && { hasCheckbox: true })}
|
|
204
247
|
isSelected={selected.includes(option.value)}
|
|
205
248
|
key={option.value || option.children}
|
|
206
249
|
isFocused={focusedItemIndex === index}
|
|
207
250
|
className={option.className}
|
|
208
|
-
id={
|
|
251
|
+
id={createItemId(option.value)}
|
|
209
252
|
{...option}
|
|
210
253
|
ref={null}
|
|
211
254
|
/>
|