@jobber/components 4.46.4 → 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.
- package/dist/Combobox/components/ComboboxContent/ComboboxContent.d.ts +17 -6
- package/dist/Combobox/components/ComboboxContent/ComboboxList.d.ts +3 -2
- package/dist/Combobox/hooks/useComboboxContent.d.ts +7 -3
- package/dist/Combobox/index.js +53 -18
- package/dist/DataList/DataList.types.d.ts +9 -1
- package/dist/DataList/components/DataListSearch/DataListSearch.d.ts +1 -1
- package/dist/DataList/index.js +10 -3
- package/package.json +2 -2
|
@@ -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),
|
|
@@ -117,7 +117,15 @@ export interface DataListSearchProps {
|
|
|
117
117
|
* prepended by "Search" or just falls back to "Search".
|
|
118
118
|
*/
|
|
119
119
|
readonly placeholder?: string;
|
|
120
|
-
|
|
120
|
+
/**
|
|
121
|
+
* The initial value of the search input.
|
|
122
|
+
*
|
|
123
|
+
* Updating this prop after the component has mounted will rerender the
|
|
124
|
+
* component with the new value. Only update the value of this when you
|
|
125
|
+
* absolutely have to.
|
|
126
|
+
*/
|
|
127
|
+
readonly initialValue?: string;
|
|
128
|
+
readonly onSearch: (value: string) => void;
|
|
121
129
|
}
|
|
122
130
|
export interface DataListFiltersProps {
|
|
123
131
|
readonly children: ReactElement | ReactElement[];
|
|
@@ -3,6 +3,6 @@ import { DataListSearchProps } from "../../DataList.types";
|
|
|
3
3
|
export declare const DATA_LIST_SEARCH_TEST_ID = "ATL-DataList-Search-input-wrapper";
|
|
4
4
|
export declare function DataListSearch(_: DataListSearchProps): null;
|
|
5
5
|
/**
|
|
6
|
-
* Renders the DataList.Search component
|
|
6
|
+
* Renders the DataList.Search component.
|
|
7
7
|
*/
|
|
8
8
|
export declare function InternalDataListSearch(): JSX.Element | null;
|
package/dist/DataList/index.js
CHANGED
|
@@ -811,7 +811,7 @@ function DataListSearch(_) {
|
|
|
811
811
|
return null;
|
|
812
812
|
}
|
|
813
813
|
/**
|
|
814
|
-
* Renders the DataList.Search component
|
|
814
|
+
* Renders the DataList.Search component.
|
|
815
815
|
*/
|
|
816
816
|
function InternalDataListSearch() {
|
|
817
817
|
const inputRef = React.useRef(null);
|
|
@@ -820,14 +820,18 @@ function InternalDataListSearch() {
|
|
|
820
820
|
const debouncedSearch = React.useCallback(debounce__default["default"]((value) => { var _a; return (_a = searchComponent === null || searchComponent === void 0 ? void 0 : searchComponent.props) === null || _a === void 0 ? void 0 : _a.onSearch(value); }, SEARCH_DEBOUNCE_DELAY), [searchComponent === null || searchComponent === void 0 ? void 0 : searchComponent.props.onSearch]);
|
|
821
821
|
if (!searchComponent)
|
|
822
822
|
return null;
|
|
823
|
-
const { placeholder } = searchComponent.props;
|
|
823
|
+
const { placeholder, initialValue } = searchComponent.props;
|
|
824
824
|
return (React__default["default"].createElement("div", { className: classnames__default["default"]({
|
|
825
825
|
[styles$4.withNoFilters]: !filterComponent && !sorting,
|
|
826
826
|
}) },
|
|
827
827
|
React__default["default"].createElement("div", { "data-testid": DATA_LIST_SEARCH_TEST_ID, className: classnames__default["default"](styles$4.searchInput, {
|
|
828
828
|
[styles$4.searchInputVisible]: visible,
|
|
829
829
|
}) },
|
|
830
|
-
React__default["default"].createElement(InputText.InputText
|
|
830
|
+
React__default["default"].createElement(InputText.InputText
|
|
831
|
+
// If the initial value changes, reset the input.
|
|
832
|
+
, {
|
|
833
|
+
// If the initial value changes, reset the input.
|
|
834
|
+
key: initialValue, defaultValue: initialValue, ref: inputRef, placeholder: getPlaceholder(), onChange: debouncedSearch, prefix: { icon: "search" }, size: "small" })),
|
|
831
835
|
React__default["default"].createElement("div", { className: styles$4.searchButton },
|
|
832
836
|
React__default["default"].createElement(AnimatedSwitcher.AnimatedSwitcher, { switched: visible, initialChild: React__default["default"].createElement(Button.Button, { variation: "subtle", icon: "search", ariaLabel: "Search", onClick: toggleSearch }), switchTo: React__default["default"].createElement(Button.Button, { variation: "subtle", icon: "remove", ariaLabel: "Close search bar", onClick: toggleSearch }) }))));
|
|
833
837
|
function getPlaceholder() {
|
|
@@ -1054,6 +1058,9 @@ DataList.EmptyState = DataListEmptyState;
|
|
|
1054
1058
|
DataList.Filters = DataListFilters;
|
|
1055
1059
|
/**
|
|
1056
1060
|
* Enables the search functionality of the DataList.
|
|
1061
|
+
*
|
|
1062
|
+
* Since the debounce is implemented within the component, it can only be an
|
|
1063
|
+
* uncontrolled component. Thus the lack of a `value` prop.
|
|
1057
1064
|
*/
|
|
1058
1065
|
DataList.Search = DataListSearch;
|
|
1059
1066
|
/**
|
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
|
}
|