@vuu-ui/vuu-ui-controls 2.2.0 → 2.3.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/cjs/index.js CHANGED
@@ -27,6 +27,7 @@ var Icon = require('./icon-button/Icon.js');
27
27
  var IconButton = require('./icon-button/IconButton.js');
28
28
  var ToggleIconButton = require('./icon-button/ToggleIconButton.js');
29
29
  var TablePicker = require('./instrument-picker/TablePicker.js');
30
+ var ItemPicker = require('./item-picker/ItemPicker.js');
30
31
  var MeasuredContainer = require('./measured-container/MeasuredContainer.js');
31
32
  var useMeasuredContainer = require('./measured-container/useMeasuredContainer.js');
32
33
  var ModalProvider = require('./modal-provider/ModalProvider.js');
@@ -120,6 +121,7 @@ exports.Icon = Icon.Icon;
120
121
  exports.IconButton = IconButton.IconButton;
121
122
  exports.ToggleIconButton = ToggleIconButton.ToggleIconButton;
122
123
  exports.TablePicker = TablePicker.TablePicker;
124
+ exports.ItemPicker = ItemPicker.ItemPicker;
123
125
  exports.MeasuredContainer = MeasuredContainer.MeasuredContainer;
124
126
  exports.reduceSizeHeight = useMeasuredContainer.reduceSizeHeight;
125
127
  exports.useMeasuredContainer = useMeasuredContainer.useMeasuredContainer;
