@allxsmith/bestax-bulma 5.36.1 → 5.38.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.
Files changed (103) hide show
  1. package/dist/bestax.css +1 -1
  2. package/dist/bestax.css.map +1 -1
  3. package/dist/esm/components/Card.js +2 -2
  4. package/dist/esm/components/Carousel.js +14 -6
  5. package/dist/esm/components/Carousel.js.map +1 -1
  6. package/dist/esm/components/Dropdown.js +7 -2
  7. package/dist/esm/components/Dropdown.js.map +1 -1
  8. package/dist/esm/components/EmptyState.js +2 -2
  9. package/dist/esm/components/Loading.js +7 -1
  10. package/dist/esm/components/Loading.js.map +1 -1
  11. package/dist/esm/components/Panel.js +34 -4
  12. package/dist/esm/components/Panel.js.map +1 -1
  13. package/dist/esm/components/Popover.js +7 -2
  14. package/dist/esm/components/Popover.js.map +1 -1
  15. package/dist/esm/components/Sidebar.js +46 -17
  16. package/dist/esm/components/Sidebar.js.map +1 -1
  17. package/dist/esm/components/Timeline.js +5 -5
  18. package/dist/esm/components/Timeline.js.map +1 -1
  19. package/dist/esm/components/TreeView.js +192 -0
  20. package/dist/esm/components/TreeView.js.map +1 -0
  21. package/dist/esm/form/Autocomplete.js +28 -18
  22. package/dist/esm/form/Autocomplete.js.map +1 -1
  23. package/dist/esm/form/Combobox.js +592 -0
  24. package/dist/esm/form/Combobox.js.map +1 -0
  25. package/dist/esm/form/Field.js +3 -3
  26. package/dist/esm/form/Field.js.map +1 -1
  27. package/dist/esm/form/_pickerInternals/PickerPopover.js +10 -5
  28. package/dist/esm/form/_pickerInternals/PickerPopover.js.map +1 -1
  29. package/dist/esm/form/useAutoLabelId.js.map +1 -1
  30. package/dist/esm/helpers/Theme.js.map +1 -1
  31. package/dist/esm/helpers/typeahead.js +82 -0
  32. package/dist/esm/helpers/typeahead.js.map +1 -0
  33. package/dist/esm/helpers/useAnchoredPosition.js +35 -6
  34. package/dist/esm/helpers/useAnchoredPosition.js.map +1 -1
  35. package/dist/esm/helpers/useComboboxEngine.js +331 -0
  36. package/dist/esm/helpers/useComboboxEngine.js.map +1 -0
  37. package/dist/esm/helpers/useFocusTrap.js +87 -49
  38. package/dist/esm/helpers/useFocusTrap.js.map +1 -1
  39. package/dist/esm/helpers/useInertSiblings.js +106 -0
  40. package/dist/esm/helpers/useInertSiblings.js.map +1 -0
  41. package/dist/esm/helpers/useMenuKeys.js +8 -58
  42. package/dist/esm/helpers/useMenuKeys.js.map +1 -1
  43. package/dist/esm/helpers/useTreeView.js +380 -0
  44. package/dist/esm/helpers/useTreeView.js.map +1 -0
  45. package/dist/esm/layout/Container.js +2 -2
  46. package/dist/esm/layout/Container.js.map +1 -1
  47. package/dist/esm/layout/Hero.js +6 -6
  48. package/dist/esm/layout/Hero.js.map +1 -1
  49. package/dist/extras.css +1 -1
  50. package/dist/extras.css.map +1 -1
  51. package/dist/index.cjs +3462 -1732
  52. package/dist/index.cjs.map +1 -1
  53. package/dist/index.esm.js +2 -0
  54. package/dist/index.esm.js.map +1 -1
  55. package/dist/types/components/Loading.d.ts +16 -2
  56. package/dist/types/components/Panel.d.ts +45 -5
  57. package/dist/types/components/Sidebar.d.ts +17 -2
  58. package/dist/types/components/TreeView.d.ts +115 -0
  59. package/dist/types/form/Combobox.d.ts +416 -0
  60. package/dist/types/form/Field.d.ts +5 -5
  61. package/dist/types/form/useAutoLabelId.d.ts +3 -2
  62. package/dist/types/helpers/Theme.d.ts +6 -0
  63. package/dist/types/helpers/typeahead.d.ts +41 -0
  64. package/dist/types/helpers/useAnchoredPosition.d.ts +25 -2
  65. package/dist/types/helpers/useComboboxEngine.d.ts +111 -0
  66. package/dist/types/helpers/useFocusTrap.d.ts +15 -2
  67. package/dist/types/helpers/useInertSiblings.d.ts +30 -0
  68. package/dist/types/helpers/useMenuKeys.d.ts +1 -2
  69. package/dist/types/helpers/useTreeView.d.ts +88 -0
  70. package/dist/types/index.d.ts +2 -0
  71. package/dist/types/layout/Container.d.ts +9 -1
  72. package/dist/types/layout/Hero.d.ts +27 -2
  73. package/dist/types-cjs/components/Loading.d.ts +16 -2
  74. package/dist/types-cjs/components/Panel.d.ts +45 -5
  75. package/dist/types-cjs/components/Sidebar.d.ts +17 -2
  76. package/dist/types-cjs/components/TreeView.d.ts +115 -0
  77. package/dist/types-cjs/form/Combobox.d.ts +416 -0
  78. package/dist/types-cjs/form/Field.d.ts +5 -5
  79. package/dist/types-cjs/form/useAutoLabelId.d.ts +3 -2
  80. package/dist/types-cjs/helpers/Theme.d.ts +6 -0
  81. package/dist/types-cjs/helpers/typeahead.d.ts +41 -0
  82. package/dist/types-cjs/helpers/useAnchoredPosition.d.ts +25 -2
  83. package/dist/types-cjs/helpers/useComboboxEngine.d.ts +111 -0
  84. package/dist/types-cjs/helpers/useFocusTrap.d.ts +15 -2
  85. package/dist/types-cjs/helpers/useInertSiblings.d.ts +30 -0
  86. package/dist/types-cjs/helpers/useMenuKeys.d.ts +1 -2
  87. package/dist/types-cjs/helpers/useTreeView.d.ts +88 -0
  88. package/dist/types-cjs/index.d.ts +2 -0
  89. package/dist/types-cjs/layout/Container.d.ts +9 -1
  90. package/dist/types-cjs/layout/Hero.d.ts +27 -2
  91. package/dist/versions/bestax-no-dark-mode.css +1 -1
  92. package/dist/versions/bestax-no-dark-mode.css.map +1 -1
  93. package/dist/versions/bestax-no-helpers-prefixed.css +1 -1
  94. package/dist/versions/bestax-no-helpers-prefixed.css.map +1 -1
  95. package/dist/versions/bestax-no-helpers.css +1 -1
  96. package/dist/versions/bestax-no-helpers.css.map +1 -1
  97. package/dist/versions/bestax-prefixed.css +1 -1
  98. package/dist/versions/bestax-prefixed.css.map +1 -1
  99. package/package.json +1 -1
  100. package/src/scss/components/_index.scss +1 -0
  101. package/src/scss/components/_treeview.scss +252 -0
  102. package/src/scss/form/_combobox.scss +323 -0
  103. package/src/scss/form/_index.scss +1 -0
