@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 ComboboxContentProps {
4
+ interface ComboboxCloseProps {
4
5
  /**
5
- * List of selectable options to display.
6
+ * Callback function invoked upon the selection of an option. Provides the selected option(s) as an argument.
6
7
  */
7
- readonly options: ComboboxOption[];
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
- * Callback function invoked upon the selection of an option. Provides the selected option as an argument.
19
+ * List of selectable options to display.
10
20
  */
11
- readonly onSelect: (selection: ComboboxOption) => void;
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 | null;
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 | null;
6
+ selected: ComboboxOption[];
7
7
  optionsListRef: React.RefObject<HTMLUListElement>;
8
- setSelectedElement: React.Dispatch<SetStateAction<HTMLElement | null>>;
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
- export declare function useComboboxContent(options: ComboboxOption[], open: boolean): {
3
+ interface useComboboxContent {
4
4
  searchValue: string;
5
5
  setSearchValue: React.Dispatch<React.SetStateAction<string>>;
6
- setSelectedElement: React.Dispatch<React.SetStateAction<HTMLElement | null>>;
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 {};
@@ -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
- var _a;
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.setSelectedElement(listItem);
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 [selectedElement, setSelectedElement] = React.useState(null);
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 && selectedElement) {
114
- selectedElement === null || selectedElement === void 0 ? void 0 : selectedElement.scrollIntoView({
122
+ if (open && firstSelectedElement) {
123
+ firstSelectedElement === null || firstSelectedElement === void 0 ? void 0 : firstSelectedElement.scrollIntoView({
115
124
  block: "nearest",
116
125
  });
117
126
  }
118
- }, [open, selectedElement]);
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
- setSelectedElement,
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, setSelectedElement, filteredOptions, optionsListRef, } = useComboboxContent(props.options, open);
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: props.selected, optionsListRef: optionsListRef, setSelectedElement: setSelectedElement, selectionHandler: handleSelection, searchValue: searchValue, subjectNoun: props.subjectNoun }),
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(selection);
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.46.5",
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": "d7aa0a27fea6da695c890f558b1f468593dddd56"
87
+ "gitHead": "8d8bf2b50b1fa2e310959fdcc803e2601b73e15c"
88
88
  }