@rescui/select 0.15.1-RUI-280-Update-select-component-e516591ed.54 → 0.15.1-RUI-280-Update-select-component-downshift-cd2a98813.66

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.
Files changed (65) hide show
  1. package/lib/_virtual/index.css.js +12 -12
  2. package/lib/constants.d.ts +0 -1
  3. package/lib/constants.js +1 -2
  4. package/lib/hooks/use-a11y.d.ts +29 -0
  5. package/lib/hooks/use-a11y.js +53 -0
  6. package/lib/hooks/use-backward-compatible-id.d.ts +4 -0
  7. package/lib/hooks/use-backward-compatible-id.js +14 -0
  8. package/lib/hooks/use-label-focus.d.ts +1 -3
  9. package/lib/hooks/use-label-focus.js +3 -9
  10. package/lib/hooks/use-multi-handlers.d.ts +11 -0
  11. package/lib/hooks/use-multi-handlers.js +48 -0
  12. package/lib/hooks/use-options.d.ts +13 -0
  13. package/lib/hooks/use-options.js +89 -0
  14. package/lib/index.css +323 -371
  15. package/lib/multiselect.d.ts +3 -29
  16. package/lib/multiselect.js +249 -171
  17. package/lib/native-select.js +67 -44
  18. package/lib/parts/hidden-select.d.ts +13 -0
  19. package/lib/parts/hidden-select.js +45 -0
  20. package/lib/parts/icons.d.ts +3 -1
  21. package/lib/parts/icons.js +11 -14
  22. package/lib/parts/list-box.d.ts +7 -15
  23. package/lib/parts/list-box.js +50 -80
  24. package/lib/parts/multi-trigger.d.ts +5 -4
  25. package/lib/parts/multi-trigger.js +27 -15
  26. package/lib/parts/native-select-trigger.d.ts +6 -8
  27. package/lib/parts/native-select-trigger.js +14 -23
  28. package/lib/select-basic.d.ts +2 -2
  29. package/lib/select-basic.js +162 -95
  30. package/lib/select-searchable.d.ts +2 -2
  31. package/lib/select-searchable.js +209 -119
  32. package/lib/select.d.ts +1 -1
  33. package/lib/select.js +38 -31
  34. package/lib/styles/common.p.module.css.js +25 -24
  35. package/lib/styles/form-trigger.p.module.css.js +12 -12
  36. package/lib/styles/list-box.p.module.css.js +11 -13
  37. package/lib/styles/native-select-trigger.p.module.css.js +15 -14
  38. package/lib/styles/native-select.p.module.css.js +6 -6
  39. package/lib/types.d.ts +26 -26
  40. package/lib/utils/get-real-items-cnt.d.ts +2 -2
  41. package/lib/utils/get-real-items-cnt.js +8 -10
  42. package/lib/utils/item-to-string.d.ts +2 -0
  43. package/lib/utils/item-to-string.js +7 -0
  44. package/lib/utils/merge-props.d.ts +29 -0
  45. package/lib/utils/merge-props.js +106 -0
  46. package/package.json +12 -8
  47. package/lib/hooks/use-css-context-transfer.d.ts +0 -14
  48. package/lib/hooks/use-css-context-transfer.js +0 -40
  49. package/lib/hooks/use-multi-option-serialization.d.ts +0 -10
  50. package/lib/hooks/use-multi-option-serialization.js +0 -82
  51. package/lib/hooks/use-observable-force-rerender.d.ts +0 -2
  52. package/lib/hooks/use-observable-force-rerender.js +0 -16
  53. package/lib/hooks/use-option-serialization.d.ts +0 -10
  54. package/lib/hooks/use-option-serialization.js +0 -59
  55. package/lib/parts/popover.d.ts +0 -12
  56. package/lib/parts/popover.js +0 -62
  57. package/lib/parts/popup-content.d.ts +0 -19
  58. package/lib/parts/popup-content.js +0 -76
  59. package/lib/styles/popover.p.module.css.js +0 -10
  60. package/lib/utils/convert-options.d.ts +0 -3
  61. package/lib/utils/convert-options.js +0 -52
  62. package/lib/utils/convert-placement.d.ts +0 -3
  63. package/lib/utils/convert-placement.js +0 -22
  64. package/lib/utils/css-vars-utils.d.ts +0 -3
  65. package/lib/utils/css-vars-utils.js +0 -20
