@jobber/components 4.46.5 → 4.47.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.
|
@@ -1,14 +1,24 @@
|
|
|
1
1
|
import { ReactElement } from "react";
|
|
2
|
+
import { XOR } from "ts-xor";
|
|
2
3
|
import { ComboboxOption } from "../../Combobox.types";
|
|
3
|
-
interface
|
|
4
|
+
interface ComboboxCloseProps {
|
|
4
5
|
/**
|
|
5
|
-
*
|
|
6
|
+
* Callback function invoked upon the selection of an option. Provides the selected option(s) as an argument.
|
|
6
7
|
*/
|
|
7
|
-
readonly
|
|
8
|
+
readonly onSelect: (selection: ComboboxOption[]) => void;
|
|
9
|
+
}
|
|
10
|
+
interface ComoboboxSelectProps {
|
|
11
|
+
/**
|
|
12
|
+
*
|
|
13
|
+
* Callback function invoked upon the Combobox menu closing. Provides the selected option(s) as an argument.
|
|
14
|
+
*/
|
|
15
|
+
readonly onClose: (selection: ComboboxOption[]) => void;
|
|
16
|
+
}
|
|
17
|
+
interface ComboboxContentBaseProps {
|
|
8
18
|
/**
|
|
9
|
-
*
|
|
19
|
+
* List of selectable options to display.
|
|
10
20
|
*/
|
|
11
|
-
readonly
|
|
21
|
+
readonly options: ComboboxOption[];
|
|
12
22
|
/**
|
|
13
23
|
* Optional action button(s) to display at the bottom of the list.
|
|
14
24
|
*/
|
|
@@ -22,12 +32,13 @@ interface ComboboxContentProps {
|
|
|
22
32
|
* @default ""
|
|
23
33
|
* @type string
|
|
24
34
|
*/
|
|
25
|
-
readonly selected: ComboboxOption
|
|
35
|
+
readonly selected: ComboboxOption[];
|
|
26
36
|
/**
|
|
27
37
|
* The encapsulating noun for the content of the combobox. Used
|
|
28
38
|
* in the empty state, and search placeholder. Should be pluralized.
|
|
29
39
|
*/
|
|
30
40
|
readonly subjectNoun?: string;
|
|
31
41
|
}
|
|
42
|
+
type ComboboxContentProps = ComboboxContentBaseProps & XOR<ComboboxCloseProps, ComoboboxSelectProps>;
|
|
32
43
|
export declare function ComboboxContent(props: ComboboxContentProps): JSX.Element;
|
|
33
44
|
export {};
|
|
@@ -3,11 +3,12 @@ import { ComboboxOption } from "../../Combobox.types";
|
|
|
3
3
|
interface ComboboxListProps {
|
|
4
4
|
options: ComboboxOption[];
|
|
5
5
|
showEmptyState: boolean;
|
|
6
|
-
selected: ComboboxOption
|
|
6
|
+
selected: ComboboxOption[];
|
|
7
7
|
optionsListRef: React.RefObject<HTMLUListElement>;
|
|
8
|
-
|
|
8
|
+
setFirstSelectedElement: React.Dispatch<SetStateAction<HTMLElement | null>>;
|
|
9
9
|
selectionHandler: (option: ComboboxOption) => void;
|
|
10
10
|
searchValue: string;
|
|
11
|
+
multiselect: boolean;
|
|
11
12
|
subjectNoun?: string;
|
|
12
13
|
}
|
|
13
14
|
export declare function ComboboxList(props: ComboboxListProps): JSX.Element;
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
/// <reference types="react" />
|
|
2
2
|
import { ComboboxOption } from "../Combobox.types";
|
|
3
|
-
|
|
3
|
+
interface useComboboxContent {
|
|
4
4
|
searchValue: string;
|
|
5
5
|
setSearchValue: React.Dispatch<React.SetStateAction<string>>;
|
|
6
|
-
|
|
6
|
+
setFirstSelectedElement: React.Dispatch<React.SetStateAction<HTMLElement | null>>;
|
|
7
7
|
filteredOptions: ComboboxOption[];
|
|
8
8
|
optionsListRef: React.RefObject<HTMLUListElement>;
|
|
9
|
-
|
|
9
|
+
selectedOptions: ComboboxOption[];
|
|
10
|
+
setInternalSelected: (selected: ComboboxOption[]) => void;
|
|
11
|
+
}
|
|
12
|
+
export declare function useComboboxContent(options: ComboboxOption[], open: boolean, selected: ComboboxOption[], onClose: ((selection: ComboboxOption[]) => void) | undefined): useComboboxContent;
|
|
13
|
+
export {};
|
package/dist/Combobox/index.js
CHANGED
|
@@ -44,7 +44,7 @@ function ComboboxSearch(props) {
|
|
|
44
44
|
}, [props.open]);
|
|
45
45
|
return (React__default["default"].createElement("div", { className: styles$4.search },
|
|
46
46
|
React__default["default"].createElement("input", { type: "search", ref: searchRef, className: styles$4.searchInput, placeholder: props.placeholder ? `Search ${props.placeholder}` : "Search", onChange: (event) => handleSearch(event), value: props.searchValue }),
|
|
47
|
-
props.searchValue && (React__default["default"].createElement("button", { className: styles$4.clearSearch, onClick: clearSearch, "data-testid": "ATL-Combobox-Content-Search-Clear", "aria-label": "Clear search" },
|
|
47
|
+
props.searchValue && (React__default["default"].createElement("button", { className: styles$4.clearSearch, onClick: clearSearch, type: "button", "data-testid": "ATL-Combobox-Content-Search-Clear", "aria-label": "Clear search" },
|
|
48
48
|
React__default["default"].createElement(Icon.Icon, { name: "remove", size: "small" })))));
|
|
49
49
|
function clearSearch() {
|
|
50
50
|
var _a;
|
|
@@ -61,14 +61,16 @@ var styles$3 = {"container":"_5n7VvEn39-Y-","optionsList":"KhX5VfZjAog-","option
|
|
|
61
61
|
styleInject_es.styleInject(css_248z$3);
|
|
62
62
|
|
|
63
63
|
function ComboboxList(props) {
|
|
64
|
+
let hasSeenFirstSelected = false;
|
|
64
65
|
return (React__default["default"].createElement("div", { className: styles$3.container },
|
|
65
|
-
!props.showEmptyState && props.options.length > 0 && (React__default["default"].createElement("ul", { className: styles$3.optionsList, role: "listbox", ref: props.optionsListRef }, !props.showEmptyState &&
|
|
66
|
+
!props.showEmptyState && props.options.length > 0 && (React__default["default"].createElement("ul", { className: styles$3.optionsList, role: "listbox", "aria-multiselectable": props.multiselect, ref: props.optionsListRef }, !props.showEmptyState &&
|
|
66
67
|
props.options.map(option => {
|
|
67
|
-
|
|
68
|
-
const isSelected = option.id.toString() === ((_a = props.selected) === null || _a === void 0 ? void 0 : _a.id.toString());
|
|
68
|
+
const isSelected = props.selected.some(selection => selection.id.toString() === option.id.toString());
|
|
69
69
|
return (React__default["default"].createElement("li", { ref: listItem => {
|
|
70
|
-
if (isSelected)
|
|
71
|
-
props.
|
|
70
|
+
if (isSelected && !hasSeenFirstSelected) {
|
|
71
|
+
props.setFirstSelectedElement(listItem);
|
|
72
|
+
hasSeenFirstSelected = true;
|
|
73
|
+
}
|
|
72
74
|
}, key: option.id, tabIndex: -1, role: "option", "aria-selected": isSelected, onClick: () => props.selectionHandler(option), className: classnames__default["default"](styles$3.option, {
|
|
73
75
|
[styles$3.selectedOption]: isSelected,
|
|
74
76
|
}) },
|
|
@@ -104,24 +106,38 @@ function ComboboxContextProvider(props) {
|
|
|
104
106
|
props.children)));
|
|
105
107
|
}
|
|
106
108
|
|
|
107
|
-
function useComboboxContent(options, open) {
|
|
109
|
+
function useComboboxContent(options, open, selected, onClose) {
|
|
108
110
|
const [searchValue, setSearchValue] = React.useState("");
|
|
109
|
-
const [
|
|
111
|
+
const [firstSelectedElement, setFirstSelectedElement] = React.useState(null);
|
|
110
112
|
const optionsListRef = React.useRef(null);
|
|
111
113
|
const filteredOptions = options.filter(option => option.label.toLowerCase().includes(searchValue.toLowerCase()));
|
|
114
|
+
const [internalSelected, setInternalSelected] = React.useState(selected);
|
|
115
|
+
const selectedOptions = onClose ? internalSelected : selected;
|
|
116
|
+
React.useEffect(() => {
|
|
117
|
+
if (!open && onClose) {
|
|
118
|
+
onClose(selectedOptions);
|
|
119
|
+
}
|
|
120
|
+
}, [open, onClose, selectedOptions]);
|
|
112
121
|
React.useEffect(() => {
|
|
113
|
-
if (open &&
|
|
114
|
-
|
|
122
|
+
if (open && firstSelectedElement) {
|
|
123
|
+
firstSelectedElement === null || firstSelectedElement === void 0 ? void 0 : firstSelectedElement.scrollIntoView({
|
|
115
124
|
block: "nearest",
|
|
116
125
|
});
|
|
117
126
|
}
|
|
118
|
-
}, [open,
|
|
127
|
+
}, [open, firstSelectedElement]);
|
|
128
|
+
React.useEffect(() => {
|
|
129
|
+
if (selectedOptions.length === 0) {
|
|
130
|
+
setFirstSelectedElement(null);
|
|
131
|
+
}
|
|
132
|
+
}, [selectedOptions]);
|
|
119
133
|
return {
|
|
120
134
|
searchValue,
|
|
121
135
|
setSearchValue,
|
|
122
|
-
|
|
136
|
+
setFirstSelectedElement,
|
|
123
137
|
filteredOptions,
|
|
124
138
|
optionsListRef,
|
|
139
|
+
selectedOptions,
|
|
140
|
+
setInternalSelected,
|
|
125
141
|
};
|
|
126
142
|
}
|
|
127
143
|
|
|
@@ -202,23 +218,42 @@ function useComboboxAccessibility(selectionCallback, filteredOptions, optionsLis
|
|
|
202
218
|
}
|
|
203
219
|
|
|
204
220
|
function ComboboxContent(props) {
|
|
205
|
-
const { open, setOpen, wrapperRef } = React__default["default"].useContext(ComboboxContext);
|
|
221
|
+
const { open, setOpen, wrapperRef, multiselect } = React__default["default"].useContext(ComboboxContext);
|
|
206
222
|
const optionsExist = props.options.length > 0;
|
|
207
|
-
const { searchValue, setSearchValue,
|
|
223
|
+
const { searchValue, setSearchValue, setFirstSelectedElement, filteredOptions, optionsListRef, selectedOptions, setInternalSelected, } = useComboboxContent(props.options, open, props.selected, props.onClose);
|
|
208
224
|
const { popperRef, popperStyles, attributes } = useComboboxAccessibility(handleSelection, filteredOptions, optionsListRef, open, setOpen, wrapperRef);
|
|
209
225
|
const template = (React__default["default"].createElement("div", Object.assign({ ref: popperRef, id: "ATL-Combobox-Content", "data-testid": "ATL-Combobox-Content", tabIndex: 0, className: classnames__default["default"](styles$5.content, { [styles$5.hidden]: !open }), style: popperStyles.popper }, attributes.popper),
|
|
210
226
|
React__default["default"].createElement(ComboboxSearch, { open: open, placeholder: props.subjectNoun, searchValue: searchValue, setSearchValue: setSearchValue }),
|
|
211
|
-
React__default["default"].createElement(ComboboxList, { showEmptyState: !optionsExist, options: filteredOptions, selected:
|
|
227
|
+
React__default["default"].createElement(ComboboxList, { multiselect: multiselect, showEmptyState: !optionsExist, options: filteredOptions, selected: selectedOptions, optionsListRef: optionsListRef, setFirstSelectedElement: setFirstSelectedElement, selectionHandler: handleSelection, searchValue: searchValue, subjectNoun: props.subjectNoun }),
|
|
212
228
|
props.children && (React__default["default"].createElement("div", { className: styles$5.actions, role: "group" }, React__default["default"].Children.toArray(props.children).map((child, index, childrenArray) => (React__default["default"].createElement("div", { key: index, className: classnames__default["default"]({
|
|
213
229
|
[styles$5.actionPadding]: index === childrenArray.length - 1,
|
|
214
230
|
}) }, child)))))));
|
|
215
231
|
return ReactDOM__default["default"].createPortal(template, document.body);
|
|
216
232
|
function handleSelection(selection) {
|
|
217
|
-
props.onSelect
|
|
233
|
+
const callbackHandler = props.onSelect
|
|
234
|
+
? props.onSelect
|
|
235
|
+
: setInternalSelected;
|
|
236
|
+
if (multiselect) {
|
|
237
|
+
handleMultiSelect(callbackHandler, selectedOptions, selection);
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
handleSingleSelect(callbackHandler, selection);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
function handleSingleSelect(selectCallback, selection) {
|
|
244
|
+
selectCallback([selection]);
|
|
218
245
|
setSearchValue("");
|
|
219
246
|
setOpen(false);
|
|
220
247
|
}
|
|
221
248
|
}
|
|
249
|
+
function handleMultiSelect(selectCallback, selected, selection) {
|
|
250
|
+
if (selected.some(s => s.id === selection.id)) {
|
|
251
|
+
selectCallback(selected.filter(s => s.id !== selection.id));
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
selectCallback([...selected, selection]);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
222
257
|
|
|
223
258
|
var css_248z$1 = ".Ow9wSNnGP-g- {\n width: 100%;\n box-sizing: border-box;\n padding: 0 calc(16px / 2);\n padding: 0 var(--space-small);\n background-color: rgba(255, 255, 255, 1);\n background-color: var(--color-surface);\n}\n\n.ncp0nKHQ7d8- {\n position: relative;\n width: 100%;\n min-height: calc((calc(16px * 3) - calc(16px / 2)));\n min-height: calc((var(--space-largest) - var(--space-small)));\n padding: calc(16px / 2) 0;\n padding: var(--space-small) 0;\n border: none;\n border-radius: calc(16px / 4);\n border-radius: var(--radius-large);\n text-align: left;\n background-color: rgba(255, 255, 255, 1);\n background-color: var(--color-surface);\n cursor: pointer;\n transition: all 200ms;\n transition: all var(--timing-base);\n}\n\n.ncp0nKHQ7d8-:focus {\n box-shadow: 0px 0px calc(16px / 4) calc(16px / 8)\n rgb(231, 213, 87);\n box-shadow: var(--shadow-focus);\n background-color: rgb(244, 244, 244);\n background-color: var(--color-surface--background);\n}\n\n.ncp0nKHQ7d8-:hover {\n background-color: rgb(244, 244, 244);\n background-color: var(--color-surface--background);\n}\n\n.ncp0nKHQ7d8- span {\n margin-left: calc(16px / 2);\n margin-left: var(--space-small);\n}\n";
|
|
224
259
|
var styles$1 = {"actionContainer":"Ow9wSNnGP-g-","actionButton":"ncp0nKHQ7d8-"};
|
|
@@ -226,7 +261,7 @@ styleInject_es.styleInject(css_248z$1);
|
|
|
226
261
|
|
|
227
262
|
function ComboboxAction(props) {
|
|
228
263
|
return (React__default["default"].createElement("div", { className: styles$1.actionContainer },
|
|
229
|
-
React__default["default"].createElement("button", { className: styles$1.actionButton, onClick: props.onClick, "aria-label": props.label },
|
|
264
|
+
React__default["default"].createElement("button", { className: styles$1.actionButton, onClick: props.onClick, type: "button", "aria-label": props.label },
|
|
230
265
|
React__default["default"].createElement(Typography.Typography, { element: "span", size: "base", textColor: "green", fontWeight: "semiBold" }, props.label))));
|
|
231
266
|
}
|
|
232
267
|
|
|
@@ -241,7 +276,7 @@ styleInject_es.styleInject(css_248z);
|
|
|
241
276
|
|
|
242
277
|
function ComboboxTriggerChip(props) {
|
|
243
278
|
const { open, setOpen } = React__default["default"].useContext(ComboboxContext);
|
|
244
|
-
return (React__default["default"].createElement("button", { className: styles.trigger, role: "combobox", "aria-expanded": open, "aria-haspopup": "listbox", "aria-controls": "ATL-Combobox-Content", "aria-autocomplete": "list", onClick: () => {
|
|
279
|
+
return (React__default["default"].createElement("button", { className: styles.trigger, role: "combobox", type: "button", "aria-expanded": open, "aria-haspopup": "listbox", "aria-controls": "ATL-Combobox-Content", "aria-autocomplete": "list", onClick: () => {
|
|
245
280
|
setOpen(!open);
|
|
246
281
|
} },
|
|
247
282
|
React__default["default"].createElement(Typography.Typography, { fontWeight: "semiBold", textColor: "heading" }, props.label),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jobber/components",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.47.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -84,5 +84,5 @@
|
|
|
84
84
|
"> 1%",
|
|
85
85
|
"IE 10"
|
|
86
86
|
],
|
|
87
|
-
"gitHead": "
|
|
87
|
+
"gitHead": "8d8bf2b50b1fa2e310959fdcc803e2601b73e15c"
|
|
88
88
|
}
|