@canonical/react-components 1.0.0 → 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/dist/components/ContextualMenu/ContextualMenu.d.ts +21 -17
- package/dist/components/ContextualMenu/ContextualMenu.js +34 -27
- package/dist/components/ContextualMenu/ContextualMenuDropdown/ContextualMenuDropdown.js +11 -10
- package/dist/components/Field/Field.d.ts +1 -1
- package/dist/components/MultiSelect/MultiSelect.d.ts +6 -1
- package/dist/components/MultiSelect/MultiSelect.js +98 -62
- package/dist/components/MultiSelect/MultiSelect.scss +7 -13
- package/dist/components/SearchBox/SearchBox.d.ts +5 -1
- package/dist/components/SearchBox/SearchBox.js +5 -2
- package/package.json +1 -1
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import type { HTMLProps } from "react";
|
|
2
|
+
import type { HTMLProps, ReactNode } from "react";
|
|
3
3
|
import type { ButtonProps } from "../Button";
|
|
4
4
|
import type { ContextualMenuDropdownProps } from "./ContextualMenuDropdown";
|
|
5
5
|
import type { MenuLink, Position } from "./ContextualMenuDropdown";
|
|
6
|
-
import { ClassName, PropsWithSpread, SubComponentProps } from "../../types";
|
|
6
|
+
import { ClassName, ExclusiveProps, PropsWithSpread, SubComponentProps } from "../../types";
|
|
7
7
|
export declare enum Label {
|
|
8
8
|
Toggle = "Toggle menu"
|
|
9
9
|
}
|
|
10
|
-
|
|
11
|
-
* The props for the ContextualMenu component.
|
|
12
|
-
* @template L - The type of the link props.
|
|
13
|
-
*/
|
|
14
|
-
export type Props<L> = PropsWithSpread<{
|
|
10
|
+
export type BaseProps<L> = PropsWithSpread<{
|
|
15
11
|
/**
|
|
16
12
|
* Whether the menu should adjust its horizontal position to fit on the screen.
|
|
17
13
|
*/
|
|
@@ -44,10 +40,6 @@ export type Props<L> = PropsWithSpread<{
|
|
|
44
40
|
* Additional props to pass to the dropdown.
|
|
45
41
|
*/
|
|
46
42
|
dropdownProps?: SubComponentProps<ContextualMenuDropdownProps>;
|
|
47
|
-
/**
|
|
48
|
-
* Whether the toggle should display a chevron icon.
|
|
49
|
-
*/
|
|
50
|
-
hasToggleIcon?: boolean;
|
|
51
43
|
/**
|
|
52
44
|
* A list of links to display in the menu (if the children prop is not supplied.)
|
|
53
45
|
*/
|
|
@@ -68,6 +60,20 @@ export type Props<L> = PropsWithSpread<{
|
|
|
68
60
|
* Whether the dropdown should scroll if it is too long to fit on the screen.
|
|
69
61
|
*/
|
|
70
62
|
scrollOverflow?: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Whether the menu should be visible.
|
|
65
|
+
*/
|
|
66
|
+
visible?: boolean;
|
|
67
|
+
}, HTMLProps<HTMLSpanElement>>;
|
|
68
|
+
/**
|
|
69
|
+
* The props for the ContextualMenu component.
|
|
70
|
+
* @template L - The type of the link props.
|
|
71
|
+
*/
|
|
72
|
+
export type Props<L> = BaseProps<L> & ExclusiveProps<{
|
|
73
|
+
/**
|
|
74
|
+
* Whether the toggle should display a chevron icon.
|
|
75
|
+
*/
|
|
76
|
+
hasToggleIcon?: boolean;
|
|
71
77
|
/**
|
|
72
78
|
* The appearance of the toggle button.
|
|
73
79
|
*/
|
|
@@ -92,15 +98,13 @@ export type Props<L> = PropsWithSpread<{
|
|
|
92
98
|
* Additional props to pass to the toggle button.
|
|
93
99
|
*/
|
|
94
100
|
toggleProps?: SubComponentProps<ButtonProps>;
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
visible?: boolean;
|
|
99
|
-
}, HTMLProps<HTMLSpanElement>>;
|
|
101
|
+
}, {
|
|
102
|
+
toggle: ReactNode;
|
|
103
|
+
}>;
|
|
100
104
|
/**
|
|
101
105
|
* This is a [React](https://reactjs.org/) component for the Vanilla [Contextual menu](https://docs.vanillaframework.io/patterns//contextual-menu/).
|
|
102
106
|
*
|
|
103
107
|
* A contextual menu can be used in conjunction with any page element to provide a contextual menu.
|
|
104
108
|
*/
|
|
105
|
-
declare const ContextualMenu: <L>({ autoAdjust, children, className, closeOnEsc, closeOnOutsideClick, constrainPanelWidth, dropdownClassName, dropdownProps, hasToggleIcon, links, onToggleMenu, position, positionNode, scrollOverflow, toggleAppearance, toggleClassName, toggleDisabled, toggleLabel, toggleLabelFirst, toggleProps, visible, ...wrapperProps }: Props<L>) => JSX.Element;
|
|
109
|
+
declare const ContextualMenu: <L>({ autoAdjust, children, className, closeOnEsc, closeOnOutsideClick, constrainPanelWidth, dropdownClassName, dropdownProps, hasToggleIcon, links, onToggleMenu, position, positionNode, scrollOverflow, toggle, toggleAppearance, toggleClassName, toggleDisabled, toggleLabel, toggleLabelFirst, toggleProps, visible, ...wrapperProps }: Props<L>) => JSX.Element;
|
|
106
110
|
export default ContextualMenu;
|
|
@@ -69,6 +69,7 @@ const ContextualMenu = _ref => {
|
|
|
69
69
|
position = "right",
|
|
70
70
|
positionNode,
|
|
71
71
|
scrollOverflow,
|
|
72
|
+
toggle,
|
|
72
73
|
toggleAppearance,
|
|
73
74
|
toggleClassName,
|
|
74
75
|
toggleDisabled,
|
|
@@ -82,7 +83,6 @@ const ContextualMenu = _ref => {
|
|
|
82
83
|
const wrapper = (0, _react.useRef)(null);
|
|
83
84
|
const [positionCoords, setPositionCoords] = (0, _react.useState)();
|
|
84
85
|
const [adjustedPosition, setAdjustedPosition] = (0, _react.useState)(position);
|
|
85
|
-
const hasToggle = hasToggleIcon || toggleLabel;
|
|
86
86
|
(0, _react.useEffect)(() => {
|
|
87
87
|
setAdjustedPosition(position);
|
|
88
88
|
}, [position, autoAdjust]);
|
|
@@ -106,14 +106,15 @@ const ContextualMenu = _ref => {
|
|
|
106
106
|
isOpen: visible,
|
|
107
107
|
onOpen: () => {
|
|
108
108
|
// Call the toggle callback, if supplied.
|
|
109
|
-
onToggleMenu
|
|
109
|
+
onToggleMenu === null || onToggleMenu === void 0 || onToggleMenu(true);
|
|
110
110
|
// When the menu opens then update the coordinates of the parent.
|
|
111
111
|
updatePositionCoords();
|
|
112
112
|
},
|
|
113
113
|
onClose: () => {
|
|
114
114
|
// Call the toggle callback, if supplied.
|
|
115
|
-
onToggleMenu
|
|
116
|
-
}
|
|
115
|
+
onToggleMenu === null || onToggleMenu === void 0 || onToggleMenu(false);
|
|
116
|
+
},
|
|
117
|
+
programmaticallyOpen: true
|
|
117
118
|
});
|
|
118
119
|
const previousVisible = (0, _hooks.usePrevious)(visible);
|
|
119
120
|
const labelNode = toggleLabel && typeof toggleLabel === "string" ? /*#__PURE__*/_react.default.createElement("span", null, toggleLabel) : /*#__PURE__*/_react.default.isValidElement(toggleLabel) ? toggleLabel : null;
|
|
@@ -166,32 +167,38 @@ const ContextualMenu = _ref => {
|
|
|
166
167
|
}, [positionNode, updatePositionCoords]);
|
|
167
168
|
(0, _hooks.useListener)(window, onResize, "resize", true, isOpen);
|
|
168
169
|
(0, _hooks.useListener)(window, onScroll, "scroll", false, isOpen, true);
|
|
170
|
+
let toggleNode = null;
|
|
171
|
+
if (toggle) {
|
|
172
|
+
toggleNode = toggle;
|
|
173
|
+
} else if (hasToggleIcon || toggleLabel) {
|
|
174
|
+
toggleNode = /*#__PURE__*/_react.default.createElement(_Button.default, _extends({
|
|
175
|
+
appearance: toggleAppearance,
|
|
176
|
+
"aria-controls": id,
|
|
177
|
+
"aria-expanded": isOpen ? "true" : "false",
|
|
178
|
+
"aria-label": toggleLabel ? null : Label.Toggle,
|
|
179
|
+
"aria-pressed": isOpen ? "true" : "false",
|
|
180
|
+
"aria-haspopup": "true",
|
|
181
|
+
className: (0, _classnames.default)("p-contextual-menu__toggle", toggleClassName),
|
|
182
|
+
disabled: toggleDisabled,
|
|
183
|
+
hasIcon: hasToggleIcon,
|
|
184
|
+
onClick: evt => {
|
|
185
|
+
if (!isOpen) {
|
|
186
|
+
openPortal(evt);
|
|
187
|
+
} else {
|
|
188
|
+
closePortal(evt);
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
type: "button"
|
|
192
|
+
}, toggleProps), toggleLabelFirst ? labelNode : null, hasToggleIcon ? /*#__PURE__*/_react.default.createElement("i", {
|
|
193
|
+
className: (0, _classnames.default)("p-icon--chevron-down p-contextual-menu__indicator", {
|
|
194
|
+
"is-light": ["negative", "positive"].includes(toggleAppearance)
|
|
195
|
+
})
|
|
196
|
+
}) : null, toggleLabelFirst ? null : labelNode);
|
|
197
|
+
}
|
|
169
198
|
return /*#__PURE__*/_react.default.createElement("span", _extends({
|
|
170
199
|
className: contextualMenuClassName,
|
|
171
200
|
ref: wrapperRef
|
|
172
|
-
}, wrapperProps),
|
|
173
|
-
appearance: toggleAppearance,
|
|
174
|
-
"aria-controls": id,
|
|
175
|
-
"aria-expanded": isOpen ? "true" : "false",
|
|
176
|
-
"aria-label": toggleLabel ? null : Label.Toggle,
|
|
177
|
-
"aria-pressed": isOpen ? "true" : "false",
|
|
178
|
-
"aria-haspopup": "true",
|
|
179
|
-
className: (0, _classnames.default)("p-contextual-menu__toggle", toggleClassName),
|
|
180
|
-
disabled: toggleDisabled,
|
|
181
|
-
hasIcon: hasToggleIcon,
|
|
182
|
-
onClick: evt => {
|
|
183
|
-
if (!isOpen) {
|
|
184
|
-
openPortal(evt);
|
|
185
|
-
} else {
|
|
186
|
-
closePortal(evt);
|
|
187
|
-
}
|
|
188
|
-
},
|
|
189
|
-
type: "button"
|
|
190
|
-
}, toggleProps), toggleLabelFirst ? labelNode : null, hasToggleIcon ? /*#__PURE__*/_react.default.createElement("i", {
|
|
191
|
-
className: (0, _classnames.default)("p-icon--chevron-down p-contextual-menu__indicator", {
|
|
192
|
-
"is-light": ["negative", "positive"].includes(toggleAppearance)
|
|
193
|
-
})
|
|
194
|
-
}) : null, toggleLabelFirst ? null : labelNode) : null, isOpen && /*#__PURE__*/_react.default.createElement(Portal, null, /*#__PURE__*/_react.default.createElement(_ContextualMenuDropdown.default, _extends({
|
|
201
|
+
}, wrapperProps), toggleNode, isOpen && /*#__PURE__*/_react.default.createElement(Portal, null, /*#__PURE__*/_react.default.createElement(_ContextualMenuDropdown.default, _extends({
|
|
195
202
|
adjustedPosition: adjustedPosition,
|
|
196
203
|
autoAdjust: autoAdjust,
|
|
197
204
|
handleClose: closePortal,
|
|
@@ -153,7 +153,7 @@ const ContextualMenuDropdown = _ref => {
|
|
|
153
153
|
contextualMenuClassName,
|
|
154
154
|
...props
|
|
155
155
|
} = _ref;
|
|
156
|
-
const dropdown = (0, _react.useRef)();
|
|
156
|
+
const dropdown = (0, _react.useRef)(null);
|
|
157
157
|
const [verticalPosition, setVerticalPosition] = (0, _react.useState)("bottom");
|
|
158
158
|
const [positionStyle, setPositionStyle] = (0, _react.useState)(getPositionStyle(adjustedPosition, verticalPosition, positionCoords, constrainPanelWidth));
|
|
159
159
|
const [maxHeight, setMaxHeight] = (0, _react.useState)();
|
|
@@ -162,6 +162,7 @@ const ContextualMenuDropdown = _ref => {
|
|
|
162
162
|
setPositionStyle(getPositionStyle(adjustedPosition, verticalPosition, positionCoords, constrainPanelWidth));
|
|
163
163
|
}, [adjustedPosition, positionCoords, verticalPosition, constrainPanelWidth]);
|
|
164
164
|
const updateVerticalPosition = (0, _react.useCallback)(() => {
|
|
165
|
+
var _dropdown$current$get;
|
|
165
166
|
if (!positionNode) {
|
|
166
167
|
return null;
|
|
167
168
|
}
|
|
@@ -170,18 +171,18 @@ const ContextualMenuDropdown = _ref => {
|
|
|
170
171
|
return null;
|
|
171
172
|
}
|
|
172
173
|
const scrollableParentRect = scrollableParent.getBoundingClientRect();
|
|
173
|
-
const
|
|
174
|
+
const toggleRect = positionNode.getBoundingClientRect();
|
|
174
175
|
|
|
175
176
|
// Calculate the rect in relation to the scrollableParent
|
|
176
|
-
const
|
|
177
|
-
top:
|
|
178
|
-
bottom:
|
|
179
|
-
height: rect.height
|
|
177
|
+
const relativeToScrollParentRect = {
|
|
178
|
+
top: toggleRect.top - scrollableParentRect.top,
|
|
179
|
+
bottom: toggleRect.bottom - scrollableParentRect.top
|
|
180
180
|
};
|
|
181
|
-
const
|
|
182
|
-
const
|
|
183
|
-
const dropdownHeight =
|
|
184
|
-
|
|
181
|
+
const scrollParentSpaceBelow = scrollableParentRect.height - relativeToScrollParentRect.bottom;
|
|
182
|
+
const scrollParentSpaceAbove = relativeToScrollParentRect.top;
|
|
183
|
+
const dropdownHeight = (_dropdown$current$get = dropdown.current.getBoundingClientRect().height) !== null && _dropdown$current$get !== void 0 ? _dropdown$current$get : 0;
|
|
184
|
+
const windowSpaceBelow = window.innerHeight - toggleRect.bottom;
|
|
185
|
+
setVerticalPosition(scrollParentSpaceBelow >= dropdownHeight && windowSpaceBelow >= dropdownHeight || windowSpaceBelow > scrollParentSpaceAbove ? "bottom" : "top");
|
|
185
186
|
}, [positionNode]);
|
|
186
187
|
|
|
187
188
|
// Update the position when the window fitment info changes.
|
|
@@ -12,7 +12,10 @@ export type MultiSelectProps = {
|
|
|
12
12
|
selectedItems?: MultiSelectItem[];
|
|
13
13
|
help?: string;
|
|
14
14
|
label?: string | null;
|
|
15
|
+
listSelected?: boolean;
|
|
16
|
+
onDeselectItem?: (item: MultiSelectItem) => void;
|
|
15
17
|
onItemsUpdate?: (items: MultiSelectItem[]) => void;
|
|
18
|
+
onSelectItem?: (item: MultiSelectItem) => void;
|
|
16
19
|
placeholder?: string;
|
|
17
20
|
required?: boolean;
|
|
18
21
|
items: MultiSelectItem[];
|
|
@@ -20,6 +23,7 @@ export type MultiSelectProps = {
|
|
|
20
23
|
renderItem?: (item: MultiSelectItem) => ReactNode;
|
|
21
24
|
dropdownHeader?: ReactNode;
|
|
22
25
|
dropdownFooter?: ReactNode;
|
|
26
|
+
showDropdownFooter?: boolean;
|
|
23
27
|
variant?: "condensed" | "search";
|
|
24
28
|
};
|
|
25
29
|
type GroupFn = (items: Parameters<typeof getGroupedItems>[0]) => ReturnType<typeof getGroupedItems>;
|
|
@@ -31,10 +35,11 @@ type MultiSelectDropdownProps = {
|
|
|
31
35
|
disabledItems: MultiSelectItem[];
|
|
32
36
|
header?: ReactNode;
|
|
33
37
|
updateItems: (newItems: MultiSelectItem[]) => void;
|
|
38
|
+
onDeselectItem?: (item: MultiSelectItem) => void;
|
|
39
|
+
onSelectItem?: (item: MultiSelectItem) => void;
|
|
34
40
|
footer?: ReactNode;
|
|
35
41
|
groupFn?: GroupFn;
|
|
36
42
|
sortFn?: SortFn;
|
|
37
|
-
shouldPinSelectedItems?: boolean;
|
|
38
43
|
} & React.HTMLAttributes<HTMLDivElement>;
|
|
39
44
|
declare const sortAlphabetically: (a: MultiSelectItem, b: MultiSelectItem) => number;
|
|
40
45
|
declare const getGroupedItems: (items: MultiSelectItem[]) => {
|
|
@@ -50,6 +50,8 @@ const MultiSelectDropdown = _ref2 => {
|
|
|
50
50
|
disabledItems,
|
|
51
51
|
header,
|
|
52
52
|
updateItems,
|
|
53
|
+
onSelectItem,
|
|
54
|
+
onDeselectItem,
|
|
53
55
|
isOpen,
|
|
54
56
|
footer,
|
|
55
57
|
sortFn = sortAlphabetically,
|
|
@@ -82,6 +84,11 @@ const MultiSelectDropdown = _ref2 => {
|
|
|
82
84
|
var _selectedItems$filter;
|
|
83
85
|
const newSelectedItems = checked ? [...selectedItems, foundItem] : (_selectedItems$filter = selectedItems.filter(item => "".concat(item.value) !== value)) !== null && _selectedItems$filter !== void 0 ? _selectedItems$filter : [];
|
|
84
86
|
updateItems(newSelectedItems);
|
|
87
|
+
if (checked) {
|
|
88
|
+
onSelectItem === null || onSelectItem === void 0 || onSelectItem(foundItem);
|
|
89
|
+
} else {
|
|
90
|
+
onDeselectItem === null || onDeselectItem === void 0 || onDeselectItem(foundItem);
|
|
91
|
+
}
|
|
85
92
|
}
|
|
86
93
|
};
|
|
87
94
|
return /*#__PURE__*/_react.default.createElement(_FadeInDown.FadeInDown, {
|
|
@@ -144,40 +151,33 @@ MultiSelectDropdown.propTypes = {
|
|
|
144
151
|
})).isRequired,
|
|
145
152
|
header: _propTypes.default.node,
|
|
146
153
|
updateItems: _propTypes.default.func.isRequired,
|
|
154
|
+
onDeselectItem: _propTypes.default.func,
|
|
155
|
+
onSelectItem: _propTypes.default.func,
|
|
147
156
|
footer: _propTypes.default.node,
|
|
148
157
|
groupFn: _propTypes.default.func,
|
|
149
|
-
sortFn: _propTypes.default.any
|
|
150
|
-
shouldPinSelectedItems: _propTypes.default.bool
|
|
158
|
+
sortFn: _propTypes.default.any
|
|
151
159
|
};
|
|
152
160
|
const MultiSelect = _ref4 => {
|
|
153
161
|
let {
|
|
154
162
|
disabled,
|
|
155
163
|
selectedItems: externalSelectedItems = [],
|
|
156
164
|
label,
|
|
165
|
+
listSelected = true,
|
|
157
166
|
onItemsUpdate,
|
|
167
|
+
onSelectItem,
|
|
168
|
+
onDeselectItem,
|
|
158
169
|
placeholder,
|
|
159
170
|
required = false,
|
|
160
171
|
items = [],
|
|
161
172
|
disabledItems = [],
|
|
162
173
|
dropdownHeader,
|
|
163
174
|
dropdownFooter,
|
|
175
|
+
showDropdownFooter = true,
|
|
164
176
|
variant = "search"
|
|
165
177
|
} = _ref4;
|
|
166
|
-
const
|
|
167
|
-
setIsDropdownOpen(false);
|
|
168
|
-
setFilter("");
|
|
169
|
-
});
|
|
170
|
-
(0, _index.useOnEscapePressed)(() => {
|
|
171
|
-
setIsDropdownOpen(false);
|
|
172
|
-
setFilter("");
|
|
173
|
-
});
|
|
178
|
+
const buttonRef = (0, _react.useRef)();
|
|
174
179
|
const [isDropdownOpen, setIsDropdownOpen] = (0, _react.useState)(false);
|
|
175
180
|
const [filter, setFilter] = (0, _react.useState)("");
|
|
176
|
-
(0, _react.useEffect)(() => {
|
|
177
|
-
if (!isDropdownOpen) {
|
|
178
|
-
setFilter("");
|
|
179
|
-
}
|
|
180
|
-
}, [isDropdownOpen]);
|
|
181
181
|
const [internalSelectedItems, setInternalSelectedItems] = (0, _react.useState)([]);
|
|
182
182
|
const selectedItems = externalSelectedItems || internalSelectedItems;
|
|
183
183
|
const updateItems = newItems => {
|
|
@@ -188,51 +188,9 @@ const MultiSelect = _ref4 => {
|
|
|
188
188
|
const dropdownId = (0, _react.useId)();
|
|
189
189
|
const inputId = (0, _react.useId)();
|
|
190
190
|
const selectedItemsLabel = selectedItems.filter(selectedItem => items.some(item => item.value === selectedItem.value)).map(el => el.label).join(", ");
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
className: "multi-select"
|
|
195
|
-
}, variant === "search" ? /*#__PURE__*/_react.default.createElement(_index.SearchBox, {
|
|
196
|
-
externallyControlled: true,
|
|
197
|
-
"aria-controls": dropdownId,
|
|
198
|
-
"aria-expanded": isDropdownOpen,
|
|
199
|
-
id: inputId,
|
|
200
|
-
role: "combobox",
|
|
201
|
-
"aria-label": label || placeholder || "Search",
|
|
202
|
-
disabled: disabled,
|
|
203
|
-
autoComplete: "off",
|
|
204
|
-
onChange: value => {
|
|
205
|
-
setFilter(value);
|
|
206
|
-
// reopen if dropdown has been closed via ESC
|
|
207
|
-
setIsDropdownOpen(true);
|
|
208
|
-
},
|
|
209
|
-
onFocus: () => setIsDropdownOpen(true),
|
|
210
|
-
placeholder: placeholder !== null && placeholder !== void 0 ? placeholder : "Search",
|
|
211
|
-
required: required,
|
|
212
|
-
type: "text",
|
|
213
|
-
value: filter,
|
|
214
|
-
className: "multi-select__input"
|
|
215
|
-
}) : /*#__PURE__*/_react.default.createElement("button", {
|
|
216
|
-
role: "combobox",
|
|
217
|
-
type: "button",
|
|
218
|
-
"aria-label": label || placeholder || "Select items",
|
|
219
|
-
"aria-controls": dropdownId,
|
|
220
|
-
"aria-expanded": isDropdownOpen,
|
|
221
|
-
className: "multi-select__select-button",
|
|
222
|
-
onClick: () => {
|
|
223
|
-
setIsDropdownOpen(isOpen => !isOpen);
|
|
224
|
-
}
|
|
225
|
-
}, /*#__PURE__*/_react.default.createElement("span", {
|
|
226
|
-
className: "multi-select__condensed-text"
|
|
227
|
-
}, selectedItems.length > 0 ? selectedItemsLabel : placeholder !== null && placeholder !== void 0 ? placeholder : "Select items")), /*#__PURE__*/_react.default.createElement(MultiSelectDropdown, {
|
|
228
|
-
id: dropdownId,
|
|
229
|
-
isOpen: isDropdownOpen,
|
|
230
|
-
items: filter.length > 0 ? items.filter(item => item.label.toLowerCase().includes(filter.toLowerCase())) : items,
|
|
231
|
-
selectedItems: selectedItems,
|
|
232
|
-
disabledItems: disabledItems,
|
|
233
|
-
header: dropdownHeader,
|
|
234
|
-
updateItems: updateItems,
|
|
235
|
-
footer: dropdownFooter ? dropdownFooter : /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement(_index.Button, {
|
|
191
|
+
let footer = null;
|
|
192
|
+
if (showDropdownFooter) {
|
|
193
|
+
footer = dropdownFooter ? dropdownFooter : /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement(_index.Button, {
|
|
236
194
|
appearance: "link",
|
|
237
195
|
onClick: () => {
|
|
238
196
|
const enabledItems = items.filter(item => !disabledItems.some(disabledItem => disabledItem.value === item.value));
|
|
@@ -246,8 +204,82 @@ const MultiSelect = _ref4 => {
|
|
|
246
204
|
updateItems(disabledSelectedItems);
|
|
247
205
|
},
|
|
248
206
|
type: "button"
|
|
249
|
-
}, "Clear"))
|
|
250
|
-
}
|
|
207
|
+
}, "Clear"));
|
|
208
|
+
}
|
|
209
|
+
return /*#__PURE__*/_react.default.createElement(_index.ContextualMenu, {
|
|
210
|
+
className: "multi-select",
|
|
211
|
+
onToggleMenu: isOpen => {
|
|
212
|
+
if (!isOpen) {
|
|
213
|
+
setFilter("");
|
|
214
|
+
}
|
|
215
|
+
// Handle syncing the state when toggling the menu from within the
|
|
216
|
+
// contextual menu component e.g. when clicking outside.
|
|
217
|
+
if (isOpen !== isDropdownOpen) {
|
|
218
|
+
setIsDropdownOpen(isOpen);
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
position: "left",
|
|
222
|
+
constrainPanelWidth: true,
|
|
223
|
+
toggle: variant === "search" ? /*#__PURE__*/_react.default.createElement(_index.SearchBox, {
|
|
224
|
+
externallyControlled: true,
|
|
225
|
+
"aria-controls": dropdownId,
|
|
226
|
+
"aria-expanded": isDropdownOpen,
|
|
227
|
+
id: inputId,
|
|
228
|
+
role: "combobox",
|
|
229
|
+
"aria-label": label || placeholder || "Search",
|
|
230
|
+
disabled: disabled,
|
|
231
|
+
autoComplete: "off",
|
|
232
|
+
onChange: value => {
|
|
233
|
+
setFilter(value);
|
|
234
|
+
// reopen if dropdown has been closed via ESC
|
|
235
|
+
setIsDropdownOpen(true);
|
|
236
|
+
},
|
|
237
|
+
onFocus: () => setIsDropdownOpen(true),
|
|
238
|
+
placeholder: placeholder !== null && placeholder !== void 0 ? placeholder : "Search",
|
|
239
|
+
required: required,
|
|
240
|
+
type: "text",
|
|
241
|
+
value: filter,
|
|
242
|
+
className: "multi-select__input"
|
|
243
|
+
}) : /*#__PURE__*/_react.default.createElement("button", {
|
|
244
|
+
role: "combobox",
|
|
245
|
+
type: "button",
|
|
246
|
+
"aria-label": label || placeholder || "Select items",
|
|
247
|
+
"aria-controls": dropdownId,
|
|
248
|
+
"aria-expanded": isDropdownOpen,
|
|
249
|
+
className: "multi-select__select-button",
|
|
250
|
+
onClick: () => {
|
|
251
|
+
setIsDropdownOpen(!isDropdownOpen);
|
|
252
|
+
},
|
|
253
|
+
onMouseDown: event => {
|
|
254
|
+
// If the dropdown is open when this button is clicked the
|
|
255
|
+
// click-outside event will fire which will close the dropdown, but
|
|
256
|
+
// then the button click event will fire which will immediately
|
|
257
|
+
// reopen the dropdown.
|
|
258
|
+
// To prevent this we can stop the propagation to the click event
|
|
259
|
+
// while `isDropdownOpen` is still set to `true` (by the time we
|
|
260
|
+
// get to the `onClick` event `isDropdownOpen` will already be `false`,
|
|
261
|
+
// hence having to do this on mouse down).
|
|
262
|
+
if (isDropdownOpen) {
|
|
263
|
+
event.stopPropagation();
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
ref: buttonRef
|
|
267
|
+
}, /*#__PURE__*/_react.default.createElement("span", {
|
|
268
|
+
className: "multi-select__condensed-text"
|
|
269
|
+
}, listSelected && selectedItems.length > 0 ? selectedItemsLabel : placeholder !== null && placeholder !== void 0 ? placeholder : "Select items")),
|
|
270
|
+
visible: isDropdownOpen
|
|
271
|
+
}, /*#__PURE__*/_react.default.createElement(MultiSelectDropdown, {
|
|
272
|
+
id: dropdownId,
|
|
273
|
+
isOpen: isDropdownOpen,
|
|
274
|
+
items: filter.length > 0 ? items.filter(item => item.label.toLowerCase().includes(filter.toLowerCase())) : items,
|
|
275
|
+
selectedItems: selectedItems,
|
|
276
|
+
disabledItems: disabledItems,
|
|
277
|
+
header: dropdownHeader,
|
|
278
|
+
updateItems: updateItems,
|
|
279
|
+
onSelectItem: onSelectItem,
|
|
280
|
+
onDeselectItem: onDeselectItem,
|
|
281
|
+
footer: footer
|
|
282
|
+
}));
|
|
251
283
|
};
|
|
252
284
|
exports.MultiSelect = MultiSelect;
|
|
253
285
|
MultiSelect.propTypes = {
|
|
@@ -260,7 +292,10 @@ MultiSelect.propTypes = {
|
|
|
260
292
|
})),
|
|
261
293
|
help: _propTypes.default.string,
|
|
262
294
|
label: _propTypes.default.oneOfType([_propTypes.default.string, _propTypes.default.oneOf([null])]),
|
|
295
|
+
listSelected: _propTypes.default.bool,
|
|
296
|
+
onDeselectItem: _propTypes.default.func,
|
|
263
297
|
onItemsUpdate: _propTypes.default.func,
|
|
298
|
+
onSelectItem: _propTypes.default.func,
|
|
264
299
|
placeholder: _propTypes.default.string,
|
|
265
300
|
required: _propTypes.default.bool,
|
|
266
301
|
items: _propTypes.default.arrayOf(_propTypes.default.shape({
|
|
@@ -276,5 +311,6 @@ MultiSelect.propTypes = {
|
|
|
276
311
|
renderItem: _propTypes.default.func,
|
|
277
312
|
dropdownHeader: _propTypes.default.node,
|
|
278
313
|
dropdownFooter: _propTypes.default.node,
|
|
314
|
+
showDropdownFooter: _propTypes.default.bool,
|
|
279
315
|
variant: _propTypes.default.oneOf(["condensed", "search"])
|
|
280
316
|
};
|
|
@@ -10,7 +10,9 @@ $dropdown-max-height: 20rem;
|
|
|
10
10
|
// XXX: This is a workaround for https://github.com/canonical/vanilla-framework/issues/5030
|
|
11
11
|
@include vf-b-forms;
|
|
12
12
|
|
|
13
|
+
margin-bottom: $input-margin-bottom;
|
|
13
14
|
position: relative;
|
|
15
|
+
width: 100%;
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
.multi-select .p-form-validation__message {
|
|
@@ -26,6 +28,7 @@ $dropdown-max-height: 20rem;
|
|
|
26
28
|
|
|
27
29
|
.multi-select__input {
|
|
28
30
|
cursor: pointer;
|
|
31
|
+
margin-bottom: 0;
|
|
29
32
|
position: relative;
|
|
30
33
|
|
|
31
34
|
&.items-selected {
|
|
@@ -40,19 +43,6 @@ $dropdown-max-height: 20rem;
|
|
|
40
43
|
}
|
|
41
44
|
}
|
|
42
45
|
|
|
43
|
-
.multi-select__dropdown {
|
|
44
|
-
background-color: $colors--theme--background-default;
|
|
45
|
-
box-shadow: $box-shadow;
|
|
46
|
-
color: $colors--theme--text-default;
|
|
47
|
-
left: 0;
|
|
48
|
-
max-height: $dropdown-max-height;
|
|
49
|
-
overflow: auto;
|
|
50
|
-
padding-top: $spv--small;
|
|
51
|
-
position: absolute;
|
|
52
|
-
right: 0;
|
|
53
|
-
top: calc(100% - #{$input-margin-bottom});
|
|
54
|
-
}
|
|
55
|
-
|
|
56
46
|
.multi-select__dropdown--side-by-side {
|
|
57
47
|
display: flex;
|
|
58
48
|
flex-wrap: wrap;
|
|
@@ -132,6 +122,10 @@ $dropdown-max-height: 20rem;
|
|
|
132
122
|
position: relative;
|
|
133
123
|
z-index: 0;
|
|
134
124
|
|
|
125
|
+
.multi-select & {
|
|
126
|
+
margin-bottom: 0;
|
|
127
|
+
}
|
|
128
|
+
|
|
135
129
|
&::after {
|
|
136
130
|
content: "";
|
|
137
131
|
margin-left: $sph--large;
|
|
@@ -28,7 +28,7 @@ export type Props = PropsWithSpread<{
|
|
|
28
28
|
/**
|
|
29
29
|
* A function that is called when the user clicks the search icon or presses enter
|
|
30
30
|
*/
|
|
31
|
-
onSearch?: () => void;
|
|
31
|
+
onSearch?: (inputValue: string) => void;
|
|
32
32
|
/**
|
|
33
33
|
* A function that is called when the user clicks the reset icon
|
|
34
34
|
*/
|
|
@@ -37,6 +37,10 @@ export type Props = PropsWithSpread<{
|
|
|
37
37
|
* A search input placeholder message.
|
|
38
38
|
*/
|
|
39
39
|
placeholder?: string;
|
|
40
|
+
/**
|
|
41
|
+
* Whether the search input should lose focus when searching.
|
|
42
|
+
*/
|
|
43
|
+
shouldBlurOnSearch?: boolean;
|
|
40
44
|
/**
|
|
41
45
|
* Whether the search input should receive focus after pressing the reset button
|
|
42
46
|
*/
|
|
@@ -31,6 +31,7 @@ const SearchBox = /*#__PURE__*/_react.default.forwardRef((_ref, forwardedRef) =>
|
|
|
31
31
|
onSearch,
|
|
32
32
|
onClear,
|
|
33
33
|
placeholder = "Search",
|
|
34
|
+
shouldBlurOnSearch = true,
|
|
34
35
|
shouldRefocusAfterReset,
|
|
35
36
|
value,
|
|
36
37
|
...props
|
|
@@ -45,11 +46,13 @@ const SearchBox = /*#__PURE__*/_react.default.forwardRef((_ref, forwardedRef) =>
|
|
|
45
46
|
}
|
|
46
47
|
};
|
|
47
48
|
const triggerSearch = () => {
|
|
48
|
-
onSearch
|
|
49
|
+
onSearch === null || onSearch === void 0 || onSearch(externallyControlled ? value : internalRef.current.value);
|
|
49
50
|
};
|
|
50
51
|
const onKeyDown = e => {
|
|
51
52
|
if (e.key === "Enter" && internalRef.current.checkValidity()) {
|
|
52
|
-
|
|
53
|
+
if (shouldBlurOnSearch) {
|
|
54
|
+
internalRef.current.blur();
|
|
55
|
+
}
|
|
53
56
|
triggerSearch();
|
|
54
57
|
}
|
|
55
58
|
};
|