package/cjs/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -0,0 +1,6 @@
1
+ 'use strict';
2
+
3
+ var itemPickerCss = ".vuuItemPicker {\n --label-margin-left: 0px;\n\n --itempicker-background: var(\n --vuuItemPicker-background,\n var(--salt-container-primary-background)\n );\n background: var(--itempicker-background);\n container-type: inline-size;\n display: flex;\n flex-direction: column;\n gap: var(--salt-spacing-300);\n height: var(--vuuItemPicker-height, 100%);\n overflow: hidden;\n padding: var(--salt-spacing-300) 0;\n\n &.vuuItemPicker-withCalculated {\n --label-margin-left: 8px;\n }\n\n .saltListBox {\n background-color: inherit;\n }\n\n .saltOption {\n align-items: center;\n background-color: inherit;\n border-bottom: var(--vuuItemListItem-border, solid) 1px\n var(--salt-separable-tertiary-borderColor);\n gap: var(--salt-spacing-200);\n height: calc(var(--salt-size-base) + var(--salt-spacing-200));\n .vuuIcon {\n --vuu-icon-color: var(--salt-content-secondary-foreground);\n }\n\n .vuuCalculatedItemIcon {\n --vuu-icon-color: var(--salt-content-secondary-foreground);\n left: 32px;\n position: absolute;\n top: 8px;\n }\n\n .vuuHighlight {\n color: var(--vuu-color-blue-40);\n text-decoration: underline;\n }\n\n .vuuItemPicker-text {\n margin-left: var(--label-margin-left);\n }\n }\n\n .vuuItemPickerListItem-action {\n margin-left: auto;\n visibility: hidden;\n }\n\n .saltOption:hover {\n .vuuItemPickerListItem-action {\n visibility: visible;\n }\n }\n\n .vuuItemPicker-search {\n --saltInput-paddingLeft: var(--salt-spacing-300);\n padding: var(--salt-spacing-200) var(--salt-spacing-300) 0\n var(--salt-spacing-300);\n }\n\n .vuuItemPicker-scrollContainer {\n flex: 1 1 0px;\n overflow: auto;\n\n .vuuItemPicker-sectionHeader {\n align-items: center;\n background: var(--itempicker-background);\n display: flex;\n height: 32px;\n padding: 0 var(--salt-spacing-300);\n position: sticky;\n top: 0;\n z-index: 1;\n }\n\n .vuuItemPicker-availableHeader {\n top: 32px;\n }\n\n .vuuItemPicker-availableList {\n .saltOption {\n padding-left: var(--salt-spacing-300);\n }\n }\n }\n}\n";
4
+
5
+ module.exports = itemPickerCss;
6
+ //# sourceMappingURL=ItemPicker.css.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ItemPicker.css.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;"}
@@ -0,0 +1,261 @@
1
+ 'use strict';
2
+
3
+ var jsxRuntime = require('react/jsx-runtime');
4
+ var core = require('@salt-ds/core');
5
+ var styles = require('@salt-ds/styles');
6
+ var window = require('@salt-ds/window');
7
+ var vuuTable = require('@vuu-ui/vuu-table');
8
+ var vuuUtils = require('@vuu-ui/vuu-utils');
9
+ var cx = require('clsx');
10
+ var React = require('react');
11
+ var Icon = require('../icon-button/Icon.js');
12
+ var IconButton = require('../icon-button/IconButton.js');
13
+ var useItemPicker = require('./useItemPicker.js');
14
+ var ItemPicker$1 = require('./ItemPicker.css.js');
15
+
16
+ const classBase = "vuuItemPicker";
17
+ const classBaseListItem = "vuuItemPickerListItem";
18
+ const searchIcon = /* @__PURE__ */ jsxRuntime.jsx(Icon.Icon, { name: "search" });
19
+ const NO_SELECTION = [];
20
+ const useSorting = (id, index) => {
21
+ const { handleRef: sortableHandleRef, ref: sortableRef } = vuuUtils.useSortable({
22
+ id,
23
+ index
24
+ });
25
+ const [handleRef, ref] = React.useMemo(() => {
26
+ return [sortableHandleRef, sortableRef];
27
+ }, [sortableHandleRef, sortableRef]);
28
+ return {
29
+ handleRef,
30
+ ref
31
+ };
32
+ };
33
+ const SelectedListItem = ({
34
+ className: classNameProp,
35
+ index,
36
+ item,
37
+ onRemove,
38
+ searchPattern = "",
39
+ ...optionProps
40
+ }) => {
41
+ const { handleRef, ref } = useSorting(item.name, index);
42
+ const value = useItemPicker.getItemLabel(item);
43
+ const valueWithHighlighting = vuuTable.applyHighlighting(value, searchPattern);
44
+ const handleRemoveButtonClick = React.useCallback(
45
+ (e) => {
46
+ e.stopPropagation();
47
+ onRemove?.(e);
48
+ },
49
+ [onRemove]
50
+ );
51
+ return /* @__PURE__ */ jsxRuntime.jsxs(
52
+ core.Option,
53
+ {
54
+ ...optionProps,
55
+ className: cx(classNameProp, classBaseListItem),
56
+ "data-name": item.name,
57
+ ref,
58
+ children: [
59
+ /* @__PURE__ */ jsxRuntime.jsx(
60
+ IconButton.IconButton,
61
+ {
62
+ "data-embedded": true,
63
+ appearance: "transparent",
64
+ icon: "draggable",
65
+ ref: handleRef,
66
+ size: 16
67
+ }
68
+ ),
69
+ item.icon ? /* @__PURE__ */ jsxRuntime.jsx(Icon.Icon, { name: item.icon }, "icon") : null,
70
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: `${classBase}-text`, children: valueWithHighlighting }),
71
+ /* @__PURE__ */ jsxRuntime.jsx(
72
+ IconButton.IconButton,
73
+ {
74
+ className: `${classBaseListItem}-action`,
75
+ "data-embedded": true,
76
+ appearance: "transparent",
77
+ icon: "cross",
78
+ onClick: handleRemoveButtonClick,
79
+ size: 16
80
+ }
81
+ )
82
+ ]
83
+ }
84
+ );
85
+ };
86
+ const AvailableListItem = ({
87
+ className: classNameProp,
88
+ index,
89
+ item,
90
+ onAdd,
91
+ searchPattern = "",
92
+ disabled,
93
+ ...optionProps
94
+ }) => {
95
+ const value = useItemPicker.getItemLabel(item);
96
+ const valueWithHighlighting = vuuTable.applyHighlighting(value, searchPattern);
97
+ return /* @__PURE__ */ jsxRuntime.jsxs(
98
+ core.Option,
99
+ {
100
+ ...optionProps,
101
+ className: cx(classNameProp, classBaseListItem),
102
+ "data-name": item.name,
103
+ disabled,
104
+ children: [
105
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: `${classBase}-text`, children: valueWithHighlighting }),
106
+ /* @__PURE__ */ jsxRuntime.jsx(
107
+ IconButton.IconButton,
108
+ {
109
+ className: `${classBaseListItem}-action`,
110
+ "data-embedded": true,
111
+ appearance: "transparent",
112
+ icon: "plus",
113
+ onClick: onAdd,
114
+ size: 16,
115
+ disabled
116
+ }
117
+ )
118
+ ]
119
+ }
120
+ );
121
+ };
122
+ const ItemPicker = React.forwardRef(function ItemPicker2({
123
+ className,
124
+ itemTypeName,
125
+ allItems,
126
+ selectedItems,
127
+ maxSelections,
128
+ onSelectedItemsChange,
129
+ onSelectionChange,
130
+ selected = NO_SELECTION,
131
+ createCustomItemProps,
132
+ ...htmlAttributes
133
+ }, forwardedRef) {
134
+ const targetWindow = window.useWindow();
135
+ styles.useComponentCssInjection({
136
+ testId: "vuu-item-picker",
137
+ css: ItemPicker$1,
138
+ window: targetWindow
139
+ });
140
+ const searchCallbackRef = React.useCallback((el) => {
141
+ setTimeout(() => {
142
+ el?.querySelector("input")?.focus();
143
+ }, 100);
144
+ }, []);
145
+ const {
146
+ selectedItemsCount,
147
+ availableItemsCount,
148
+ selectedItemsFiltered,
149
+ availableItemsFiltered,
150
+ searchText,
151
+ onChangeSearchInput,
152
+ onAddItemToSelectedList,
153
+ onRemoveItemFromSelectedList,
154
+ onReorderSelectedItems
155
+ } = useItemPicker.useItemPicker({
156
+ allItems,
157
+ selectedItems,
158
+ onSelectedItemsChange,
159
+ maxSelections
160
+ });
161
+ const listRef = React.useRef(null);
162
+ const getOptionName = (option) => {
163
+ if (option) {
164
+ const { name } = option.dataset;
165
+ if (name) {
166
+ return name;
167
+ }
168
+ }
169
+ throw Error("[ItemPicker] list option has no data-name");
170
+ };
171
+ const handleDragEnd = React.useCallback(() => {
172
+ setTimeout(() => {
173
+ const listItems = listRef.current?.querySelectorAll(".saltOption");
174
+ if (listItems) {
175
+ const orderedItemNames = Array.from(listItems).map(getOptionName);
176
+ onReorderSelectedItems(orderedItemNames);
177
+ }
178
+ }, 300);
179
+ }, [availableItemsFiltered, selectedItemsFiltered]);
180
+ const searchPlaceholderText = `Find ${vuuUtils.singularForm(itemTypeName)}`;
181
+ const maxSelectionsSubHeading = maxSelections ? `(${maxSelections} max)` : "";
182
+ const selectedItemsHeading = `${selectedItemsCount} ${selectedItemsCount === 1 ? vuuUtils.singularForm(itemTypeName) : vuuUtils.pluralForm(itemTypeName)} in view ${maxSelectionsSubHeading}`;
183
+ const availableItemsHeading = `${availableItemsCount} available ${availableItemsCount === 1 ? vuuUtils.singularForm(itemTypeName) : vuuUtils.pluralForm(itemTypeName)}`;
184
+ return /* @__PURE__ */ jsxRuntime.jsxs(
185
+ "div",
186
+ {
187
+ ...htmlAttributes,
188
+ className: cx(classBase, className),
189
+ ref: forwardedRef,
190
+ children: [
191
+ /* @__PURE__ */ jsxRuntime.jsx("form", { className: `${classBase}-search`, role: "search", children: /* @__PURE__ */ jsxRuntime.jsx(
192
+ core.Input,
193
+ {
194
+ startAdornment: searchIcon,
195
+ placeholder: searchPlaceholderText,
196
+ ref: searchCallbackRef,
197
+ value: searchText,
198
+ onChange: onChangeSearchInput
199
+ }
200
+ ) }),
201
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `${classBase}-scrollContainer vuuScrollable`, children: [
202
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: `${classBase}-sectionHeader`, children: selectedItemsHeading }),
203
+ /* @__PURE__ */ jsxRuntime.jsx(vuuUtils.DragDropProvider, { onDragEnd: handleDragEnd, children: /* @__PURE__ */ jsxRuntime.jsx(
204
+ core.ListBox,
205
+ {
206
+ className: `${classBase}-selectedList`,
207
+ onSelectionChange,
208
+ ref: listRef,
209
+ selected,
210
+ children: selectedItemsFiltered.map((item, index) => /* @__PURE__ */ jsxRuntime.jsx(
211
+ SelectedListItem,
212
+ {
213
+ item,
214
+ index,
215
+ onRemove: onRemoveItemFromSelectedList,
216
+ searchPattern: searchText.toLowerCase(),
217
+ value: item
218
+ },
219
+ item.name
220
+ ))
221
+ }
222
+ ) }),
223
+ /* @__PURE__ */ jsxRuntime.jsx(
224
+ "div",
225
+ {
226
+ className: cx(
227
+ `${classBase}-sectionHeader`,
228
+ `${classBase}-availableHeader`
229
+ ),
230
+ children: availableItemsHeading
231
+ }
232
+ ),
233
+ /* @__PURE__ */ jsxRuntime.jsx(
234
+ core.ListBox,
235
+ {
236
+ className: `${classBase}-availableList`,
237
+ selected: NO_SELECTION,
238
+ children: availableItemsFiltered.map((item, index) => /* @__PURE__ */ jsxRuntime.jsx(
239
+ AvailableListItem,
240
+ {
241
+ item,
242
+ index,
243
+ onAdd: onAddItemToSelectedList,
244
+ searchPattern: searchText.toLowerCase(),
245
+ value: item,
246
+ disabled: selectedItemsCount === maxSelections
247
+ },
248
+ item.name
249
+ ))
250
+ }
251
+ )
252
+ ] }),
253
+ createCustomItemProps ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: `${classBase}-item-buttons`, children: /* @__PURE__ */ jsxRuntime.jsx(core.Button, { onClick: createCustomItemProps.onClickCreateCustomItem, children: createCustomItemProps.buttonLabel }) }) : null
254
+ ]
255
+ }
256
+ );
257
+ });
258
+
259
+ exports.ItemPicker = ItemPicker;
260
+ exports.classBaseListItem = classBaseListItem;
261
+ //# sourceMappingURL=ItemPicker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ItemPicker.js","sources":["../../../../packages/vuu-ui-controls/src/item-picker/ItemPicker.tsx"],"sourcesContent":["import {\n Button,\n Input,\n ListBox,\n ListBoxProps,\n Option,\n OptionProps,\n} from \"@salt-ds/core\";\nimport { useComponentCssInjection } from \"@salt-ds/styles\";\nimport { useWindow } from \"@salt-ds/window\";\nimport { applyHighlighting } from \"@vuu-ui/vuu-table\";\nimport {\n DragDropProvider,\n ItemTypeName,\n pluralForm,\n singularForm,\n useSortable,\n} from \"@vuu-ui/vuu-utils\";\nimport cx from \"clsx\";\nimport {\n ForwardedRef,\n forwardRef,\n HTMLAttributes,\n MouseEventHandler,\n RefCallback,\n useCallback,\n useMemo,\n useRef,\n} from \"react\";\nimport { Icon, IconButton } from \"../icon-button\";\nimport {\n CreateCustomItemProps,\n getItemLabel,\n ItemDescriptor,\n ItemPickerHookProps,\n useItemPicker,\n} from \"./useItemPicker\";\n\nimport itemPickerCss from \"./ItemPicker.css\";\n\nconst classBase = \"vuuItemPicker\";\n\nexport const classBaseListItem = \"vuuItemPickerListItem\";\n\nexport interface ItemPickerProps\n extends ItemPickerHookProps,\n HTMLAttributes<HTMLDivElement>,\n Pick<ListBoxProps<ItemDescriptor>, \"selected\" | \"onSelectionChange\"> {\n itemTypeName: ItemTypeName;\n createCustomItemProps?: CreateCustomItemProps;\n}\n\nconst searchIcon = <Icon name=\"search\" />;\nconst NO_SELECTION: ItemDescriptor[] = [] as const;\n\nconst useSorting = (id: string, index: number) => {\n const { handleRef: sortableHandleRef, ref: sortableRef } = useSortable({\n id,\n index,\n });\n\n const [handleRef, ref] = useMemo(() => {\n return [sortableHandleRef, sortableRef];\n }, [sortableHandleRef, sortableRef]);\n\n return {\n handleRef,\n ref,\n };\n};\n\nconst SelectedListItem = ({\n className: classNameProp,\n index,\n item,\n onRemove,\n searchPattern = \"\",\n ...optionProps\n}: OptionProps & {\n index: number;\n item: ItemDescriptor;\n onRemove: MouseEventHandler<HTMLButtonElement>;\n searchPattern?: Lowercase<string>;\n}) => {\n const { handleRef, ref } = useSorting(item.name, index);\n const value = getItemLabel(item as ItemDescriptor);\n const valueWithHighlighting = applyHighlighting(value, searchPattern);\n\n const handleRemoveButtonClick = useCallback<\n MouseEventHandler<HTMLButtonElement>\n >(\n (e) => {\n e.stopPropagation();\n onRemove?.(e);\n },\n [onRemove],\n );\n\n return (\n <Option\n {...optionProps}\n className={cx(classNameProp, classBaseListItem)}\n data-name={item.name}\n ref={ref}\n >\n <IconButton\n data-embedded\n appearance=\"transparent\"\n icon=\"draggable\"\n ref={handleRef}\n size={16}\n />\n {item.icon ? <Icon name={item.icon} key=\"icon\" /> : null}\n <span className={`${classBase}-text`}>{valueWithHighlighting}</span>\n <IconButton\n className={`${classBaseListItem}-action`}\n data-embedded\n appearance=\"transparent\"\n icon=\"cross\"\n onClick={handleRemoveButtonClick}\n size={16}\n />\n </Option>\n );\n};\n\nconst AvailableListItem = ({\n className: classNameProp,\n index,\n item,\n onAdd,\n searchPattern = \"\",\n disabled,\n ...optionProps\n}: OptionProps & {\n index: number;\n item: ItemDescriptor;\n onAdd: MouseEventHandler<HTMLButtonElement>;\n searchPattern?: Lowercase<string>;\n}) => {\n const value = getItemLabel(item as ItemDescriptor);\n const valueWithHighlighting = applyHighlighting(value, searchPattern);\n\n return (\n <Option\n {...optionProps}\n className={cx(classNameProp, classBaseListItem)}\n data-name={item.name}\n disabled={disabled}\n >\n <span className={`${classBase}-text`}>{valueWithHighlighting}</span>\n <IconButton\n className={`${classBaseListItem}-action`}\n data-embedded\n appearance=\"transparent\"\n icon=\"plus\"\n onClick={onAdd}\n size={16}\n disabled={disabled}\n />\n </Option>\n );\n};\n\n/** Generic controlled component that displays items in a 'picker' that are available to search, select, reorder or deselect.\n * As input props, all available items, currently selected items, search text and callback functions for changes in selected items and search text\n * must be supplied by the client.\n */\nexport const ItemPicker = forwardRef(function ItemPicker(\n {\n className,\n itemTypeName,\n allItems,\n selectedItems,\n maxSelections,\n onSelectedItemsChange,\n onSelectionChange,\n selected = NO_SELECTION,\n createCustomItemProps,\n ...htmlAttributes\n }: ItemPickerProps,\n forwardedRef: ForwardedRef<HTMLDivElement>,\n) {\n const targetWindow = useWindow();\n useComponentCssInjection({\n testId: \"vuu-item-picker\",\n css: itemPickerCss,\n window: targetWindow,\n });\n\n const searchCallbackRef = useCallback<RefCallback<HTMLElement>>((el) => {\n setTimeout(() => {\n el?.querySelector(\"input\")?.focus();\n }, 100);\n }, []);\n\n const {\n selectedItemsCount,\n availableItemsCount,\n selectedItemsFiltered,\n availableItemsFiltered,\n searchText,\n onChangeSearchInput,\n onAddItemToSelectedList,\n onRemoveItemFromSelectedList,\n onReorderSelectedItems,\n } = useItemPicker({\n allItems,\n selectedItems,\n onSelectedItemsChange,\n maxSelections,\n });\n\n const listRef = useRef<HTMLDivElement>(null);\n\n const getOptionName = (option?: HTMLElement) => {\n if (option) {\n const { name } = option.dataset;\n if (name) {\n return name;\n }\n }\n throw Error(\"[ItemPicker] list option has no data-name\");\n };\n\n const handleDragEnd = useCallback(() => {\n setTimeout(() => {\n const listItems =\n listRef.current?.querySelectorAll<HTMLDivElement>(\".saltOption\");\n if (listItems) {\n const orderedItemNames = Array.from(listItems).map(getOptionName);\n onReorderSelectedItems(orderedItemNames);\n }\n }, 300);\n }, [availableItemsFiltered, selectedItemsFiltered]);\n\n const searchPlaceholderText = `Find ${singularForm(itemTypeName)}`;\n const maxSelectionsSubHeading = maxSelections ? `(${maxSelections} max)` : \"\";\n const selectedItemsHeading = `${selectedItemsCount} ${selectedItemsCount === 1 ? singularForm(itemTypeName) : pluralForm(itemTypeName)} in view ${maxSelectionsSubHeading}`;\n const availableItemsHeading = `${availableItemsCount} available ${availableItemsCount === 1 ? singularForm(itemTypeName) : pluralForm(itemTypeName)}`;\n\n return (\n <div\n {...htmlAttributes}\n className={cx(classBase, className)}\n ref={forwardedRef}\n >\n <form className={`${classBase}-search`} role=\"search\">\n <Input\n startAdornment={searchIcon}\n placeholder={searchPlaceholderText}\n ref={searchCallbackRef}\n value={searchText}\n onChange={onChangeSearchInput}\n />\n </form>\n\n <div className={`${classBase}-scrollContainer vuuScrollable`}>\n <div className={`${classBase}-sectionHeader`}>\n {selectedItemsHeading}\n </div>\n <DragDropProvider onDragEnd={handleDragEnd}>\n <ListBox\n className={`${classBase}-selectedList`}\n onSelectionChange={onSelectionChange}\n ref={listRef}\n selected={selected}\n >\n {selectedItemsFiltered.map((item, index) => (\n <SelectedListItem\n item={item}\n index={index}\n key={item.name}\n onRemove={onRemoveItemFromSelectedList}\n searchPattern={searchText.toLowerCase() as Lowercase<string>}\n value={item}\n />\n ))}\n </ListBox>\n </DragDropProvider>\n\n <div\n className={cx(\n `${classBase}-sectionHeader`,\n `${classBase}-availableHeader`,\n )}\n >\n {availableItemsHeading}\n </div>\n <ListBox\n className={`${classBase}-availableList`}\n selected={NO_SELECTION}\n >\n {availableItemsFiltered.map((item, index) => (\n <AvailableListItem\n item={item}\n index={index}\n key={item.name}\n onAdd={onAddItemToSelectedList}\n searchPattern={searchText.toLowerCase() as Lowercase<string>}\n value={item}\n disabled={selectedItemsCount === maxSelections}\n />\n ))}\n </ListBox>\n </div>\n {createCustomItemProps ? (\n <div className={`${classBase}-item-buttons`}>\n <Button onClick={createCustomItemProps.onClickCreateCustomItem}>\n {createCustomItemProps.buttonLabel}\n </Button>\n </div>\n ) : null}\n </div>\n );\n});\n"],"names":["jsx","Icon","useSortable","useMemo","getItemLabel","applyHighlighting","useCallback","jsxs","Option","IconButton","forwardRef","ItemPicker","useWindow","useComponentCssInjection","itemPickerCss","useItemPicker","useRef","singularForm","pluralForm","Input","DragDropProvider","ListBox","Button"],"mappings":"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"}
@@ -0,0 +1,118 @@
1
+ 'use strict';
2
+
3
+ var vuuUtils = require('@vuu-ui/vuu-utils');
4
+ var React = require('react');
5
+
6
+ const filterItems = (items, pattern) => {
7
+ if (pattern) {
8
+ const lowerCasePattern = pattern.toLowerCase();
9
+ return items.filter(
10
+ (item) => getItemLabel(item).toLowerCase().indexOf(lowerCasePattern) !== -1
11
+ );
12
+ } else {
13
+ return items;
14
+ }
15
+ };
16
+ const getItemLabel = (item) => {
17
+ if (item.label) return item.label;
18
+ else return item.name;
19
+ };
20
+ const itemName = (target) => {
21
+ const listItem = vuuUtils.queryClosest(target, ".saltOption", true);
22
+ const { name } = listItem.dataset;
23
+ if (name) {
24
+ return name;
25
+ } else {
26
+ throw Error(
27
+ "[useItemPicker] item name could not be identified, data-name attribute not found"
28
+ );
29
+ }
30
+ };
31
+ const byItemName = ({ name: n1 }, { name: n2 }) => n1 > n2 ? 1 : n2 > n1 ? -1 : 0;
32
+ const useItemPicker = ({
33
+ allItems,
34
+ selectedItems,
35
+ maxSelections,
36
+ onSelectedItemsChange
37
+ }) => {
38
+ if (maxSelections && selectedItems.length > maxSelections) {
39
+ throw Error(
40
+ `[useItemPicker] max selections ${maxSelections} exceeded by selected items (that has count ${selectedItems.length})`
41
+ );
42
+ }
43
+ const [searchPattern, setSearchPattern] = React.useState("");
44
+ const handleChangeSearchInput = React.useCallback((evt) => {
45
+ const { value } = evt.target;
46
+ setSearchPattern(value);
47
+ }, []);
48
+ const handleAddItemToSelectedList = React.useCallback(
49
+ (e) => {
50
+ const name = itemName(e.target);
51
+ const itemToAdd = allItems.find((item) => item.name === name);
52
+ if (itemToAdd) {
53
+ const newSelectedItems = selectedItems.concat(itemToAdd);
54
+ onSelectedItemsChange(newSelectedItems);
55
+ } else {
56
+ throw Error(
57
+ `[useItemPicker] handleAddItemToSelectedList, item '${name}' not found`
58
+ );
59
+ }
60
+ },
61
+ [allItems, selectedItems, onSelectedItemsChange]
62
+ );
63
+ const handleRemoveItemFromSelectedList = React.useCallback(
64
+ (e) => {
65
+ const name = itemName(e.target);
66
+ const itemToRemove = selectedItems.find((item) => item.name === name);
67
+ if (itemToRemove) {
68
+ const newSelectedItems = selectedItems.filter(
69
+ (item) => item.name !== name
70
+ );
71
+ onSelectedItemsChange(newSelectedItems);
72
+ } else {
73
+ throw Error(
74
+ `[useItemPicker] handleRemoveItemFromSelectedList, item '${name}' not found`
75
+ );
76
+ }
77
+ },
78
+ [selectedItems, onSelectedItemsChange]
79
+ );
80
+ const handleReorderSelectedItems = React.useCallback(
81
+ (orderedItemNames) => {
82
+ const reorderedSelectedItems = vuuUtils.reorderItems(
83
+ selectedItems,
84
+ orderedItemNames
85
+ );
86
+ onSelectedItemsChange(reorderedSelectedItems);
87
+ },
88
+ [selectedItems, onSelectedItemsChange]
89
+ );
90
+ const getSelectedItemsFiltered = React.useMemo(() => {
91
+ return filterItems(selectedItems, searchPattern);
92
+ }, [selectedItems, searchPattern]);
93
+ const getAvailableItemsFiltered = React.useMemo(() => {
94
+ return filterItems(allItems, searchPattern).filter(
95
+ ({ name }) => selectedItems.findIndex((item) => item.name === name) === -1
96
+ ).toSorted(byItemName);
97
+ }, [allItems, selectedItems, searchPattern]);
98
+ const getAvailableItemsCount = React.useMemo(() => {
99
+ return allItems.filter(
100
+ ({ name }) => selectedItems.findIndex((item) => item.name === name) === -1
101
+ ).length;
102
+ }, [allItems, selectedItems]);
103
+ return {
104
+ selectedItemsCount: selectedItems.length,
105
+ availableItemsCount: getAvailableItemsCount,
106
+ selectedItemsFiltered: getSelectedItemsFiltered,
107
+ availableItemsFiltered: getAvailableItemsFiltered,
108
+ searchText: searchPattern,
109
+ onChangeSearchInput: handleChangeSearchInput,
110
+ onAddItemToSelectedList: handleAddItemToSelectedList,
111
+ onRemoveItemFromSelectedList: handleRemoveItemFromSelectedList,
112
+ onReorderSelectedItems: handleReorderSelectedItems
113
+ };
114
+ };
115
+
116
+ exports.getItemLabel = getItemLabel;
117
+ exports.useItemPicker = useItemPicker;
118
+ //# sourceMappingURL=useItemPicker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useItemPicker.js","sources":["../../../../packages/vuu-ui-controls/src/item-picker/useItemPicker.tsx"],"sourcesContent":["import { queryClosest, reorderItems } from \"@vuu-ui/vuu-utils\";\nimport {\n FormEventHandler,\n MouseEventHandler,\n useCallback,\n useMemo,\n useState,\n} from \"react\";\n\n/** This is a public description of an Item that can be displayed in the ItemPicker component, defining all the\n * mandatory and option attributes that can be defined by the client. */\nexport interface ItemDescriptor {\n /** Unique name for this data value */\n name: string;\n /** Optional label for display instead of the name */\n label?: string;\n /** Optional data-icon name for display of an icon to the left of the item label/name in the selected list */\n icon?: string;\n /** Optional string to define groups of items for client reference, not currently used for rendering */\n group?: string;\n}\n\nexport interface CreateCustomItemProps {\n buttonLabel: string;\n onClickCreateCustomItem: MouseEventHandler<HTMLButtonElement>;\n}\n\nexport interface ItemPickerHookProps {\n allItems: ItemDescriptor[];\n selectedItems: ItemDescriptor[];\n maxSelections?: number;\n onSelectedItemsChange: (newSelectedItems: ItemDescriptor[]) => void;\n}\n\nconst filterItems = (\n items: readonly ItemDescriptor[],\n pattern: string,\n): readonly ItemDescriptor[] => {\n if (pattern) {\n const lowerCasePattern = pattern.toLowerCase();\n return items.filter(\n (item) =>\n getItemLabel(item).toLowerCase().indexOf(lowerCasePattern) !== -1,\n );\n } else {\n return items;\n }\n};\n\nexport const getItemLabel = (item: ItemDescriptor) => {\n if (item.label) return item.label;\n else return item.name;\n};\n\nconst itemName = (target: EventTarget): string => {\n const listItem = queryClosest(target, \".saltOption\", true);\n const { name } = listItem.dataset;\n if (name) {\n return name;\n } else {\n throw Error(\n \"[useItemPicker] item name could not be identified, data-name attribute not found\",\n );\n }\n};\n\nconst byItemName = (\n { name: n1 }: ItemDescriptor,\n { name: n2 }: ItemDescriptor,\n) => (n1 > n2 ? 1 : n2 > n1 ? -1 : 0);\n\nexport const useItemPicker = ({\n allItems,\n selectedItems,\n maxSelections,\n onSelectedItemsChange,\n}: ItemPickerHookProps) => {\n if (maxSelections && selectedItems.length > maxSelections) {\n throw Error(\n `[useItemPicker] max selections ${maxSelections} exceeded by selected items (that has count ${selectedItems.length})`,\n );\n }\n\n const [searchPattern, setSearchPattern] = useState(\"\");\n\n const handleChangeSearchInput = useCallback<FormEventHandler>((evt) => {\n const { value } = evt.target as HTMLInputElement;\n setSearchPattern(value);\n }, []);\n\n const handleAddItemToSelectedList = useCallback<\n MouseEventHandler<HTMLButtonElement>\n >(\n (e) => {\n const name = itemName(e.target);\n const itemToAdd = allItems.find((item) => item.name === name);\n if (itemToAdd) {\n const newSelectedItems = selectedItems.concat(itemToAdd);\n onSelectedItemsChange(newSelectedItems);\n } else {\n throw Error(\n `[useItemPicker] handleAddItemToSelectedList, item '${name}' not found`,\n );\n }\n },\n [allItems, selectedItems, onSelectedItemsChange],\n );\n\n const handleRemoveItemFromSelectedList = useCallback<\n MouseEventHandler<HTMLButtonElement>\n >(\n (e) => {\n const name = itemName(e.target);\n const itemToRemove = selectedItems.find((item) => item.name === name);\n if (itemToRemove) {\n const newSelectedItems = selectedItems.filter(\n (item) => item.name !== name,\n );\n onSelectedItemsChange(newSelectedItems);\n } else {\n throw Error(\n `[useItemPicker] handleRemoveItemFromSelectedList, item '${name}' not found`,\n );\n }\n },\n [selectedItems, onSelectedItemsChange],\n );\n\n const handleReorderSelectedItems = useCallback(\n (orderedItemNames: string[]) => {\n const reorderedSelectedItems: ItemDescriptor[] = reorderItems(\n selectedItems,\n orderedItemNames,\n );\n onSelectedItemsChange(reorderedSelectedItems);\n },\n [selectedItems, onSelectedItemsChange],\n );\n\n const getSelectedItemsFiltered = useMemo(() => {\n return filterItems(selectedItems, searchPattern);\n }, [selectedItems, searchPattern]);\n\n const getAvailableItemsFiltered = useMemo(() => {\n return filterItems(allItems, searchPattern)\n .filter(\n ({ name }) =>\n selectedItems.findIndex((item) => item.name === name) === -1,\n )\n .toSorted(byItemName);\n }, [allItems, selectedItems, searchPattern]);\n\n const getAvailableItemsCount = useMemo(() => {\n return allItems.filter(\n ({ name }) =>\n selectedItems.findIndex((item) => item.name === name) === -1,\n ).length;\n }, [allItems, selectedItems]);\n\n return {\n selectedItemsCount: selectedItems.length,\n availableItemsCount: getAvailableItemsCount,\n selectedItemsFiltered: getSelectedItemsFiltered,\n availableItemsFiltered: getAvailableItemsFiltered,\n searchText: searchPattern,\n onChangeSearchInput: handleChangeSearchInput,\n onAddItemToSelectedList: handleAddItemToSelectedList,\n onRemoveItemFromSelectedList: handleRemoveItemFromSelectedList,\n onReorderSelectedItems: handleReorderSelectedItems,\n };\n};\n"],"names":["queryClosest","useState","useCallback","reorderItems","useMemo"],"mappings":"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"}
package/esm/index.js CHANGED
@@ -25,6 +25,7 @@ export { Icon } from './icon-button/Icon.js';
25
25
  export { IconButton } from './icon-button/IconButton.js';