@@ -1,29 +1,3 @@
1
- import React from 'react';
2
- import PropTypes from 'prop-types';
3
- import type { MultiselectProps, NoOptionsMessage, SelectOption, SelectOptionOrGroup } from './types';
4
- export declare function Multiselect({ options, value, onChange, error, note, size, mode, theme: themeFromProps, placement, isSingleLine, boldLabel, reserveErrorLine, maxVisibleItems, isClearable, isLoading, isDisabled, ...props }: MultiselectProps): JSX.Element;
5
- export declare namespace Multiselect {
6
- var propTypes: {
7
- className: PropTypes.Requireable<string>;
8
- style: PropTypes.Requireable<object>;
9
- options: React.Validator<SelectOptionOrGroup[]>;
10
- value: React.Validator<SelectOption[]>;
11
- onChange: PropTypes.Requireable<(...args: any[]) => any>;
12
- size: PropTypes.Requireable<string>;
13
- mode: PropTypes.Requireable<string>;
14
- theme: PropTypes.Requireable<string>;
15
- placeholder: PropTypes.Requireable<string>;
16
- isSingleLine: PropTypes.Requireable<boolean>;
17
- isClearable: PropTypes.Requireable<boolean>;
18
- isDisabled: PropTypes.Requireable<boolean>;
19
- isLoading: PropTypes.Requireable<boolean>;
20
- error: React.Validator<React.ReactNode>;
21
- label: PropTypes.Requireable<PropTypes.ReactNodeLike>;
22
- note: PropTypes.Requireable<PropTypes.ReactNodeLike>;
23
- id: PropTypes.Requireable<string>;
24
- noOptionsMessage: React.Validator<NoOptionsMessage>;
25
- placement: PropTypes.Requireable<string>;
26
- name: PropTypes.Requireable<string>;
27
- boldLabel: PropTypes.Requireable<boolean>;
28
- };
29
- }
1
+ /// <reference types="react" />
2
+ import type { MultiselectProps } from './types';
3
+ export declare function Multiselect({ options, value: selectedItems, onChange, error, label, note, size, mode, theme: themeFromProps, placement, isSingleLine, boldLabel, reserveErrorLine, maxVisibleItems, isClearable, isLoading, isDisabled, name, noOptionsMessage, className, id, style, placeholder, ...rest }: MultiselectProps): JSX.Element;
@@ -1,22 +1,24 @@
1
- "use client";
2
- import React, { useRef } from 'react';
3
- import PropTypes from 'prop-types';
1
+ import React, { useEffect } from 'react';
4
2
  import cn from 'classnames';
5
- import { useSelectState } from 'react-stately';
6
- import { useSelect, useButton, useFocusRing, mergeProps, HiddenSelect } from 'react-aria';
3
+ import { Dropdown } from '@rescui/dropdown';
4
+ import { useSelect } from 'downshift';
7
5
  import { useThemeWithUndefined } from '@rescui/ui-contexts';
8
- import { ErrorMessage } from '@rescui/form-field';
9
- import { optionType, optionsGroupType, noOptionsMessageType } from './common-prop-types.js';
10
- import { PopupContent } from './parts/popup-content.js';
6
+ import { useFocusRing } from '@react-aria/focus';
7
+ import scrollIntoView from 'scroll-into-view-if-needed';
8
+ import { useLabelFocus } from './hooks/use-label-focus.js';
9
+ import { useOptions } from './hooks/use-options.js';
10
+ import { useMultiHandlers } from './hooks/use-multi-handlers.js';
11
+ import { useA11y } from './hooks/use-a11y.js';
12
+ import { ListBox } from './parts/list-box.js';
11
13
  import { Icons } from './parts/icons.js';
12
- import { FormTriggerWrapper } from './parts/form-trigger-wrapper.js';
13
14
  import { MultiTrigger } from './parts/multi-trigger.js';
14
- import { useLabelFocus } from './hooks/use-label-focus.js';
15
- import { useMultiOptionSerialization } from './hooks/use-multi-option-serialization.js';
15
+ import { HiddenSelect } from './parts/hidden-select.js';
16
+ import { FormTriggerWrapper } from './parts/form-trigger-wrapper.js';
16
17
  import { getEmptyMessage } from './utils/get-empty-message.js';