@@ -0,0 +1,592 @@
1
+ /* @allxsmith/bestax-bulma — AI agents: see AGENTS.md in the package root, or https://bestax.io/llms.txt */
2
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
3
+ import { createContext, forwardRef, useState, useRef, useCallback, useId, useContext } from 'react';
4
+ import { usePrefixedClassNames, classNames } from '../helpers/classNames.js';
5
+ import { useBulmaClasses } from '../helpers/useBulmaClasses.js';
6
+ import { withSubComponents } from '../helpers/withSubComponents.js';
7
+ import { Portal } from '../helpers/portal.js';
8
+ import { useIsHydrated } from '../helpers/useIsHydrated.js';
9
+ import { useClientLayoutEffect } from '../helpers/useClientLayoutEffect.js';
10
+ import { flattenChildren } from '../helpers/flattenChildren.js';
11
+ import { warnOnce } from '../helpers/devWarnings.js';
12
+ import { useAnchoredPosition, marginGap } from '../helpers/useAnchoredPosition.js';
13
+ import { spokenText, StatusRegion } from '../helpers/statusRegion.js';
14
+ import { useComboboxEngine } from '../helpers/useComboboxEngine.js';
15
+ import { Delete } from '../elements/Delete.js';
16
+ import { Tag } from '../elements/Tag.js';
17
+ import { Tags } from '../elements/Tags.js';
18
+ import { Field } from './Field.js';
19
+ import { Input } from './Input.js';
20
+ import { useInsideField, useInsideControl, rendersOwnField } from './FormContext.js';
21
+ import { useAutoLabelId } from './useAutoLabelId.js';
22
+ import { useControlLoading } from './controlLoading.js';
23
+
24
+ const validComboboxSizes = ['small', 'medium', 'large'];
25
+ // What the status region announces: the `Combobox.Empty` content, as the one
26
+ // item it's handed while that content is on screen because of the user's
27
+ // typing.
28
+ const typedNoMatches = [{ id: 'empty' }];
29
+ const nothingToAnnounce = [];
30
+ const noop = () => { };
31
+ const registerNothing = () => noop;
32
+ // What a part rendered outside a Combobox reads: closed, with nothing to
33
+ // report to, so it renders its markup and does nothing.
34
+ const standalone = {
35
+ engine: {
36
+ open: false,
37
+ expanded: false,
38
+ highlighted: null,
39
+ typed: false,
40
+ listboxId: '',
41
+ itemId: () => '',
42
+ rootRef: { current: null },
43
+ inputRef: { current: null },
44
+ listboxRef: { current: null },
45
+ registerPopup: registerNothing,
46
+ registerItem: registerNothing,
47
+ onKeyDown: noop,
48
+ onChange: noop,
49
+ onClick: noop,
50
+ onBlur: noop,
51
+ hover: noop,
52
+ pick: noop,
53
+ keepFocus: noop,
54
+ markInside: noop,
55
+ },
56
+ inputId: undefined,
57
+ reportInputId: noop,
58
+ labelId: undefined,
59
+ ownLabelId: undefined,
60
+ reportLoading: noop,
61
+ reportEmpty: registerNothing,
62
+ onFieldChange: noop,
63
+ reportPanel: registerNothing,
64
+ placement: { position: 'bottom-left' },
65
+ multiple: false,
66
+ inline: false,
67
+ appendToBody: false,
68
+ };
69
+ const ComboboxContext = createContext(null);
70
+ const useCombobox = () => useContext(ComboboxContext) ?? standalone;
71
+ /** Hands a node to a forwarded ref and to one of the component's own. */
72
+ function setRefs(node, own, forwarded) {
73
+ own.current = node;
74
+ if (typeof forwarded === 'function')
75
+ forwarded(node);
76
+ else if (forwarded)
77
+ forwarded.current = node;
78
+ }
79
+ const ComboboxParts = forwardRef(({ children, below, onSettle, open, defaultOpen = false, onOpenChange, onSelect, keepFirst = false, multiple = false, inline = false, position = 'bottom-left', appendToBody = false, size, label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, className, ...props }, ref) => {
80
+ const insideField = useInsideField();
81
+ const insideControl = useInsideControl();
82
+ const ownField = rendersOwnField('Combobox', {
83
+ insideField,
84
+ insideControl,
85
+ label,
86
+ message,
87
+ horizontal,
88
+ fieldClassName,
89
+ });
90
+ // The field's own `id`, when `Combobox.Input` was given one, which the
91
+ // label then points at.
92
+ const [givenInputId, setGivenInputId] = useState();
93
+ const { controlId, fieldLabelProps, labelId: listLabelId, } = useAutoLabelId({
94
+ label,
95
+ id: givenInputId,
96
+ labelProps,
97
+ rendersLabel: ownField,
98
+ needsLabelId: true,
99
+ });
100
+ const engine = useComboboxEngine({
101
+ open,
102
+ defaultOpen,
103
+ onOpenChange,
104
+ onSelect,
105
+ onSettle,
106
+ keepFirst,
107
+ multiple,
108
+ inline,
109
+ // A portaled panel has no server markup to point at.
110
+ deferred: appendToBody && !inline,
111
+ });
112
+ const { bulmaHelperClasses, rest } = useBulmaClasses(props);
113
+ const validSize = validComboboxSizes.includes(size)
114
+ ? size
115
+ : undefined;
116
+ const rootClass = usePrefixedClassNames('combobox', {
117
+ [`is-${validSize}`]: !!validSize,
118
+ 'is-inline': inline,
119
+ });
120
+ const helpClass = usePrefixedClassNames('help', {
121
+ [`is-${messageColor}`]: !!messageColor,
122
+ });
123
+ // A polite status region, in the page from the start so it's there
124
+ // before anything is written into it, announces `Combobox.Empty` when
125
+ // the user's typing leaves it on screen: once each time that happens,
126
+ // not again on each keystroke while it stays. An empty field isn't a
127
+ // search, so it stays quiet then. While the field's `isLoading`, or a
128
+ // loading Control around the Combobox, says results are on their way, it
129
+ // waits, and it's said once loading ends with the message still there.
130
+ const [inputLoading, setInputLoading] = useState(false);
131
+ const controlLoading = useControlLoading();
132
+ const [hasText, setHasText] = useState(false);
133
+ const emptyRef = useRef(null);
134
+ const [emptyShown, setEmptyShown] = useState(false);
135
+ const announced = engine.typed && hasText && emptyShown && !inputLoading && !controlLoading
136
+ ? typedNoMatches
137
+ : nothingToAnnounce;
138
+ // The region reads the text on screen, whatever the message renders.
139
+ const describeEmpty = useCallback(() => {
140
+ const node = emptyRef.current;
141
+ /* istanbul ignore if: it's only announced while it's on screen */
142
+ if (!node)
143
+ return null;
144
+ return spokenText(node);
145
+ }, []);
146
+ const reportEmpty = useCallback((element) => {
147
+ emptyRef.current = element;
148
+ setEmptyShown(true);
149
+ return () => {
150
+ emptyRef.current = null;
151
+ setEmptyShown(false);
152
+ };
153
+ }, []);
154
+ // The menu is placed from here rather than from the part, since this
155
+ // is where the field's element is known by the time the panel mounts.
156
+ const panelRef = useRef(null);
157
+ const [panelShown, setPanelShown] = useState(false);
158
+ const placement = useAnchoredPosition(engine.rootRef, panelRef, {
159
+ active: panelShown,
160
+ position,
161
+ fixed: appendToBody,
162
+ // The gap comes from `--bulma-combobox-menu-offset` as a margin, so
163
+ // the coordinates leave it out and `auto` reads it off the panel.
164
+ offset: 0,
165
+ styledGap: marginGap,
166
+ matchWidth: true,
167
+ // The list filters as the user types, so the menu's height changes.
168
+ followContent: true,
169
+ });
170
+ const { registerPopup } = engine;
171
+ const reportPanel = useCallback((element) => {
172
+ const forget = registerPopup(element);
173
+ panelRef.current = element;
174
+ setPanelShown(true);
175
+ return () => {
176
+ forget();
177
+ panelRef.current = null;
178
+ setPanelShown(false);
179
+ };
180
+ }, [registerPopup]);
181
+ const onFieldChange = (value) => {
182
+ setHasText(value !== '');
183
+ engine.onChange();
184
+ };
185
+ // A new value each render: the engine's state and handlers are, and the
186
+ // parts read the highlight and the open state from it.
187
+ const context = {
188
+ engine,
189
+ inputId: controlId,
190
+ reportInputId: setGivenInputId,
191
+ labelId: listLabelId,
192
+ ownLabelId: label && ownField ? listLabelId : undefined,
193
+ reportLoading: setInputLoading,
194
+ reportEmpty,
195
+ onFieldChange,
196
+ reportPanel,
197
+ placement,
198
+ size: validSize,
199
+ multiple,
200
+ inline,
201
+ appendToBody,
202
+ };
203
+ const { rootRef } = engine;
204
+ const rootRefs = useCallback((node) => setRefs(node, rootRef, ref), [rootRef, ref]);
205
+ const comboboxElement = (jsxs("div", { className: classNames(rootClass, bulmaHelperClasses, className), ...rest, ref: rootRefs, children: [jsx(ComboboxContext.Provider, { value: context, children: children }), jsx(StatusRegion, { items: announced, describe: describeEmpty })] }));
206
+ const fieldElement = below === undefined ? (comboboxElement) : (jsxs("div", { children: [comboboxElement, jsx("div", { onPointerDownCapture: engine.markInside, onMouseDown: engine.keepFocus, children: below })] }));
207
+ const messageEl = message ? jsx("p", { className: helpClass, children: message }) : null;
208
+ if (ownField) {
209
+ return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [fieldElement, messageEl] }));
210
+ }
211
+ return (jsxs(Fragment, { children: [fieldElement, messageEl] }));
212
+ });
213
+ ComboboxParts.displayName = 'ComboboxParts';
214
+ /**
215
+ * The text field, with the combobox role and the keys, rendered as an `Input`.
216
+ *
217
+ * @function
218
+ * @param {ComboboxInputProps} props - Props for the Combobox.Input component.
219
+ * @param {React.Ref<HTMLInputElement>} ref - Forwarded ref to the `<input>`.
220
+ * @returns {JSX.Element} The field in its Control.
221
+ */
222
+ const ComboboxInput = forwardRef(({ id, isLoading, controlSize, onKeyDown, onChange, onClick, onBlur, 'aria-autocomplete': ariaAutocomplete, ...props }, ref) => {
223
+ const context = useCombobox();
224
+ const { engine, reportInputId, reportLoading } = context;
225
+ // Inside a caller's Control the field renders none of its own, so the
226
+ // Combobox's size isn't handed to one there.
227
+ const insideControl = useInsideControl();
228
+ useClientLayoutEffect(() => {
229
+ reportInputId(id);
230
+ return () => reportInputId(undefined);
231
+ }, [reportInputId, id]);
232
+ useClientLayoutEffect(() => {
233
+ reportLoading(!!isLoading);
234
+ return () => reportLoading(false);
235
+ }, [reportLoading, isLoading]);
236
+ const { inputRef } = engine;
237
+ const inputRefs = useCallback((node) => setRefs(node, inputRef, ref), [inputRef, ref]);
238
+ const { expanded, highlighted } = engine;
239
+ // An `id` of the field's own reaches the label from a layout effect, so
240
+ // in server markup, and the first render, the label's `for` still names
241
+ // the generated id. Until then the field points at the label itself,
242
+ // unless the caller named it.
243
+ const callerNamed = !!props['aria-label'] || 'aria-labelledby' in props;
244
+ const labelledBy = callerNamed
245
+ ? props['aria-labelledby']
246
+ : id !== undefined && id !== context.inputId
247
+ ? context.ownLabelId
248
+ : undefined;
249
+ return (jsx(Input, { ...props, ref: inputRefs, id: id ?? context.inputId, "aria-labelledby": labelledBy, isLoading: isLoading, size: context.size, controlSize: controlSize ?? (insideControl ? undefined : context.size), role: "combobox", "aria-autocomplete": ariaAutocomplete ?? 'list', "aria-haspopup": "listbox", "aria-expanded": expanded, "aria-controls": expanded ? engine.listboxId : undefined, "aria-activedescendant": expanded && highlighted !== null
250
+ ? engine.itemId(highlighted)
251
+ : undefined, autoComplete: "off", onKeyDown: event => {
252
+ onKeyDown?.(event);
253
+ engine.onKeyDown(event);
254
+ }, onChange: event => {
255
+ onChange?.(event);
256
+ context.onFieldChange(event.target.value);
257
+ }, onClick: event => {
258
+ onClick?.(event);
259
+ if (!event.defaultPrevented)
260
+ engine.onClick();
261
+ }, onBlur: event => {
262
+ onBlur?.(event);
263
+ engine.onBlur(event);
264
+ } }));
265
+ });
266
+ ComboboxInput.displayName = 'ComboboxInput';
267
+ /**
268
+ * The floating panel under the field, on screen while the list is open and its children aren't all empty.
269
+ *
270
+ * @function
271
+ * @param {ComboboxMenuProps} props - Props for the Combobox.Menu component.
272
+ * @returns {JSX.Element | null} The panel, or nothing while the list is closed or its children are all empty.
273
+ */
274
+ const ComboboxMenu = ({ children, className, style, onMouseDown, onPointerDownCapture, ...props }) => {
275
+ const { engine, inline, appendToBody, placement, reportPanel, size } = useCombobox();
276
+ // Children that are all `null`, `false` or `''` leave no panel, whatever
277
+ // `open` says. This reads the elements it's handed, not what they render.
278
+ const hasContent = flattenChildren(children).some(child => child !== '');
279
+ const shown = engine.open && !inline && hasContent;
280
+ // A portaled panel has no server markup, so it only exists from the
281
+ // commit after hydration. Everything that measures it waits for that.
282
+ const hydrated = useIsHydrated();
283
+ const mounted = shown && (hydrated || !appendToBody);
284
+ const panelRef = useRef(null);
285
+ useClientLayoutEffect(() => {
286
+ const panel = panelRef.current;
287
+ return mounted && panel ? reportPanel(panel) : undefined;
288
+ }, [mounted, reportPanel]);
289
+ const menuClass = usePrefixedClassNames('dropdown-menu', 'combobox-menu', {
290
+ [`is-${placement.position}`]: true,
291
+ 'is-portal': appendToBody,
292
+ [`is-${size}`]: !!size,
293
+ });
294
+ const contentClass = usePrefixedClassNames('dropdown-content');
295
+ // Inline, the list is in the flow already.
296
+ if (inline)
297
+ return jsx(Fragment, { children: children });
298
+ if (!shown)
299
+ return null;
300
+ return (jsx(Portal, { disabled: !appendToBody, children: jsx("div", { ...props, ref: panelRef, className: classNames(menuClass, className), style: appendToBody
301
+ ? {
302
+ ...style,
303
+ position: 'fixed',
304
+ top: placement.top,
305
+ left: placement.left,
306
+ minWidth: placement.minWidth,
307
+ }
308
+ : style, onMouseDown: event => {
309
+ onMouseDown?.(event);
310
+ engine.keepFocus(event);
311
+ }, onPointerDownCapture: event => {
312
+ onPointerDownCapture?.(event);
313
+ engine.markInside(event);
314
+ }, children: jsx("div", { className: contentClass, children: children }) }) }));
315
+ };
316
+ ComboboxMenu.displayName = 'ComboboxMenu';
317
+ /**
318
+ * The list of items, a `listbox` named by the label that names the field.
319
+ *
320
+ * @function
321
+ * @param {ComboboxListProps} props - Props for the Combobox.List component.
322
+ * @returns {JSX.Element} The listbox.
323
+ */
324
+ const ComboboxList = ({ children, className, onMouseDown, ...props }) => {
325
+ const { engine, labelId, multiple } = useCombobox();
326
+ const { listboxRef, listboxId, keepFocus } = engine;
327
+ const listClass = usePrefixedClassNames('combobox-list');
328
+ // A name the caller gave the list wins over the label's.
329
+ const callerNamed = !!props['aria-label'] || 'aria-labelledby' in props;
330
+ return (jsx("div", { className: classNames(listClass, className), ...props, ref: listboxRef, id: listboxId || undefined, role: "listbox", "aria-labelledby": callerNamed ? props['aria-labelledby'] : labelId, "aria-label": callerNamed ? props['aria-label'] : labelId ? undefined : 'Suggestions', "aria-multiselectable": multiple || undefined, onMouseDown: event => {
331
+ onMouseDown?.(event);
332
+ keepFocus(event);
333
+ }, children: children }));
334
+ };
335
+ ComboboxList.displayName = 'ComboboxList';
336
+ /**
337
+ * One choice in the list, with a check mark while it's `checked`.
338
+ *
339
+ * @function
340
+ * @param {ComboboxItemProps} props - Props for the Combobox.Item component.
341
+ * @returns {JSX.Element} The item.
342
+ */
343
+ const ComboboxItem = ({ value, disabled = false, checked = false, children, className, onClick, onPointerMove, ...props }) => {
344
+ const { engine, multiple } = useCombobox();
345
+ const itemRef = useRef(null);
346
+ const { registerItem } = engine;
347
+ useClientLayoutEffect(() => registerItem(itemRef.current, {
348
+ value,
349
+ disabled,
350
+ checked,
351
+ }), [registerItem, value, disabled, checked]);
352
+ const highlighted = engine.highlighted === value;
353
+ const itemClass = usePrefixedClassNames('dropdown-item', 'combobox-item', {
354
+ 'is-active': highlighted,
355
+ 'is-checked': checked,
356
+ 'is-disabled': disabled,
357
+ });
358
+ const checkClass = usePrefixedClassNames('combobox-check');
359
+ // One value: the highlighted option, as the ARIA Authoring Practices'
360
+ // combobox has it, and no attribute on the rest. Several: each option says
361
+ // whether it's chosen. Never a disabled option.
362
+ const selected = disabled
363
+ ? undefined
364
+ : multiple
365
+ ? checked
366
+ : highlighted || undefined;
367
+ return (jsxs("div", { className: classNames(itemClass, className), ...props, ref: itemRef, id: engine.itemId(value) || undefined, role: "option", "aria-selected": selected, "aria-disabled": disabled || undefined, onClick: event => {
368
+ onClick?.(event);
369
+ if (!disabled && !event.defaultPrevented)
370
+ engine.pick(value);
371
+ }, onPointerMove: event => {
372
+ onPointerMove?.(event);
373
+ if (!disabled && !highlighted)
374
+ engine.hover(value);
375
+ }, children: [checked && (jsx("span", { className: checkClass, "aria-hidden": "true", children: jsx("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", width: "1em", height: "1em", focusable: "false", children: jsx("polyline", { points: "3 8.5 6.5 12 13 4.5" }) }) })), children] }));
376
+ };
377
+ ComboboxItem.displayName = 'ComboboxItem';
378
+ /**
379
+ * Items under a heading, a `group` named by it.
380
+ *
381
+ * @function
382
+ * @param {ComboboxGroupProps} props - Props for the Combobox.Group component.
383
+ * @returns {JSX.Element} The group and its heading.
384
+ */
385
+ const ComboboxGroup = ({ label, children, className, ...props }) => {
386
+ const labelId = useId();
387
+ const groupClass = usePrefixedClassNames('combobox-group');
388
+ const labelClass = usePrefixedClassNames('combobox-group-label');
389
+ // A name the caller gave the group wins over its heading's, as on the list.
390
+ const callerNamed = !!props['aria-label'] || 'aria-labelledby' in props;
391
+ return (jsxs("div", { className: classNames(groupClass, className), ...props, role: "group", "aria-labelledby": callerNamed ? props['aria-labelledby'] : labelId, children: [jsx("div", { className: labelClass, id: labelId, children: label }), children] }));
392
+ };
393
+ ComboboxGroup.displayName = 'ComboboxGroup';
394
+ /**
395
+ * The message shown in place of the list when nothing matches, announced when typing brings it on screen.
396
+ *
397
+ * @function
398
+ * @param {ComboboxEmptyProps} props - Props for the Combobox.Empty component.
399
+ * @returns {JSX.Element} The message.
400
+ */
401
+ const ComboboxEmpty = ({ children, className, onMouseDown, ...props }) => {
402
+ const { engine, reportEmpty } = useCombobox();
403
+ const emptyRef = useRef(null);
404
+ useClientLayoutEffect(() => reportEmpty(emptyRef.current), [reportEmpty]);
405
+ const emptyClass = usePrefixedClassNames('combobox-empty');
406
+ return (jsx("div", { className: classNames(emptyClass, className), ...props, ref: emptyRef, onMouseDown: event => {
407
+ onMouseDown?.(event);
408
+ engine.keepFocus(event);
409
+ }, children: children }));
410
+ };
411
+ ComboboxEmpty.displayName = 'ComboboxEmpty';
412
+ /** An entry in `data` as an item: a string is its own label. */
413
+ const toItem = (entry) => typeof entry === 'string' ? { value: entry, label: entry } : entry;
414
+ const labelOf = (item) => item.label ?? item.value;
415
+ /** One value or several, as a list: one value is a list of none or one. */
416
+ const asList = (value) => value == null ? [] : Array.isArray(value) ? value : [value];
417
+ /**
418
+ * The Combobox given `data`. It renders the parts itself, filters the items
419
+ * by label as the user types, and keeps the value, so a pick goes through
420
+ * the same parts, keys and ARIA as one built by hand.
421
+ */
422
+ const ComboboxFromData = forwardRef((props, ref) => {
423
+ const { data, value, defaultValue,
424
+ // Called below through `props`, which knows which value it takes.
425
+ onChange: _onChange, multiple = false, placeholder, name, empty, itemTemplate, onSelect, onOpenChange, inline = false, disabled = false, required = false, form, id, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, ariaCloseLabel, children, ...rest } = props;
426
+ // A name given in place of a label is the field's and the list's, not
427
+ // the wrapper's. Only the ones given are passed, since an
428
+ // `aria-labelledby` that's there at all, even undefined, keeps the
429
+ // label's name off the field.
430
+ const named = {
431
+ ...(ariaLabel !== undefined && { 'aria-label': ariaLabel }),
432
+ ...(ariaLabelledBy !== undefined && {
433
+ 'aria-labelledby': ariaLabelledBy,
434
+ }),
435
+ };
436
+ if (flattenChildren(children).length > 0) {
437
+ warnOnce('Combobox:data-children', '[bestax-bulma] <Combobox data> renders its own field, menu and ' +
438
+ 'items, so its children are ignored. Give it data or build it ' +
439
+ 'from parts, not both.');
440
+ }
441
+ const items = data.map(toItem);
442
+ const labelFor = (picked) => {
443
+ const item = items.find(entry => entry.value === picked);
444
+ return item ? labelOf(item) : picked;
445
+ };
446
+ const controlled = value !== undefined;
447
+ const [ownValue, setOwnValue] = useState(() => asList(defaultValue));
448
+ const picked = controlled ? asList(value) : ownValue;
449
+ const change = (next) => {
450
+ if (!controlled)
451
+ setOwnValue(next);
452
+ if (props.multiple === true)
453
+ props.onChange?.(next);
454
+ else
455
+ props.onChange?.(next[0] ?? null);
456
+ };
457
+ // What's typed, or `null` while the field shows the value instead. The
458
+ // ref holds it as it is now, since a pick sets it and the list's closing
459
+ // reads it in the same event, before a render.
460
+ const [query, setQueryState] = useState(null);
461
+ const queryRef = useRef(null);
462
+ const setQuery = (next) => {
463
+ queryRef.current = next;
464
+ setQueryState(next);
465
+ };
466
+ // Once the list closes or the field is left, the field shows the value
467
+ // again: typed text goes back to the pick, and with one value a field
468
+ // left empty clears it.
469
+ const settle = () => {
470
+ if (!multiple && queryRef.current === '' && picked.length > 0) {
471
+ change([]);
472
+ }
473
+ setQuery(null);
474
+ };
475
+ const term = (query ?? '').trim().toLowerCase();
476
+ const shown = items.filter(item => labelOf(item).toLowerCase().includes(term));
477
+ const inputRef = useRef(null);
478
+ const select = (pick) => {
479
+ onSelect?.(pick);
480
+ if (multiple) {
481
+ change(picked.includes(pick)
482
+ ? picked.filter(v => v !== pick)
483
+ : [...picked, pick]);
484
+ }
485
+ else {
486
+ if (picked[0] !== pick)
487
+ change([pick]);
488
+ setQuery(null);
489
+ }
490
+ };
491
+ const remove = (pick) => {
492
+ change(picked.filter(v => v !== pick));
493
+ inputRef.current?.focus();
494
+ };
495
+ // The picks, for the field's description, since its text is the search.
496
+ const tagsId = useId();
497
+ const tagIds = picked.map((_, index) => `${tagsId}-${index}`);
498
+ // Each group where `data` first names it, and the items with none
499
+ // together where the first of them is.
500
+ const sections = new Map();
501
+ for (const item of shown) {
502
+ const group = item.group ?? '';
503
+ sections.set(group, [...(sections.get(group) ?? []), item]);
504
+ }
505
+ const renderItem = (item) => (jsx(ComboboxItem, { value: item.value, disabled: disabled || item.disabled, checked: picked.includes(item.value), children: itemTemplate ? itemTemplate(item) : labelOf(item) }, item.value));
506
+ const list = shown.length > 0 ? (jsx(ComboboxList, { ...named, children: Array.from(sections, ([group, members]) => group ? (jsx(ComboboxGroup, { label: group, children: members.map(renderItem) }, group)) : (members.map(renderItem))) })) : empty ? (jsx(ComboboxEmpty, { children: empty })) : null;
507
+ return (jsxs(ComboboxParts, { ...rest, ref: ref, multiple: multiple, inline: inline, onSelect: select, onSettle: settle, onOpenChange: next => {
508
+ if (!next)
509
+ settle();
510
+ onOpenChange?.(next);
511
+ }, below: multiple
512
+ ? picked.length > 0 && (jsx(Tags, { mt: "2", children: picked.map((v, index) => (jsxs(Tag, { children: [jsx("span", { id: tagIds[index], children: labelFor(v) }), !disabled && (jsx(Delete, { size: "small", ariaLabel: ariaCloseLabel || `Remove ${labelFor(v)}`, "aria-describedby": ariaCloseLabel ? tagIds[index] : undefined, onClick: () => remove(v) }))] }, v))) }))
513
+ : undefined, children: [jsx(ComboboxInput, { ...named, ref: inputRef, id: id, disabled: disabled,
514
+ // The browser's check reads the field's text, and only a pick
515
+ // fills it for good, so it's required until there is one.
516
+ required: required && picked.length === 0, "aria-required": required || undefined, form: form, "aria-describedby": multiple && picked.length > 0 ? tagIds.join(' ') : undefined, placeholder: placeholder, value: query ??
517
+ (multiple || picked.length === 0 ? '' : labelFor(picked[0])), onChange: event => setQuery(event.target.value), onBlur: settle }), inline ? list : jsx(ComboboxMenu, { children: list }), name &&
518
+ (multiple ? picked : [picked[0] ?? '']).map(v => (jsx("input", { type: "hidden", name: name, value: v, form: form, disabled: disabled }, v)))] }));
519
+ });
520
+ ComboboxFromData.displayName = 'ComboboxFromData';
521
+ /**
522
+ * The `Combobox` component is a text field with a list to pick from under it, for a searchable select, a multi-select with tags, grouped items or a server search: given `data` it does the rest, and built from parts it leaves the data to you.
523
+ *
524
+ * Given `data`, it's one element. It renders the field, the menu, the list,
525
+ * the groups and the empty message, filters the items by label as the user
526
+ * types, and keeps the value, which `onChange` reports. With `multiple`, the
527
+ * picks show as tags under the field.
528
+ *
529
+ * Built from parts, the app owns the data: what's in the list, how it's
530
+ * filtered, and the value. A pick calls `onSelect` with the item's `value`,
531
+ * and the Combobox keeps no value and never changes the text in the field,
532
+ * so the app sets both there. Nothing filters the items either, so a
533
+ * server's answer shows as it came. For a free-text field with suggestions,
534
+ * `Autocomplete` is the simpler choice.
535
+ *
536
+ * Either way, focus stays in the field, and the highlighted item is its
537
+ * `aria-activedescendant`. With one value, the highlighted item is the
538
+ * `aria-selected` one, as the ARIA Authoring Practices' combobox does it, and
539
+ * a `checked` item shows a check mark and takes the highlight when the list
540
+ * opens. With `multiple`, `checked` is each item's `aria-selected`. Tab and
541
+ * focus leaving close the menu without picking.
542
+ *
543
+ * When typing leaves the empty message on screen, its text is announced
544
+ * through a polite status region, once, after any loading ends. Result
545
+ * counts aren't announced: a screen reader reads the list's size from the
546
+ * markup, and a count on every keystroke talks over the typing. Show one in
547
+ * a `role="status"` line beside the field when it matters.
548
+ *
549
+ * @function
550
+ * @param {ComboboxProps} props - Props for the Combobox component.
551
+ * @param {React.Ref<HTMLDivElement>} ref - Forwarded ref to the `div.combobox`.
552
+ * @returns {JSX.Element} The field and its list, in a `Field` with the label unless one is around it.
553
+ *
554
+ * @example
555
+ * <Combobox
556
+ * label="Country"
557
+ * data={['Canada', 'France', 'Japan']}
558
+ * placeholder="Pick a country"
559
+ * empty="No countries match"
560
+ * />
561
+ *
562
+ * @example
563
+ * <Combobox label="Country" onSelect={pick}>
564
+ * <Combobox.Input value={query} onChange={e => setQuery(e.target.value)} />
565
+ * <Combobox.Menu>
566
+ * {matches.length > 0 ? (
567
+ * <Combobox.List>
568
+ * {matches.map(c => (
569
+ * <Combobox.Item key={c} value={c} checked={c === value}>
570
+ * {c}
571
+ * </Combobox.Item>
572
+ * ))}
573
+ * </Combobox.List>
574
+ * ) : (
575
+ * <Combobox.Empty>No countries match</Combobox.Empty>
576
+ * )}
577
+ * </Combobox.Menu>
578
+ * </Combobox>
579
+ */
580
+ const ComboboxRoot = forwardRef((props, ref) => props.data === undefined ? (jsx(ComboboxParts, { ...props, ref: ref })) : (jsx(ComboboxFromData, { ...props, ref: ref })));
581
+ /** Combobox with Input, Menu, List, Item, Group and Empty sub-components. */
582
+ const Combobox = withSubComponents(ComboboxRoot, {
583
+ Input: ComboboxInput,
584
+ Menu: ComboboxMenu,
585
+ List: ComboboxList,
586
+ Item: ComboboxItem,
587
+ Group: ComboboxGroup,
588
+ Empty: ComboboxEmpty,
589
+ }, 'Combobox');
590
+
591
+ export { Combobox, ComboboxEmpty, ComboboxGroup, ComboboxInput, ComboboxItem, ComboboxList, ComboboxMenu };
592
+ //# sourceMappingURL=Combobox.js.map