26
26
  export { ToggleIconButton } from './icon-button/ToggleIconButton.js';
27
27
  export { TablePicker } from './instrument-picker/TablePicker.js';
28
+ export { ItemPicker } from './item-picker/ItemPicker.js';
28
29
  export { MeasuredContainer } from './measured-container/MeasuredContainer.js';
29
30
  export { reduceSizeHeight, useMeasuredContainer } from './measured-container/useMeasuredContainer.js';
30
31
  export { ModalProvider, useModal } from './modal-provider/ModalProvider.js';
package/esm/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -0,0 +1,4 @@
1
+ var itemPickerCss = ".vuuItemPicker {\n --label-margin-left: 0px;\n\n --itempicker-background: var(\n --vuuItemPicker-background,\n var(--salt-container-primary-background)\n );\n background: var(--itempicker-background);\n container-type: inline-size;\n display: flex;\n flex-direction: column;\n gap: var(--salt-spacing-300);\n height: var(--vuuItemPicker-height, 100%);\n overflow: hidden;\n padding: var(--salt-spacing-300) 0;\n\n &.vuuItemPicker-withCalculated {\n --label-margin-left: 8px;\n }\n\n .saltListBox {\n background-color: inherit;\n }\n\n .saltOption {\n align-items: center;\n background-color: inherit;\n border-bottom: var(--vuuItemListItem-border, solid) 1px\n var(--salt-separable-tertiary-borderColor);\n gap: var(--salt-spacing-200);\n height: calc(var(--salt-size-base) + var(--salt-spacing-200));\n .vuuIcon {\n --vuu-icon-color: var(--salt-content-secondary-foreground);\n }\n\n .vuuCalculatedItemIcon {\n --vuu-icon-color: var(--salt-content-secondary-foreground);\n left: 32px;\n position: absolute;\n top: 8px;\n }\n\n .vuuHighlight {\n color: var(--vuu-color-blue-40);\n text-decoration: underline;\n }\n\n .vuuItemPicker-text {\n margin-left: var(--label-margin-left);\n }\n }\n\n .vuuItemPickerListItem-action {\n margin-left: auto;\n visibility: hidden;\n }\n\n .saltOption:hover {\n .vuuItemPickerListItem-action {\n visibility: visible;\n }\n }\n\n .vuuItemPicker-search {\n --saltInput-paddingLeft: var(--salt-spacing-300);\n padding: var(--salt-spacing-200) var(--salt-spacing-300) 0\n var(--salt-spacing-300);\n }\n\n .vuuItemPicker-scrollContainer {\n flex: 1 1 0px;\n overflow: auto;\n\n .vuuItemPicker-sectionHeader {\n align-items: center;\n background: var(--itempicker-background);\n display: flex;\n height: 32px;\n padding: 0 var(--salt-spacing-300);\n position: sticky;\n top: 0;\n z-index: 1;\n }\n\n .vuuItemPicker-availableHeader {\n top: 32px;\n }\n\n .vuuItemPicker-availableList {\n .saltOption {\n padding-left: var(--salt-spacing-300);\n }\n }\n }\n}\n";
2
+
3
+ export { itemPickerCss as default };
4
+ //# sourceMappingURL=ItemPicker.css.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ItemPicker.css.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;"}
@@ -0,0 +1,258 @@
1
+ import { jsxs, jsx } from 'react/jsx-runtime';
2
+ import { Input, ListBox, Button, Option } from '@salt-ds/core';
3
+ import { useComponentCssInjection } from '@salt-ds/styles';
4
+ import { useWindow } from '@salt-ds/window';
5
+ import { applyHighlighting } from '@vuu-ui/vuu-table';
6
+ import { singularForm, pluralForm, DragDropProvider, useSortable } from '@vuu-ui/vuu-utils';
7
+ import cx from 'clsx';
8
+ import { forwardRef, useCallback, useRef, useMemo } from 'react';
9
+ import { Icon } from '../icon-button/Icon.js';
10
+ import { IconButton } from '../icon-button/IconButton.js';
11
+ import { useItemPicker, getItemLabel } from './useItemPicker.js';
12
+ import itemPickerCss from './ItemPicker.css.js';
13
+
14
+ const classBase = "vuuItemPicker";
15
+ const classBaseListItem = "vuuItemPickerListItem";
16
+ const searchIcon = /* @__PURE__ */ jsx(Icon, { name: "search" });
17
+ const NO_SELECTION = [];
18
+ const useSorting = (id, index) => {
19
+ const { handleRef: sortableHandleRef, ref: sortableRef } = useSortable({
20
+ id,
21
+ index
22
+ });
23
+ const [handleRef, ref] = useMemo(() => {
24
+ return [sortableHandleRef, sortableRef];
25
+ }, [sortableHandleRef, sortableRef]);
26
+ return {
27
+ handleRef,
28
+ ref
29
+ };
30
+ };
31
+ const SelectedListItem = ({
32
+ className: classNameProp,
33
+ index,
34
+ item,
35
+ onRemove,
36
+ searchPattern = "",
37
+ ...optionProps
38
+ }) => {
39
+ const { handleRef, ref } = useSorting(item.name, index);
40
+ const value = getItemLabel(item);
41
+ const valueWithHighlighting = applyHighlighting(value, searchPattern);
42
+ const handleRemoveButtonClick = useCallback(
43
+ (e) => {
44
+ e.stopPropagation();
45
+ onRemove?.(e);
46
+ },
47
+ [onRemove]
48
+ );
49
+ return /* @__PURE__ */ jsxs(
50
+ Option,
51
+ {
52
+ ...optionProps,
53
+ className: cx(classNameProp, classBaseListItem),
54
+ "data-name": item.name,
55
+ ref,
56
+ children: [
57
+ /* @__PURE__ */ jsx(
58
+ IconButton,
59
+ {
60
+ "data-embedded": true,
61
+ appearance: "transparent",
62
+ icon: "draggable",
63
+ ref: handleRef,
64
+ size: 16
65
+ }
66
+ ),
67
+ item.icon ? /* @__PURE__ */ jsx(Icon, { name: item.icon }, "icon") : null,
68
+ /* @__PURE__ */ jsx("span", { className: `${classBase}-text`, children: valueWithHighlighting }),
69
+ /* @__PURE__ */ jsx(
70
+ IconButton,
71
+ {
72
+ className: `${classBaseListItem}-action`,
73
+ "data-embedded": true,
74
+ appearance: "transparent",
75
+ icon: "cross",
76
+ onClick: handleRemoveButtonClick,
77
+ size: 16
78
+ }
79
+ )
80
+ ]
81
+ }
82
+ );
83
+ };
84
+ const AvailableListItem = ({
85
+ className: classNameProp,
86
+ index,
87
+ item,
88
+ onAdd,
89
+ searchPattern = "",
90
+ disabled,
91
+ ...optionProps
92
+ }) => {
93
+ const value = getItemLabel(item);
94
+ const valueWithHighlighting = applyHighlighting(value, searchPattern);
95
+ return /* @__PURE__ */ jsxs(
96
+ Option,
97
+ {
98
+ ...optionProps,
99
+ className: cx(classNameProp, classBaseListItem),
100
+ "data-name": item.name,
101
+ disabled,
102
+ children: [
103
+ /* @__PURE__ */ jsx("span", { className: `${classBase}-text`, children: valueWithHighlighting }),
104
+ /* @__PURE__ */ jsx(
105
+ IconButton,
106
+ {
107
+ className: `${classBaseListItem}-action`,
108
+ "data-embedded": true,
109
+ appearance: "transparent",
110
+ icon: "plus",
111
+ onClick: onAdd,
112
+ size: 16,
113
+ disabled
114
+ }
115
+ )
116
+ ]
117
+ }
118
+ );
119
+ };
120
+ const ItemPicker = forwardRef(function ItemPicker2({
121
+ className,
122
+ itemTypeName,
123
+ allItems,
124
+ selectedItems,
125
+ maxSelections,
126
+ onSelectedItemsChange,
127
+ onSelectionChange,
128
+ selected = NO_SELECTION,
129
+ createCustomItemProps,
130
+ ...htmlAttributes
131
+ }, forwardedRef) {
132
+ const targetWindow = useWindow();
133
+ useComponentCssInjection({
134
+ testId: "vuu-item-picker",
135
+ css: itemPickerCss,
136
+ window: targetWindow
137
+ });
138
+ const searchCallbackRef = useCallback((el) => {
139
+ setTimeout(() => {
140
+ el?.querySelector("input")?.focus();
141
+ }, 100);
142
+ }, []);
143
+ const {
144
+ selectedItemsCount,
145
+ availableItemsCount,
146
+ selectedItemsFiltered,
147
+ availableItemsFiltered,
148
+ searchText,
149
+ onChangeSearchInput,
150
+ onAddItemToSelectedList,
151
+ onRemoveItemFromSelectedList,
152
+ onReorderSelectedItems
153
+ } = useItemPicker({
154
+ allItems,
155
+ selectedItems,
156
+ onSelectedItemsChange,
157
+ maxSelections
158
+ });
159
+ const listRef = useRef(null);
160
+ const getOptionName = (option) => {
161
+ if (option) {
162
+ const { name } = option.dataset;
163
+ if (name) {
164
+ return name;
165
+ }
166
+ }
167
+ throw Error("[ItemPicker] list option has no data-name");
168
+ };
169
+ const handleDragEnd = useCallback(() => {
170
+ setTimeout(() => {
171
+ const listItems = listRef.current?.querySelectorAll(".saltOption");
172
+ if (listItems) {
173
+ const orderedItemNames = Array.from(listItems).map(getOptionName);
174
+ onReorderSelectedItems(orderedItemNames);
175
+ }
176
+ }, 300);
177
+ }, [availableItemsFiltered, selectedItemsFiltered]);
178
+ const searchPlaceholderText = `Find ${singularForm(itemTypeName)}`;
179
+ const maxSelectionsSubHeading = maxSelections ? `(${maxSelections} max)` : "";
180
+ const selectedItemsHeading = `${selectedItemsCount} ${selectedItemsCount === 1 ? singularForm(itemTypeName) : pluralForm(itemTypeName)} in view ${maxSelectionsSubHeading}`;
181
+ const availableItemsHeading = `${availableItemsCount} available ${availableItemsCount === 1 ? singularForm(itemTypeName) : pluralForm(itemTypeName)}`;
182
+ return /* @__PURE__ */ jsxs(
183
+ "div",
184
+ {
185
+ ...htmlAttributes,
186
+ className: cx(classBase, className),
187
+ ref: forwardedRef,
188
+ children: [
189
+ /* @__PURE__ */ jsx("form", { className: `${classBase}-search`, role: "search", children: /* @__PURE__ */ jsx(
190
+ Input,
191
+ {
192
+ startAdornment: searchIcon,
193
+ placeholder: searchPlaceholderText,
194
+ ref: searchCallbackRef,
195
+ value: searchText,
196
+ onChange: onChangeSearchInput
197
+ }
198
+ ) }),
199
+ /* @__PURE__ */ jsxs("div", { className: `${classBase}-scrollContainer vuuScrollable`, children: [
200
+ /* @__PURE__ */ jsx("div", { className: `${classBase}-sectionHeader`, children: selectedItemsHeading }),
201
+ /* @__PURE__ */ jsx(DragDropProvider, { onDragEnd: handleDragEnd, children: /* @__PURE__ */ jsx(
202
+ ListBox,
203
+ {
204
+ className: `${classBase}-selectedList`,
205
+ onSelectionChange,
206
+ ref: listRef,
207
+ selected,
208
+ children: selectedItemsFiltered.map((item, index) => /* @__PURE__ */ jsx(
209
+ SelectedListItem,
210
+ {
211
+ item,
212
+ index,
213
+ onRemove: onRemoveItemFromSelectedList,
214
+ searchPattern: searchText.toLowerCase(),
215
+ value: item
216
+ },
217
+ item.name
218
+ ))
219
+ }
220
+ ) }),
221
+ /* @__PURE__ */ jsx(
222
+ "div",
223
+ {
224
+ className: cx(
225
+ `${classBase}-sectionHeader`,
226
+ `${classBase}-availableHeader`
227
+ ),
228
+ children: availableItemsHeading
229
+ }
230
+ ),
231
+ /* @__PURE__ */ jsx(
232
+ ListBox,
233
+ {
234
+ className: `${classBase}-availableList`,
235
+ selected: NO_SELECTION,
236
+ children: availableItemsFiltered.map((item, index) => /* @__PURE__ */ jsx(
237
+ AvailableListItem,
238
+ {
239
+ item,
240
+ index,
241
+ onAdd: onAddItemToSelectedList,
242
+ searchPattern: searchText.toLowerCase(),
243
+ value: item,
244
+ disabled: selectedItemsCount === maxSelections
245
+ },
246
+ item.name
247
+ ))
248
+ }
249
+ )
250
+ ] }),
251
+ createCustomItemProps ? /* @__PURE__ */ jsx("div", { className: `${classBase}-item-buttons`, children: /* @__PURE__ */ jsx(Button, { onClick: createCustomItemProps.onClickCreateCustomItem, children: createCustomItemProps.buttonLabel }) }) : null
252
+ ]
253
+ }
254
+ );
255
+ });
256
+
257
+ export { ItemPicker, classBaseListItem };
258
+ //# sourceMappingURL=ItemPicker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ItemPicker.js","sources":["../../../../packages/vuu-ui-controls/src/item-picker/ItemPicker.tsx"],"sourcesContent":["import {\n Button,\n Input,\n ListBox,\n ListBoxProps,\n Option,\n OptionProps,\n} from \"@salt-ds/core\";\nimport { useComponentCssInjection } from \"@salt-ds/styles\";\nimport { useWindow } from \"@salt-ds/window\";\nimport { applyHighlighting } from \"@vuu-ui/vuu-table\";\nimport {\n DragDropProvider,\n ItemTypeName,\n pluralForm,\n singularForm,\n useSortable,\n} from \"@vuu-ui/vuu-utils\";\nimport cx from \"clsx\";\nimport {\n ForwardedRef,\n forwardRef,\n HTMLAttributes,\n MouseEventHandler,\n RefCallback,\n useCallback,\n useMemo,\n useRef,\n} from \"react\";\nimport { Icon, IconButton } from \"../icon-button\";\nimport {\n CreateCustomItemProps,\n getItemLabel,\n ItemDescriptor,\n ItemPickerHookProps,\n useItemPicker,\n} from \"./useItemPicker\";\n\nimport itemPickerCss from \"./ItemPicker.css\";\n\nconst classBase = \"vuuItemPicker\";\n\nexport const classBaseListItem = \"vuuItemPickerListItem\";\n\nexport interface ItemPickerProps\n extends ItemPickerHookProps,\n HTMLAttributes<HTMLDivElement>,\n Pick<ListBoxProps<ItemDescriptor>, \"selected\" | \"onSelectionChange\"> {\n itemTypeName: ItemTypeName;\n createCustomItemProps?: CreateCustomItemProps;\n}\n\nconst searchIcon = <Icon name=\"search\" />;\nconst NO_SELECTION: ItemDescriptor[] = [] as const;\n\nconst useSorting = (id: string, index: number) => {\n const { handleRef: sortableHandleRef, ref: sortableRef } = useSortable({\n id,\n index,\n });\n\n const [handleRef, ref] = useMemo(() => {\n return [sortableHandleRef, sortableRef];\n }, [sortableHandleRef, sortableRef]);\n\n return {\n handleRef,\n ref,\n };\n};\n\nconst SelectedListItem = ({\n className: classNameProp,\n index,\n item,\n onRemove,\n searchPattern = \"\",\n ...optionProps\n}: OptionProps & {\n index: number;\n item: ItemDescriptor;\n onRemove: MouseEventHandler<HTMLButtonElement>;\n searchPattern?: Lowercase<string>;\n}) => {\n const { handleRef, ref } = useSorting(item.name, index);\n const value = getItemLabel(item as ItemDescriptor);\n const valueWithHighlighting = applyHighlighting(value, searchPattern);\n\n const handleRemoveButtonClick = useCallback<\n MouseEventHandler<HTMLButtonElement>\n >(\n (e) => {\n e.stopPropagation();\n onRemove?.(e);\n },\n [onRemove],\n );\n\n return (\n <Option\n {...optionProps}\n className={cx(classNameProp, classBaseListItem)}\n data-name={item.name}\n ref={ref}\n >\n <IconButton\n data-embedded\n appearance=\"transparent\"\n icon=\"draggable\"\n ref={handleRef}\n size={16}\n />\n {item.icon ? <Icon name={item.icon} key=\"icon\" /> : null}\n <span className={`${classBase}-text`}>{valueWithHighlighting}</span>\n <IconButton\n className={`${classBaseListItem}-action`}\n data-embedded\n appearance=\"transparent\"\n icon=\"cross\"\n onClick={handleRemoveButtonClick}\n size={16}\n />\n </Option>\n );\n};\n\nconst AvailableListItem = ({\n className: classNameProp,\n index,\n item,\n onAdd,\n searchPattern = \"\",\n disabled,\n ...optionProps\n}: OptionProps & {\n index: number;\n item: ItemDescriptor;\n onAdd: MouseEventHandler<HTMLButtonElement>;\n searchPattern?: Lowercase<string>;\n}) => {\n const value = getItemLabel(item as ItemDescriptor);\n const valueWithHighlighting = applyHighlighting(value, searchPattern);\n\n return (\n <Option\n {...optionProps}\n className={cx(classNameProp, classBaseListItem)}\n data-name={item.name}\n disabled={disabled}\n >\n <span className={`${classBase}-text`}>{valueWithHighlighting}</span>\n <IconButton\n className={`${classBaseListItem}-action`}\n data-embedded\n appearance=\"transparent\"\n icon=\"plus\"\n onClick={onAdd}\n size={16}\n disabled={disabled}\n />\n </Option>\n );\n};\n\n/** Generic controlled component that displays items in a 'picker' that are available to search, select, reorder or deselect.\n * As input props, all available items, currently selected items, search text and callback functions for changes in selected items and search text\n * must be supplied by the client.\n */\nexport const ItemPicker = forwardRef(function ItemPicker(\n {\n className,\n itemTypeName,\n allItems,\n selectedItems,\n maxSelections,\n onSelectedItemsChange,\n onSelectionChange,\n selected = NO_SELECTION,\n createCustomItemProps,\n ...htmlAttributes\n }: ItemPickerProps,\n forwardedRef: ForwardedRef<HTMLDivElement>,\n) {\n const targetWindow = useWindow();\n useComponentCssInjection({\n testId: \"vuu-item-picker\",\n css: itemPickerCss,\n window: targetWindow,\n });\n\n const searchCallbackRef = useCallback<RefCallback<HTMLElement>>((el) => {\n setTimeout(() => {\n el?.querySelector(\"input\")?.focus();\n }, 100);\n }, []);\n\n const {\n selectedItemsCount,\n availableItemsCount,\n selectedItemsFiltered,\n availableItemsFiltered,\n searchText,\n onChangeSearchInput,\n onAddItemToSelectedList,\n onRemoveItemFromSelectedList,\n onReorderSelectedItems,\n } = useItemPicker({\n allItems,\n selectedItems,\n onSelectedItemsChange,\n maxSelections,\n });\n\n const listRef = useRef<HTMLDivElement>(null);\n\n const getOptionName = (option?: HTMLElement) => {\n if (option) {\n const { name } = option.dataset;\n if (name) {\n return name;\n }\n }\n throw Error(\"[ItemPicker] list option has no data-name\");\n };\n\n const handleDragEnd = useCallback(() => {\n setTimeout(() => {\n const listItems =\n listRef.current?.querySelectorAll<HTMLDivElement>(\".saltOption\");\n if (listItems) {\n const orderedItemNames = Array.from(listItems).map(getOptionName);\n onReorderSelectedItems(orderedItemNames);\n }\n }, 300);\n }, [availableItemsFiltered, selectedItemsFiltered]);\n\n const searchPlaceholderText = `Find ${singularForm(itemTypeName)}`;\n const maxSelectionsSubHeading = maxSelections ? `(${maxSelections} max)` : \"\";\n const selectedItemsHeading = `${selectedItemsCount} ${selectedItemsCount === 1 ? singularForm(itemTypeName) : pluralForm(itemTypeName)} in view ${maxSelectionsSubHeading}`;\n const availableItemsHeading = `${availableItemsCount} available ${availableItemsCount === 1 ? singularForm(itemTypeName) : pluralForm(itemTypeName)}`;\n\n return (\n <div\n {...htmlAttributes}\n className={cx(classBase, className)}\n ref={forwardedRef}\n >\n <form className={`${classBase}-search`} role=\"search\">\n <Input\n startAdornment={searchIcon}\n placeholder={searchPlaceholderText}\n ref={searchCallbackRef}\n value={searchText}\n onChange={onChangeSearchInput}\n />\n </form>\n\n <div className={`${classBase}-scrollContainer vuuScrollable`}>\n <div className={`${classBase}-sectionHeader`}>\n {selectedItemsHeading}\n </div>\n <DragDropProvider onDragEnd={handleDragEnd}>\n <ListBox\n className={`${classBase}-selectedList`}\n onSelectionChange={onSelectionChange}\n ref={listRef}\n selected={selected}\n >\n {selectedItemsFiltered.map((item, index) => (\n <SelectedListItem\n item={item}\n index={index}\n key={item.name}\n onRemove={onRemoveItemFromSelectedList}\n searchPattern={searchText.toLowerCase() as Lowercase<string>}\n value={item}\n />\n ))}\n </ListBox>\n </DragDropProvider>\n\n <div\n className={cx(\n `${classBase}-sectionHeader`,\n `${classBase}-availableHeader`,\n )}\n >\n {availableItemsHeading}\n </div>\n <ListBox\n className={`${classBase}-availableList`}\n selected={NO_SELECTION}\n >\n {availableItemsFiltered.map((item, index) => (\n <AvailableListItem\n item={item}\n index={index}\n key={item.name}\n onAdd={onAddItemToSelectedList}\n searchPattern={searchText.toLowerCase() as Lowercase<string>}\n value={item}\n disabled={selectedItemsCount === maxSelections}\n />\n ))}\n </ListBox>\n </div>\n {createCustomItemProps ? (\n <div className={`${classBase}-item-buttons`}>\n <Button onClick={createCustomItemProps.onClickCreateCustomItem}>\n {createCustomItemProps.buttonLabel}\n </Button>\n </div>\n ) : null}\n </div>\n );\n});\n"],"names":["ItemPicker"],"mappings":"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"}
@@ -0,0 +1,115 @@
1
+ import { reorderItems, queryClosest } from '@vuu-ui/vuu-utils';
2
+ import { useState, useCallback, useMemo } from 'react';
3
+
4
+ const filterItems = (items, pattern) => {
5
+ if (pattern) {
6
+ const lowerCasePattern = pattern.toLowerCase();
7
+ return items.filter(
8
+ (item) => getItemLabel(item).toLowerCase().indexOf(lowerCasePattern) !== -1
9
+ );
10
+ } else {
11
+ return items;
12
+ }
13
+ };
14
+ const getItemLabel = (item) => {
15
+ if (item.label) return item.label;
16
+ else return item.name;
17
+ };
18
+ const itemName = (target) => {
19
+ const listItem = queryClosest(target, ".saltOption", true);
20
+ const { name } = listItem.dataset;
21
+ if (name) {
22
+ return name;
23
+ } else {
24
+ throw Error(
25
+ "[useItemPicker] item name could not be identified, data-name attribute not found"
26
+ );
27
+ }
28
+ };
29
+ const byItemName = ({ name: n1 }, { name: n2 }) => n1 > n2 ? 1 : n2 > n1 ? -1 : 0;
30
+ const useItemPicker = ({
31
+ allItems,
32
+ selectedItems,
33
+ maxSelections,
34
+ onSelectedItemsChange
35
+ }) => {
36
+ if (maxSelections && selectedItems.length > maxSelections) {
37
+ throw Error(
38
+ `[useItemPicker] max selections ${maxSelections} exceeded by selected items (that has count ${selectedItems.length})`
39
+ );
40
+ }
41
+ const [searchPattern, setSearchPattern] = useState("");
42
+ const handleChangeSearchInput = useCallback((evt) => {
43
+ const { value } = evt.target;
44
+ setSearchPattern(value);
45
+ }, []);
46
+ const handleAddItemToSelectedList = useCallback(
47
+ (e) => {
48
+ const name = itemName(e.target);
49
+ const itemToAdd = allItems.find((item) => item.name === name);
50
+ if (itemToAdd) {
51
+ const newSelectedItems = selectedItems.concat(itemToAdd);
52
+ onSelectedItemsChange(newSelectedItems);
53
+ } else {
54
+ throw Error(
55
+ `[useItemPicker] handleAddItemToSelectedList, item '${name}' not found`
56
+ );
57
+ }
58
+ },
59
+ [allItems, selectedItems, onSelectedItemsChange]
60
+ );
61
+ const handleRemoveItemFromSelectedList = useCallback(
62
+ (e) => {
63
+ const name = itemName(e.target);
64
+ const itemToRemove = selectedItems.find((item) => item.name === name);
65
+ if (itemToRemove) {
66
+ const newSelectedItems = selectedItems.filter(
67
+ (item) => item.name !== name
68
+ );
69
+ onSelectedItemsChange(newSelectedItems);
70
+ } else {
71
+ throw Error(
72
+ `[useItemPicker] handleRemoveItemFromSelectedList, item '${name}' not found`
73
+ );
74
+ }
75
+ },
76
+ [selectedItems, onSelectedItemsChange]
77
+ );
78
+ const handleReorderSelectedItems = useCallback(
79
+ (orderedItemNames) => {
80
+ const reorderedSelectedItems = reorderItems(
81
+ selectedItems,
82
+ orderedItemNames
83
+ );
84
+ onSelectedItemsChange(reorderedSelectedItems);
85
+ },
86
+ [selectedItems, onSelectedItemsChange]
87
+ );
88
+ const getSelectedItemsFiltered = useMemo(() => {
89
+ return filterItems(selectedItems, searchPattern);
90
+ }, [selectedItems, searchPattern]);
91
+ const getAvailableItemsFiltered = useMemo(() => {
92
+ return filterItems(allItems, searchPattern).filter(
93
+ ({ name }) => selectedItems.findIndex((item) => item.name === name) === -1
94
+ ).toSorted(byItemName);
95
+ }, [allItems, selectedItems, searchPattern]);
96
+ const getAvailableItemsCount = useMemo(() => {
97
+ return allItems.filter(
98
+ ({ name }) => selectedItems.findIndex((item) => item.name === name) === -1
99
+ ).length;
100
+ }, [allItems, selectedItems]);
101
+ return {
102
+ selectedItemsCount: selectedItems.length,
103
+ availableItemsCount: getAvailableItemsCount,
104
+ selectedItemsFiltered: getSelectedItemsFiltered,
105
+ availableItemsFiltered: getAvailableItemsFiltered,
106
+ searchText: searchPattern,
107
+ onChangeSearchInput: handleChangeSearchInput,
108
+ onAddItemToSelectedList: handleAddItemToSelectedList,
109
+ onRemoveItemFromSelectedList: handleRemoveItemFromSelectedList,
110
+ onReorderSelectedItems: handleReorderSelectedItems
111
+ };
112
+ };
113
+
114
+ export { getItemLabel, useItemPicker };
115
+ //# sourceMappingURL=useItemPicker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useItemPicker.js","sources":["../../../../packages/vuu-ui-controls/src/item-picker/useItemPicker.tsx"],"sourcesContent":["import { queryClosest, reorderItems } from \"@vuu-ui/vuu-utils\";\nimport {\n FormEventHandler,\n MouseEventHandler,\n useCallback,\n useMemo,\n useState,\n} from \"react\";\n\n/** This is a public description of an Item that can be displayed in the ItemPicker component, defining all the\n * mandatory and option attributes that can be defined by the client. */\nexport interface ItemDescriptor {\n /** Unique name for this data value */\n name: string;\n /** Optional label for display instead of the name */\n label?: string;\n /** Optional data-icon name for display of an icon to the left of the item label/name in the selected list */\n icon?: string;\n /** Optional string to define groups of items for client reference, not currently used for rendering */\n group?: string;\n}\n\nexport interface CreateCustomItemProps {\n buttonLabel: string;\n onClickCreateCustomItem: MouseEventHandler<HTMLButtonElement>;\n}\n\nexport interface ItemPickerHookProps {\n allItems: ItemDescriptor[];\n selectedItems: ItemDescriptor[];\n maxSelections?: number;\n onSelectedItemsChange: (newSelectedItems: ItemDescriptor[]) => void;\n}\n\nconst filterItems = (\n items: readonly ItemDescriptor[],\n pattern: string,\n): readonly ItemDescriptor[] => {\n if (pattern) {\n const lowerCasePattern = pattern.toLowerCase();\n return items.filter(\n (item) =>\n getItemLabel(item).toLowerCase().indexOf(lowerCasePattern) !== -1,\n );\n } else {\n return items;\n }\n};\n\nexport const getItemLabel = (item: ItemDescriptor) => {\n if (item.label) return item.label;\n else return item.name;\n};\n\nconst itemName = (target: EventTarget): string => {\n const listItem = queryClosest(target, \".saltOption\", true);\n const { name } = listItem.dataset;\n if (name) {\n return name;\n } else {\n throw Error(\n \"[useItemPicker] item name could not be identified, data-name attribute not found\",\n );\n }\n};\n\nconst byItemName = (\n { name: n1 }: ItemDescriptor,\n { name: n2 }: ItemDescriptor,\n) => (n1 > n2 ? 1 : n2 > n1 ? -1 : 0);\n\nexport const useItemPicker = ({\n allItems,\n selectedItems,\n maxSelections,\n onSelectedItemsChange,\n}: ItemPickerHookProps) => {\n if (maxSelections && selectedItems.length > maxSelections) {\n throw Error(\n `[useItemPicker] max selections ${maxSelections} exceeded by selected items (that has count ${selectedItems.length})`,\n );\n }\n\n const [searchPattern, setSearchPattern] = useState(\"\");\n\n const handleChangeSearchInput = useCallback<FormEventHandler>((evt) => {\n const { value } = evt.target as HTMLInputElement;\n setSearchPattern(value);\n }, []);\n\n const handleAddItemToSelectedList = useCallback<\n MouseEventHandler<HTMLButtonElement>\n >(\n (e) => {\n const name = itemName(e.target);\n const itemToAdd = allItems.find((item) => item.name === name);\n if (itemToAdd) {\n const newSelectedItems = selectedItems.concat(itemToAdd);\n onSelectedItemsChange(newSelectedItems);\n } else {\n throw Error(\n `[useItemPicker] handleAddItemToSelectedList, item '${name}' not found`,\n );\n }\n },\n [allItems, selectedItems, onSelectedItemsChange],\n );\n\n const handleRemoveItemFromSelectedList = useCallback<\n MouseEventHandler<HTMLButtonElement>\n >(\n (e) => {\n const name = itemName(e.target);\n const itemToRemove = selectedItems.find((item) => item.name === name);\n if (itemToRemove) {\n const newSelectedItems = selectedItems.filter(\n (item) => item.name !== name,\n );\n onSelectedItemsChange(newSelectedItems);\n } else {\n throw Error(\n `[useItemPicker] handleRemoveItemFromSelectedList, item '${name}' not found`,\n );\n }\n },\n [selectedItems, onSelectedItemsChange],\n );\n\n const handleReorderSelectedItems = useCallback(\n (orderedItemNames: string[]) => {\n const reorderedSelectedItems: ItemDescriptor[] = reorderItems(\n selectedItems,\n orderedItemNames,\n );\n onSelectedItemsChange(reorderedSelectedItems);\n },\n [selectedItems, onSelectedItemsChange],\n );\n\n const getSelectedItemsFiltered = useMemo(() => {\n return filterItems(selectedItems, searchPattern);\n }, [selectedItems, searchPattern]);\n\n const getAvailableItemsFiltered = useMemo(() => {\n return filterItems(allItems, searchPattern)\n .filter(\n ({ name }) =>\n selectedItems.findIndex((item) => item.name === name) === -1,\n )\n .toSorted(byItemName);\n }, [allItems, selectedItems, searchPattern]);\n\n const getAvailableItemsCount = useMemo(() => {\n return allItems.filter(\n ({ name }) =>\n selectedItems.findIndex((item) => item.name === name) === -1,\n ).length;\n }, [allItems, selectedItems]);\n\n return {\n selectedItemsCount: selectedItems.length,\n availableItemsCount: getAvailableItemsCount,\n selectedItemsFiltered: getSelectedItemsFiltered,\n availableItemsFiltered: getAvailableItemsFiltered,\n searchText: searchPattern,\n onChangeSearchInput: handleChangeSearchInput,\n onAddItemToSelectedList: handleAddItemToSelectedList,\n onRemoveItemFromSelectedList: handleRemoveItemFromSelectedList,\n onReorderSelectedItems: handleReorderSelectedItems,\n };\n};\n"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,20 +1,20 @@
1
1
  {
2
- "version": "2.2.0",
2
+ "version": "2.3.0",
3
3
  "description": "VUU UI Controls",
4
4
  "author": "heswell",
5
5
  "license": "Apache-2.0",
6
6
  "devDependencies": {
7
- "@vuu-ui/vuu-data-types": "2.2.0",
8
- "@vuu-ui/vuu-protocol-types": "2.2.0",
9
- "@vuu-ui/vuu-table-types": "2.2.0"
7
+ "@vuu-ui/vuu-data-types": "2.3.0",
8
+ "@vuu-ui/vuu-protocol-types": "2.3.0",
9
+ "@vuu-ui/vuu-table-types": "2.3.0"
10
10
  },
11
11
  "dependencies": {
12
- "@vuu-ui/vuu-context-menu": "2.2.0",
13
- "@vuu-ui/vuu-data-react": "2.2.0",
14
- "@vuu-ui/vuu-layout": "2.2.0",
15
- "@vuu-ui/vuu-popups": "2.2.0",
16
- "@vuu-ui/vuu-table": "2.2.0",
17
- "@vuu-ui/vuu-utils": "2.2.0",
12
+ "@vuu-ui/vuu-context-menu": "2.3.0",
13
+ "@vuu-ui/vuu-data-react": "2.3.0",
14
+ "@vuu-ui/vuu-layout": "2.3.0",
15
+ "@vuu-ui/vuu-popups": "2.3.0",
16
+ "@vuu-ui/vuu-table": "2.3.0",
17
+ "@vuu-ui/vuu-utils": "2.3.0",
18
18
  "@salt-ds/core": "1.54.1",
19
19
  "@salt-ds/icons": "1.16.0",
20
20
  "@salt-ds/styles": "0.2.1",
package/types/index.d.ts CHANGED
@@ -12,6 +12,8 @@ export * from "./expando-input";
12
12
  export { HoverOverlay } from "./hover-overlay/HoverOverlay";
13
13
  export * from "./icon-button";
14
14
  export * from "./instrument-picker";
15
+ export { ItemPicker, type ItemPickerProps } from "./item-picker/ItemPicker";
16
+ export { type ItemDescriptor, type CreateCustomItemProps, } from "./item-picker/useItemPicker";
15
17
  export * from "./measured-container";
16
18
  export { ModalProvider, useModal } from "./modal-provider/ModalProvider";
17
19
  export * from "./overflow-container";
@@ -0,0 +1,14 @@
1
+ import { ListBoxProps } from "@salt-ds/core";
2
+ import { ItemTypeName } from "@vuu-ui/vuu-utils";
3
+ import { HTMLAttributes } from "react";
4
+ import { CreateCustomItemProps, ItemDescriptor, ItemPickerHookProps } from "./useItemPicker";
5
+ export declare const classBaseListItem = "vuuItemPickerListItem";
6
+ export interface ItemPickerProps extends ItemPickerHookProps, HTMLAttributes<HTMLDivElement>, Pick<ListBoxProps<ItemDescriptor>, "selected" | "onSelectionChange"> {
7
+ itemTypeName: ItemTypeName;
8
+ createCustomItemProps?: CreateCustomItemProps;
9
+ }
10
+ /** Generic controlled component that displays items in a 'picker' that are available to search, select, reorder or deselect.
11
+ * As input props, all available items, currently selected items, search text and callback functions for changes in selected items and search text
12
+ * must be supplied by the client.
13
+ */
14
+ export declare const ItemPicker: import("react").ForwardRefExoticComponent<ItemPickerProps & import("react").RefAttributes<HTMLDivElement>>;
@@ -0,0 +1,35 @@
1
+ import { FormEventHandler, MouseEventHandler } from "react";
2
+ /** This is a public description of an Item that can be displayed in the ItemPicker component, defining all the
3
+ * mandatory and option attributes that can be defined by the client. */
4
+ export interface ItemDescriptor {
5
+ /** Unique name for this data value */
6
+ name: string;
7
+ /** Optional label for display instead of the name */
8
+ label?: string;
9
+ /** Optional data-icon name for display of an icon to the left of the item label/name in the selected list */
10
+ icon?: string;
11
+ /** Optional string to define groups of items for client reference, not currently used for rendering */
12
+ group?: string;
13
+ }
14
+ export interface CreateCustomItemProps {
15
+ buttonLabel: string;
16
+ onClickCreateCustomItem: MouseEventHandler<HTMLButtonElement>;
17
+ }
18
+ export interface ItemPickerHookProps {
19
+ allItems: ItemDescriptor[];
20
+ selectedItems: ItemDescriptor[];
21
+ maxSelections?: number;
22
+ onSelectedItemsChange: (newSelectedItems: ItemDescriptor[]) => void;
23
+ }
24
+ export declare const getItemLabel: (item: ItemDescriptor) => string;
25
+ export declare const useItemPicker: ({ allItems, selectedItems, maxSelections, onSelectedItemsChange, }: ItemPickerHookProps) => {
26
+ selectedItemsCount: number;
27
+ availableItemsCount: number;
28
+ selectedItemsFiltered: readonly ItemDescriptor[];
29
+ availableItemsFiltered: ItemDescriptor[];
30
+ searchText: string;
31
+ onChangeSearchInput: FormEventHandler;
32
+ onAddItemToSelectedList: MouseEventHandler<HTMLButtonElement>;
33
+ onRemoveItemFromSelectedList: MouseEventHandler<HTMLButtonElement>;
34
+ onReorderSelectedItems: (orderedItemNames: string[]) => void;
35
+ };