@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.
@@ -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),
@@ -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
- readonly onSearch: (search: string) => void;
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;
@@ -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, { ref: inputRef, placeholder: getPlaceholder(), onChange: debouncedSearch, prefix: { icon: "search" }, size: "small" })),
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.46.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": "49be7868b390cc604c4b09bb948ef0d69061d895"
87
+ "gitHead": "8d8bf2b50b1fa2e310959fdcc803e2601b73e15c"
88
88
  }