17
- import { convertOptions } from './utils/convert-options.js';
18
- import { convertPlacement } from './utils/convert-placement.js';
18
+ import { mergeProps } from './utils/merge-props.js';
19
+ import { itemToString } from './utils/item-to-string.js';
19
20
  import { getOptionValue } from './utils/get-option-value.js';
21
+ import { getRealItemsCnt } from './utils/get-real-items-cnt.js';
20
22
  import styles from './styles/common.p.module.css.js';
21
23
  const THEME_STYLES = {
22
24
  light: styles.themeLight,
@@ -30,65 +32,188 @@ const SIZE_STYLES = {
30
32
  const MODE_STYLES = {
31
33
  classic: styles.classic,
32
34
  rock: styles.rock
35
+ };
36
+
37
+ const stateReducer = (state, actionAndChanges) => {
38
+ const {
39
+ changes,
40
+ type
41
+ } = actionAndChanges; // console.log(type, state, changes);
42
+
43
+ switch (type) {
44
+ case useSelect.stateChangeTypes.ToggleButtonKeyDownEnter:
45
+ case useSelect.stateChangeTypes.ToggleButtonKeyDownSpaceButton:
46
+ case useSelect.stateChangeTypes.ItemClick:
47
+ return { ...changes,
48
+ isOpen: true,
49
+ highlightedIndex: state.highlightedIndex
50
+ };
51
+
52
+ default:
53
+ return { ...changes,
54
+ selectedItem: null
55
+ };
56
+ }
33
57
  }; // eslint-disable-next-line complexity
34
58
 
35
- function MultiselectInner(_ref) {
59
+
60
+ function Multiselect(_ref) {
36
61
  let {
62
+ options = [],
63
+ value: selectedItems = [],
64
+ onChange,
65
+ error,
66
+ label,
67
+ note,
68
+ size = 'm',
69
+ mode = 'classic',
70
+ theme: themeFromProps,
71
+ placement = 'bottom',
72
+ isSingleLine = false,
73
+ boldLabel = true,
74
+ reserveErrorLine = false,
75
+ maxVisibleItems = 5,
76
+ isClearable = false,
77
+ isLoading = false,
78
+ isDisabled = false,
79
+ name,
80
+ noOptionsMessage,
81
+ className,
37
82
  id,
38
- size,
39
- mode,
40
- theme,
41
83
  style,
42
- className,
43
- isClearable,
44
- isLoading,
45
- placement,
46
- boldLabel,
47
- reserveErrorLine,
48
- noOptionsMessage,
49
- maxVisibleItems,
50
- isSingleLine,
51
- ...ariaProps
84
+ placeholder,
85
+ ...rest
52
86
  } = _ref;
53
- // Create state with multiple selection mode
54
- const state = useSelectState({ ...ariaProps,
55
- selectionMode: 'multiple',
56
- allowsEmptyCollection: true
87
+ const theme = useThemeWithUndefined(themeFromProps);
88
+ const triggerRef = React.useRef(null);
89
+ const menuRef = React.useRef(null);
90
+ const dropdownRef = React.useRef(null);
91
+ const {
92
+ selectedMap,
93
+ removeItem: removeSelectedItem,
94
+ addItem: addSelectedItem,
95
+ removeAllItems: reset
96
+ } = useMultiHandlers({
97
+ selectedItems,
98
+ onChange
57
99
  });
58
- const hasValue = state.selectedItems.length > 0; // Get props for child elements from useSelect
59
-
60
- const triggerRef = useRef(null);
61
100
  const {
62
- labelProps,
63
- triggerProps,
64
- valueProps,
65
- menuProps: listBoxProps,
66
- hiddenSelectProps,
101
+ flattenedOptions,
102
+ listBoxItems
103
+ } = useOptions(options);
104
+ const isListBoxEmpty = getRealItemsCnt(listBoxItems) === 0;
105
+ const {
106
+ fieldProps,
67
107
  errorMessageProps,
68
- descriptionProps
69
- } = useSelect({ ...ariaProps,
70
- selectionMode: 'multiple',
71
- onKeyDown: e => {
72
- if (hasValue) {
73
- if (e.key === 'Backspace') {
74
- state.setValue([...state.selectedItems].map(item => item.key).slice(0, -1));
75
- e.stopPropagation();
108
+ descriptionProps,
109
+ baseId
110
+ } = useA11y({
111
+ prefix: 'multiselect',
112
+ error,
113
+ description: note
114
+ });
115
+ const {
116
+ isOpen,
117
+ getToggleButtonProps,
118
+ getLabelProps,
119
+ getMenuProps,
120
+ highlightedIndex,
121
+ getItemProps,
122
+ setHighlightedIndex
123
+ } = useSelect({
124
+ items: flattenedOptions,
125
+ itemToString,
126
+ id: baseId,
127
+
128
+ onIsOpenChange(newIsOpen) {
129
+ if (newIsOpen) {
130
+ // set the first selected item as a highlighted item if the popup is opened
131
+ const firstSelectedKey = [...selectedMap.keys()][0];
132
+
133
+ if (firstSelectedKey) {
134
+ setHighlightedIndex(flattenedOptions.findIndex(item => firstSelectedKey === getOptionValue(item)));
76
135
  }
136
+ }
137
+ },
77
138
 
78
- if (e.key === 'Delete') {
79
- state.setValue([]);
80
- e.stopPropagation();
139
+ stateReducer,
140
+ // there's no actual selection happening in the hook, we're doing it custom via controlled props.
141
+ selectedItem: null,
142
+ // defining our scroll behavior because on the first menu render the hook
143
+ // doesn't know menuRef and use page as the scroll boundary
144
+ scrollIntoView: (listItemEl, listEl) => {
145
+ scrollIntoView(listItemEl, {
146
+ scrollMode: 'if-needed',
147
+ block: 'nearest',
148
+ boundary: listEl ?? menuRef.current
149
+ });
150
+ },
151
+ onSelectedItemChange: _ref2 => {
152
+ let {
153
+ selectedItem
154
+ } = _ref2;
155
+
156
+ if (!selectedItem) {
157
+ return;
158
+ }
159
+
160
+ const changedValue = getOptionValue(selectedItem);
161
+
162
+ if (selectedMap.has(changedValue)) {
163
+ removeSelectedItem(selectedItem);
164
+ } else {
165
+ addSelectedItem(selectedItem);
166
+ }
167
+ },
168
+
169
+ isItemDisabled(item) {
170
+ return item && item.disabled;
171
+ }
172
+
173
+ });
174
+ const menuProps = getMenuProps({
175
+ ref: menuRef,
176
+ 'aria-multiselectable': isListBoxEmpty ? undefined : 'true'
177
+ }, {
178
+ suppressRefError: true
179
+ });
180
+ const triggerProps = getToggleButtonProps({
181
+ ref: triggerRef,
182
+ // eslint-disable-next-line complexity
183
+ onKeyDown: event => {
184
+ // shift + arrow up/down selection (when popup is open)
185
+ if (isOpen && event.shiftKey && (event.key === 'ArrowUp' || event.key === 'ArrowDown')) {
186
+ const newHighlightedIndex = event.key === 'ArrowUp' ? Math.max(highlightedIndex - 1, 0) : Math.min(highlightedIndex + 1, flattenedOptions.length - 1);
187
+
188
+ if (newHighlightedIndex === highlightedIndex) {
189
+ return;
81
190
  }
191
+
192
+ addSelectedItem(flattenedOptions[newHighlightedIndex]);
193
+ } // backspace removing the last selected item (when popup is closed)
194
+
195
+
196
+ if (!isOpen && event.key === 'Backspace') {
197
+ removeSelectedItem(selectedItems[selectedItems.length - 1]);
198
+ } // delete removing all selected items (when popup is closed)
199
+
200
+
201
+ if (!isOpen && event.key === 'Delete') {
202
+ reset();
82
203
  }
204
+ },
205
+ // fix screen-reader focus on an opened popup with an empty list
206
+ ...(isOpen && isListBoxEmpty ? {
207
+ 'aria-activedescendant': menuProps.id
208
+ } : {})
209
+ }); // recalculate the popup position after a possible trigger height change
210
+
211
+ useEffect(() => {
212
+ if (dropdownRef.current) {
213
+ dropdownRef.current.scheduleUpdate();
83
214
  }
84
- }, state, triggerRef);
85
- const {
86
- buttonProps
87
- } = useButton({ ...triggerProps,
88
- elementType: 'div'
89
- }, triggerRef);
90
- const isDisabled = ariaProps.isDisabled;
91
- const isOpen = state.isOpen;
215
+ }, [selectedItems]); // use it instead of ":has(:focus-visible), :focus-visible"
216
+
92
217
  const {
93
218
  isFocusVisible,
94
219
  focusProps
@@ -99,14 +224,21 @@ function MultiselectInner(_ref) {
99
224
  isFocusedByLabel,
100
225
  labelClickProps,
101
226
  triggerFocusProps: labelFocusForTriggerProps
102
- } = useLabelFocus(triggerRef);
227
+ } = useLabelFocus();
103
228
  const emptyListMessage = getEmptyMessage({
104
229
  isLoading,
105
230
  isNothingFound: false,
106
231
  noOptionsMessage
107
232
  });
233
+ const listBoxState = {
234
+ selectedMap,
235
+ selectionMode: 'multiple',
236
+ highlightedIndex,
237
+ getItemProps,
238
+ menuProps
239
+ };
108
240
  return /*#__PURE__*/React.createElement("div", {
109
- className: cn(className, styles.root, THEME_STYLES[theme], SIZE_STYLES[size], MODE_STYLES[mode], isDisabled ? styles.disabled : styles.enabled, Boolean(ariaProps.errorMessage) && styles.error),
241
+ className: cn(className, styles.root, THEME_STYLES[theme], SIZE_STYLES[size], MODE_STYLES[mode], isDisabled ? styles.disabled : styles.enabled, Boolean(error) && styles.error),
110
242
  id: id,
111
243
  style: style,
112
244
  "data-test": "multiselect"
@@ -114,126 +246,72 @@ function MultiselectInner(_ref) {
114
246
  theme: theme,
115
247
  size: size,
116
248
  disabled: isDisabled,
117
- label: ariaProps.label,
118
- labelProps: mergeProps(labelClickProps, labelProps),
249
+ label: label,
250
+ labelProps: mergeProps(getLabelProps(), labelClickProps),
119
251
  boldLabel: boldLabel,
120
- error: ariaProps.errorMessage,
252
+ error: error,
121
253
  reserveErrorLine: reserveErrorLine,
122
254
  errorMessageProps: errorMessageProps,
123
- note: ariaProps.description,
255
+ note: note,
124
256
  descriptionProps: descriptionProps
125
- }, /*#__PURE__*/React.createElement(HiddenSelect, { ...hiddenSelectProps
126
- }), /*#__PURE__*/React.createElement("div", { ...mergeProps(buttonProps, focusProps, labelFocusForTriggerProps),
127
- ref: triggerRef,
128
- className: cn(styles.trigger, !isSingleLine && styles.canBeMultiline),
129
- "data-open": isOpen,
130
- "data-focus-visible": isFocusVisible,
131
- "data-label-focused": isFocusedByLabel,
132
- "data-test": "multiselect__trigger"
133
- }, /*#__PURE__*/React.createElement(MultiTrigger, {
134
- state: state,
135
- valueProps: valueProps,
136
- isSingleLine: isSingleLine,
137
- isDisabled: isDisabled,
138
- placeholder: ariaProps.placeholder,
139
- triggerRef: triggerRef
140
- }), /*#__PURE__*/React.createElement(Icons, {
141
- isLoading: isLoading,
142
- canHaveClearIcon: isClearable && !isDisabled && hasValue,
143
- onClear: () => {
144
- var _triggerRef$current;
145
-
146
- state.setValue([]);
147
- (_triggerRef$current = triggerRef.current) === null || _triggerRef$current === void 0 || _triggerRef$current.focus();
148
- },
149
- triggerId: triggerProps.id
150
- })), /*#__PURE__*/React.createElement(PopupContent, {
257
+ }, /*#__PURE__*/React.createElement(HiddenSelect, {
258
+ name: name,
259
+ value: selectedItems,
260
+ isMultiple: true,
261
+ options: flattenedOptions
262
+ }), /*#__PURE__*/React.createElement(Dropdown, {
263
+ mode: mode,
151
264
  theme: theme,
152
- state: state,
153
- triggerRef: triggerRef,
265
+ ref: dropdownRef,
154
266
  placement: placement,
155
- size: size,
156
- mode: mode,
157
- listBoxProps: listBoxProps,
158
- emptyListMessage: emptyListMessage,
159
- maxVisibleItems: maxVisibleItems,
160
- selectionMode: "multiple"
161
- })));
162
- }
267
+ maxWidth: "trigger",
268
+ minWidth: "trigger",
269
+ isOpen: isOpen,
270
+ cancelCloseAnimation: true,
271
+ disableFocusLock: true,
272
+ trigger: _ref3 => {
273
+ let {
274
+ ref: triggerRefForDropdown
275
+ } = _ref3;
276
+ return /*#__PURE__*/React.createElement("div", { ...rest,
277
+ ...mergeProps(fieldProps, // aria-invalid & aria-describedby, rest are overridden by triggerProps
278
+ triggerProps, focusProps, labelFocusForTriggerProps, {
279
+ ref: triggerRefForDropdown
280
+ }),
281
+ className: cn(styles.trigger, !isSingleLine && styles.canBeMultiline),
282
+ "data-open": isOpen,
283
+ "data-has-focus-visible": isFocusVisible,
284
+ "data-label-focused": isFocusedByLabel,
285
+ "data-test": "multiselect__trigger"
286
+ }, /*#__PURE__*/React.createElement(MultiTrigger, {
287
+ selectedItems: selectedItems,
288
+ valueProps: {},
289
+ isSingleLine: isSingleLine,
290
+ isDisabled: isDisabled,
291
+ placeholder: placeholder,
292
+ triggerRef: triggerRef,
293
+ removeSelectedItem: removeSelectedItem
294
+ }), /*#__PURE__*/React.createElement(Icons, {
295
+ isLoading: isLoading,
296
+ canHaveClearIcon: isClearable && !isDisabled && selectedItems.length > 0,
297
+ onClear: () => {
298
+ var _triggerRef$current;
163
299
 
164
- function Multiselect(_ref2) {
165
- let {
166
- options = [],
167
- value,
168
- onChange,
169
- error,
170
- note,
171
- size = 'm',
172
- mode = 'classic',
173
- theme: themeFromProps,
174
- placement = 'bottom',
175
- isSingleLine = false,
176
- boldLabel = true,
177
- reserveErrorLine = false,
178
- maxVisibleItems = 5,
179
- isClearable = false,
180
- isLoading = false,
181
- isDisabled = false,
182
- ...props
183
- } = _ref2;
184
- const theme = useThemeWithUndefined(themeFromProps);
185
- const {
186
- selectedKeys,
187
- findOptionsByKeys,
188
- virtualOptions
189
- } = useMultiOptionSerialization(options, value);
190
-
191
- const handleSelectionChange = keys => {
192
- const newValue = findOptionsByKeys(keys);
193
- onChange === null || onChange === void 0 || onChange(newValue);
194
- };
195
-
196
- return /*#__PURE__*/React.createElement(MultiselectInner, { ...props,
300
+ reset();
301
+ (_triggerRef$current = triggerRef.current) === null || _triggerRef$current === void 0 || _triggerRef$current.focus({
302
+ focusVisible: false
303
+ });
304
+ },
305
+ triggerId: triggerProps.id
306
+ }));
307
+ }
308
+ }, /*#__PURE__*/React.createElement(ListBox, {
309
+ state: listBoxState,
197
310
  size: size,
198
- mode: mode,
199
- theme: theme,
200
- value: selectedKeys,
201
- onChange: handleSelectionChange,
202
- errorMessage: error,
203
- description: note,
204
- placement: convertPlacement(placement),
205
- isSingleLine: isSingleLine,
206
- disabledKeys: virtualOptions.map(option => getOptionValue(option)),
207
- boldLabel: boldLabel,
208
- reserveErrorLine: reserveErrorLine,
209
311
  maxVisibleItems: maxVisibleItems,
210
- isClearable: isClearable,
211
- isLoading: isLoading,
212
- isDisabled: isDisabled
213
- }, convertOptions(options, virtualOptions));
312
+ emptyListMessage: emptyListMessage,
313
+ items: listBoxItems
314
+ }))));
214
315
  }
215
316
 
216
- Multiselect.propTypes = {
217
- className: PropTypes.string,
218
- style: PropTypes.object,
219
- options: PropTypes.arrayOf(PropTypes.oneOfType([optionType, optionsGroupType])),
220
- value: PropTypes.arrayOf(optionType),
221
- onChange: PropTypes.func,
222
- size: PropTypes.oneOf(['m', 's', 'l']),
223
- mode: PropTypes.oneOf(['classic', 'rock']),
224
- theme: PropTypes.oneOf(['light', 'dark']),
225
- placeholder: PropTypes.string,
226
- isSingleLine: PropTypes.bool,
227
- isClearable: PropTypes.bool,
228
- isDisabled: PropTypes.bool,
229
- isLoading: PropTypes.bool,
230
- error: ErrorMessage.propTypes.children,
231
- label: PropTypes.node,
232
- note: PropTypes.node,
233
- id: PropTypes.string,
234
- noOptionsMessage: noOptionsMessageType,
235
- placement: PropTypes.oneOf(['auto-start', 'auto', 'auto-end', 'top-start', 'top', 'top-end', 'right-start', 'right', 'right-end', 'bottom-end', 'bottom', 'bottom-start', 'left-end', 'left', 'left-start']),
236
- name: PropTypes.string,
237
- boldLabel: PropTypes.bool
238
- };
239
317
  export { Multiselect };
@@ -1,17 +1,18 @@
1
1
  "use client";
2
- import React, { useState } from 'react';
2
+ import React from 'react';
3
3
  import PropTypes from 'prop-types';
4
4
  import cn from 'classnames';
5
5
  import { useThemeWithUndefined } from '@rescui/ui-contexts';
6
6
  import { ErrorMessage } from '@rescui/form-field';
7
- import { useField, useFocusRing, mergeProps } from 'react-aria';
7
+ import { useFocus, useHover } from 'react-aria';
8
8
  import { nativeOptionType, nativeOptionsGroupType, noOptionsMessageType } from './common-prop-types.js';
9
+ import { useA11y } from './hooks/use-a11y.js';
9
10
  import { FormTriggerWrapper } from './parts/form-trigger-wrapper.js';
10
- import { isOptionAGroup } from './utils/is-option-a-group.js';
11
- import { getEmptyMessage } from './utils/get-empty-message.js';
12
- import { useLabelFocus } from './hooks/use-label-focus.js';
13
11
  import { NativeSelectTrigger } from './parts/native-select-trigger.js';
14
12
  import { NativeSelectOptionsWithGroups } from './parts/native-select-options-with-groups.js';
13
+ import { isOptionAGroup } from './utils/is-option-a-group.js';
14
+ import { getEmptyMessage } from './utils/get-empty-message.js';
15
+ import { mergeProps } from './utils/merge-props.js';
15
16
  import styles from './styles/native-select.p.module.css.js';
16
17
 
17
18
  function findSelectedOption(options, selectedValue) {
@@ -55,6 +56,7 @@ const NativeSelect = _ref => {
55
56
  placeholder,
56
57
  isLoading = false,
57
58
  isDisabled = false,
59
+ isClearable = false,
58
60
  trigger,
59
61
  label,
60
62
  note,
@@ -63,20 +65,25 @@ const NativeSelect = _ref => {
63
65
  noOptionsMessage,
64
66
  name,
65
67
  boldLabel = true,
66
- ...ariaRestProps
68
+ ...rest
67
69
  } = _ref;
70
+ const triggerRef = React.useRef(null);
68
71
  const {
69
72
  labelProps,
70
73
  fieldProps,
71
- descriptionProps,
72
- errorMessageProps
73
- } = useField({ ...ariaRestProps,
74
- label,
74
+ errorMessageProps,
75
+ descriptionProps
76
+ } = useA11y({
77
+ prefix: 'native-select',
78
+ error,
75
79
  description: note,
76
- errorMessage: error
80
+ label,
81
+ fieldRef: triggerRef
77
82
  });
83
+
84
+ const reset = () => onChange(null);
85
+
78
86
  const theme = useThemeWithUndefined(themeFromProps);
79
- const [isHovered, setIsHovered] = useState(false);
80
87
 
81
88
  const handleChange = event => {
82
89
  const newValue = event.target.value;
@@ -85,21 +92,24 @@ const NativeSelect = _ref => {
85
92
  if (onChange) {
86
93
  onChange(selectedOption);
87
94
  }
88
- };
95
+ }; // delete it when triggerFunc is migrated to the css-states instead of js-props for hover/focus
96
+ // replace it with ":focus + &" selector in that case
89
97
 
90
- const triggerRef = React.useRef(null);
98
+
99
+ const [isFocused, setIsFocused] = React.useState(false);
91
100
  const {
92
- isFocusVisible,
93
101
  focusProps
94
- } = useFocusRing({
95
- within: true
96
- });
102
+ } = useFocus({
103
+ isDisabled,
104
+ onFocusChange: setIsFocused
105
+ }); // same as above, could be replaced with ":hover + &" selector
106
+
97
107
  const {
98
- isFocusedByLabel,
99
- labelClickProps,
100
- triggerFocusProps: labelFocusForTriggerProps
101
- } = useLabelFocus(triggerRef);
102
- const isFocused = isFocusedByLabel || isFocusVisible;
108
+ isHovered,
109
+ hoverProps
110
+ } = useHover({
111
+ isDisabled
112
+ });
103
113
  const emptyListMessage = getEmptyMessage({
104
114
  isLoading,
105
115
  isNothingFound: false,
@@ -115,7 +125,7 @@ const NativeSelect = _ref => {
115
125
  size: size,
116
126
  disabled: isDisabled,
117
127
  label: label,
118
- labelProps: mergeProps(labelClickProps, labelProps),
128
+ labelProps: mergeProps(labelProps),
119
129
  boldLabel: boldLabel,
120
130
  error: error,
121
131
  errorMessageProps: errorMessageProps,
@@ -124,25 +134,14 @@ const NativeSelect = _ref => {
124
134
  descriptionProps: descriptionProps
125
135
  }, /*#__PURE__*/React.createElement("div", {
126
136
  className: styles.wrapper
127
- }, trigger ? trigger({
128
- focused: isFocused,
129
- hovered: isHovered,
130
- selectedLabel: value === null || value === void 0 ? void 0 : value.label,
131
- selectedValue: value === null || value === void 0 ? void 0 : value.value,
132
- placeholder
133
- }) : /*#__PURE__*/React.createElement(NativeSelectTrigger, {
134
- value: value,
135
- isLoading: isLoading,
136
- isFocused: isFocused,
137
- isHovered: isHovered,
138
- isDisabled: isDisabled,
139
- size: size,
140
- theme: theme,
141
- hasError: Boolean(error),
142
- placeholder: placeholder
143
- }), /*#__PURE__*/React.createElement("select", { ...mergeProps(fieldProps, focusProps, labelFocusForTriggerProps, {
144
- onMouseEnter: () => setIsHovered(true),
145
- onMouseLeave: () => setIsHovered(false)
137
+ }, /*#__PURE__*/React.createElement("select", { ...rest,
138
+ ...mergeProps(fieldProps, focusProps, hoverProps, {
139
+ onKeyDown: event => {
140
+ // backspace and backspace removing the last selected item (when popup is closed)
141
+ if (isClearable && (event.key === 'Backspace' || event.key === 'Delete')) {
142
+ reset();
143
+ }
144
+ }
146
145
  }),
147
146
  ref: triggerRef,
148
147
  "data-test": "native-select_select",
@@ -156,7 +155,31 @@ const NativeSelect = _ref => {
156
155
  optionsWithGroups: options,
157
156
  placeholder: placeholder,
158
157
  emptyListMessage: emptyListMessage
159
- })))));
158
+ })), trigger ? trigger({
159
+ focused: isFocused,
160
+ hovered: isHovered,
161
+ selectedLabel: value === null || value === void 0 ? void 0 : value.label,
162
+ selectedValue: value === null || value === void 0 ? void 0 : value.value,
163
+ placeholder
164
+ }) : /*#__PURE__*/React.createElement(NativeSelectTrigger, {
165
+ value: value,
166
+ isLoading: isLoading,
167
+ isDisabled: isDisabled,
168
+ isClearable: isClearable,
169
+ triggerId: fieldProps.id,
170
+ onClear: () => {
171
+ var _triggerRef$current;
172
+
173
+ reset();
174
+ (_triggerRef$current = triggerRef.current) === null || _triggerRef$current === void 0 || _triggerRef$current.focus({
175
+ focusVisible: false
176
+ });
177
+ },
178
+ size: size,
179
+ theme: theme,
180
+ hasError: Boolean(error),
181
+ placeholder: placeholder
182
+ }))));
160
183
  };
161
184
 
162
185
  NativeSelect.propTypes = {