@allxsmith/bestax-bulma 5.50.0 → 5.52.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 (65) hide show
  1. package/dist/bestax.css +1 -1
  2. package/dist/bestax.css.map +1 -1
  3. package/dist/esm/components/Command.js +637 -0
  4. package/dist/esm/components/Command.js.map +1 -0
  5. package/dist/esm/components/EmptyState.js +2 -2
  6. package/dist/esm/components/Timeline.js +5 -5
  7. package/dist/esm/components/TreeView.js +3 -3
  8. package/dist/esm/components/TreeView.js.map +1 -1
  9. package/dist/esm/elements/DescriptionList.js +133 -0
  10. package/dist/esm/elements/DescriptionList.js.map +1 -0
  11. package/dist/esm/form/Autocomplete.js +2 -2
  12. package/dist/esm/form/CheckboxCard.js +118 -0
  13. package/dist/esm/form/CheckboxCard.js.map +1 -0
  14. package/dist/esm/form/Checkboxes.js +14 -3
  15. package/dist/esm/form/Checkboxes.js.map +1 -1
  16. package/dist/esm/form/RadioCard.js +85 -0
  17. package/dist/esm/form/RadioCard.js.map +1 -0
  18. package/dist/esm/form/Radios.js +14 -3
  19. package/dist/esm/form/Radios.js.map +1 -1
  20. package/dist/esm/form/groupFieldset.js +38 -0
  21. package/dist/esm/form/groupFieldset.js.map +1 -0
  22. package/dist/esm/form/useAutoLabelId.js +1 -1
  23. package/dist/esm/helpers/useComboboxEngine.js +3 -3
  24. package/dist/esm/helpers/useFocusTrap.js +2 -2
  25. package/dist/esm/helpers/useFocusTrap.js.map +1 -1
  26. package/dist/extras.css +1 -1
  27. package/dist/extras.css.map +1 -1
  28. package/dist/index.cjs +2685 -1694
  29. package/dist/index.cjs.map +1 -1
  30. package/dist/index.esm.js +4 -0
  31. package/dist/index.esm.js.map +1 -1
  32. package/dist/types/components/Command.d.ts +318 -0
  33. package/dist/types/elements/DescriptionList.d.ts +185 -0
  34. package/dist/types/form/CheckboxCard.d.ts +94 -0
  35. package/dist/types/form/Checkboxes.d.ts +13 -0
  36. package/dist/types/form/RadioCard.d.ts +81 -0
  37. package/dist/types/form/Radios.d.ts +13 -0
  38. package/dist/types/form/groupFieldset.d.ts +32 -0
  39. package/dist/types/index.d.ts +4 -0
  40. package/dist/types-cjs/components/Command.d.ts +318 -0
  41. package/dist/types-cjs/elements/DescriptionList.d.ts +185 -0
  42. package/dist/types-cjs/form/CheckboxCard.d.ts +94 -0
  43. package/dist/types-cjs/form/Checkboxes.d.ts +13 -0
  44. package/dist/types-cjs/form/RadioCard.d.ts +81 -0
  45. package/dist/types-cjs/form/Radios.d.ts +13 -0
  46. package/dist/types-cjs/form/groupFieldset.d.ts +32 -0
  47. package/dist/types-cjs/index.d.ts +4 -0
  48. package/dist/versions/bestax-no-dark-mode.css +1 -1
  49. package/dist/versions/bestax-no-dark-mode.css.map +1 -1
  50. package/dist/versions/bestax-no-helpers-prefixed.css +1 -1
  51. package/dist/versions/bestax-no-helpers-prefixed.css.map +1 -1
  52. package/dist/versions/bestax-no-helpers.css +1 -1
  53. package/dist/versions/bestax-no-helpers.css.map +1 -1
  54. package/dist/versions/bestax-prefixed.css +1 -1
  55. package/dist/versions/bestax-prefixed.css.map +1 -1
  56. package/package.json +1 -1
  57. package/src/scss/components/_command.scss +259 -0
  58. package/src/scss/components/_index.scss +1 -0
  59. package/src/scss/elements/_descriptionlist.scss +153 -0
  60. package/src/scss/elements/_index.scss +1 -0
  61. package/src/scss/form/_checkbox.scss +44 -0
  62. package/src/scss/form/_checkboxcard.scss +355 -0
  63. package/src/scss/form/_index.scss +2 -0
  64. package/src/scss/form/_radio.scss +44 -0
  65. package/src/scss/form/_radiocard.scss +291 -0
package/dist/index.cjs CHANGED
@@ -3537,7 +3537,7 @@ const EmptyStateActions = ({ className, ...props }) => {
3537
3537
  };
3538
3538
  EmptyStateActions.displayName = 'EmptyState.Actions';
3539
3539
  /** Whether a shorthand prop was given something to render. */
3540
- const isGiven$2 = (node) => node !== undefined &&
3540
+ const isGiven$3 = (node) => node !== undefined &&
3541
3541
  node !== null &&
3542
3542
  typeof node !== 'boolean' &&
3543
3543
  node !== '';
@@ -3584,7 +3584,7 @@ const EmptyStateRoot = React.forwardRef(({ className, icon, title, titleAs = 'h2
3584
3584
  });
3585
3585
  // The props' parts first, each only when it's given, then the children
3586
3586
  // exactly as they were written: nothing here looks at them.
3587
- return (jsxRuntime.jsxs("div", { ref: ref, className: classNames(rootClass, bulmaHelperClasses, className), ...rest, children: [isGiven$2(icon) && (jsxRuntime.jsx(EmptyStateIndicator, { children: typeof icon === 'string' ? jsxRuntime.jsx(Icon, { name: icon }) : icon })), isGiven$2(title) && (jsxRuntime.jsx(EmptyStateTitle, { as: titleAs, children: title })), isGiven$2(description) && (jsxRuntime.jsx(EmptyStateDescription, { children: description })), children] }));
3587
+ return (jsxRuntime.jsxs("div", { ref: ref, className: classNames(rootClass, bulmaHelperClasses, className), ...rest, children: [isGiven$3(icon) && (jsxRuntime.jsx(EmptyStateIndicator, { children: typeof icon === 'string' ? jsxRuntime.jsx(Icon, { name: icon }) : icon })), isGiven$3(title) && (jsxRuntime.jsx(EmptyStateTitle, { as: titleAs, children: title })), isGiven$3(description) && (jsxRuntime.jsx(EmptyStateDescription, { children: description })), children] }));
3588
3588
  });
3589
3589
  const EmptyState = withSubComponents(EmptyStateRoot, {
3590
3590
  Indicator: EmptyStateIndicator,
@@ -4156,7 +4156,7 @@ function flatChildren(el) {
4156
4156
  }
4157
4157
  return Array.from(el.children);
4158
4158
  }
4159
- const isShown = (el) => !el.hasAttribute('inert') && getComputedStyle(el).display !== 'none';
4159
+ const isShown$1 = (el) => !el.hasAttribute('inert') && getComputedStyle(el).display !== 'none';
4160
4160
  const isVisible = (el) => {
4161
4161
  const { visibility } = getComputedStyle(el);
4162
4162
  return visibility !== 'hidden' && visibility !== 'collapse';
@@ -4170,7 +4170,7 @@ const isVisible = (el) => {
4170
4170
  function isShownUpTo(el, top) {
4171
4171
  let below = null;
4172
4172
  for (let node = el; node && node !== top; node = flatParent(node)) {
4173
- if (!isShown(node))
4173
+ if (!isShown$1(node))
4174
4174
  return false;
4175
4175
  if (below &&
4176
4176
  node.localName === 'details' &&
@@ -8477,7 +8477,7 @@ const TimelineItemContext = React.createContext({
8477
8477
  titleAs: 'h2',
8478
8478
  });
8479
8479
  /** Whether a prop was given something to render. */
8480
- const isGiven$1 = (node) => node !== undefined &&
8480
+ const isGiven$2 = (node) => node !== undefined &&
8481
8481
  node !== null &&
8482
8482
  typeof node !== 'boolean' &&
8483
8483
  node !== '';
@@ -8510,7 +8510,7 @@ const TimelineItem = React.forwardRef(({ className, title, time, dateTime, icon,
8510
8510
  [`is-${color}`]: isTimelineColor(color),
8511
8511
  });
8512
8512
  const markerClass = usePrefixedClassNames('timeline-marker', {
8513
- 'is-icon': isGiven$1(icon),
8513
+ 'is-icon': isGiven$2(icon),
8514
8514
  });
8515
8515
  const contentClass = usePrefixedClassNames('timeline-content');
8516
8516
  const titleClass = usePrefixedClassNames('timeline-title');
@@ -8522,8 +8522,8 @@ const TimelineItem = React.forwardRef(({ className, title, time, dateTime, icon,
8522
8522
  const words = label ? (jsxRuntime.jsx("span", { style: visuallyHidden, children: ` ${label}` })) : null;
8523
8523
  return (jsxRuntime.jsxs("li", { ref: ref, className: classNames(itemClass, bulmaHelperClasses, className), ...rest, ...(state && {
8524
8524
  'aria-current': state === 'active' ? 'step' : undefined,
8525
- }), children: [jsxRuntime.jsx("div", { className: markerClass, "aria-hidden": "true", children: isGiven$1(icon) &&
8526
- (typeof icon === 'string' ? jsxRuntime.jsx(Icon, { name: icon }) : icon) }), jsxRuntime.jsxs("div", { className: contentClass, children: [isGiven$1(title) ? (jsxRuntime.jsxs(TitleTag, { className: titleClass, children: [title, words] })) : (words), isGiven$1(time) &&
8525
+ }), children: [jsxRuntime.jsx("div", { className: markerClass, "aria-hidden": "true", children: isGiven$2(icon) &&
8526
+ (typeof icon === 'string' ? jsxRuntime.jsx(Icon, { name: icon }) : icon) }), jsxRuntime.jsxs("div", { className: contentClass, children: [isGiven$2(title) ? (jsxRuntime.jsxs(TitleTag, { className: titleClass, children: [title, words] })) : (words), isGiven$2(time) &&
8527
8527
  (dateTime ? (jsxRuntime.jsx("time", { className: timeClass, dateTime: dateTime, children: time })) : (jsxRuntime.jsx("span", { className: timeClass, children: time }))), children] })] }));
8528
8528
  });
8529
8529
  TimelineItem.displayName = 'Timeline.Item';
@@ -8597,7 +8597,7 @@ const TimelineRoot = React.forwardRef(({ className, children, active, pending, a
8597
8597
  // Each child gets its state through context rather than a clone, so an
8598
8598
  // app's own component around a Timeline.Item still gets it. Fragments
8599
8599
  // and arrays are opened up first, so each item inside one counts.
8600
- return (jsxRuntime.jsxs("ol", { ref: ref, className: classNames(rootClass, bulmaHelperClasses, className), ...rest, role: "list", children: [flattenChildren(children).map((child, index) => (jsxRuntime.jsx(TimelineItemContext.Provider, { value: contextFor(stateAt(index)), children: child }, React.isValidElement(child) ? child.key : index))), isGiven$1(pending) && (jsxRuntime.jsx(TimelineItemContext.Provider, { value: contextFor('pending'), children: jsxRuntime.jsx(TimelineItem, { title: pending }) }))] }));
8600
+ return (jsxRuntime.jsxs("ol", { ref: ref, className: classNames(rootClass, bulmaHelperClasses, className), ...rest, role: "list", children: [flattenChildren(children).map((child, index) => (jsxRuntime.jsx(TimelineItemContext.Provider, { value: contextFor(stateAt(index)), children: child }, React.isValidElement(child) ? child.key : index))), isGiven$2(pending) && (jsxRuntime.jsx(TimelineItemContext.Provider, { value: contextFor('pending'), children: jsxRuntime.jsx(TimelineItem, { title: pending }) }))] }));
8601
8601
  });
8602
8602
  const Timeline = withSubComponents(TimelineRoot, { Item: TimelineItem }, 'Timeline');
8603
8603
 
@@ -10934,7 +10934,7 @@ const BranchContext = React.createContext(TOP);
10934
10934
  /** The box's input lets the row's click toggle it, so a change on its own does nothing. */
10935
10935
  const ignoreChange = () => { };
10936
10936
  /** Whether an `icon` was given something to render. */
10937
- const isGiven = (node) => node !== undefined &&
10937
+ const isGiven$1 = (node) => node !== undefined &&
10938
10938
  node !== null &&
10939
10939
  typeof node !== 'boolean' &&
10940
10940
  node !== '';
@@ -11050,7 +11050,7 @@ const TreeViewItem = React.forwardRef(function TreeViewItem({ value, label, icon
11050
11050
  ? null
11051
11051
  : isLoading
11052
11052
  ? 'loading'
11053
- : isGiven(loadError)
11053
+ : isGiven$1(loadError)
11054
11054
  ? 'error'
11055
11055
  : null;
11056
11056
  const labelRef = React.useRef(null);
@@ -11159,7 +11159,7 @@ const TreeViewItem = React.forwardRef(function TreeViewItem({ value, label, icon
11159
11159
  if (!(event.target instanceof HTMLInputElement)) {
11160
11160
  event.preventDefault();
11161
11161
  }
11162
- }, children: jsxRuntime.jsx(Checkbox, { tabIndex: -1, checked: checked === true, indeterminate: checked === 'mixed', disabled: disabled, onChange: ignoreChange }) })), isGiven(icon) && (jsxRuntime.jsx("span", { className: iconClass, "aria-hidden": "true", children: typeof icon === 'string' ? jsxRuntime.jsx(Icon, { name: icon }) : icon })), jsxRuntime.jsx("span", { className: labelClass, id: labelId, ref: labelRef, children: label })] }), open && (hasItems || status) ? (jsxRuntime.jsxs("ul", { className: groupClass, role: "group", children: [items, status && (jsxRuntime.jsx(StatusRow, { kind: status, parent: value, level: branch.path.length + 2, message: loadError, onRetry: onRetry, labelRef: statusLabelRef }))] })) : (items)] }));
11162
+ }, children: jsxRuntime.jsx(Checkbox, { tabIndex: -1, checked: checked === true, indeterminate: checked === 'mixed', disabled: disabled, onChange: ignoreChange }) })), isGiven$1(icon) && (jsxRuntime.jsx("span", { className: iconClass, "aria-hidden": "true", children: typeof icon === 'string' ? jsxRuntime.jsx(Icon, { name: icon }) : icon })), jsxRuntime.jsx("span", { className: labelClass, id: labelId, ref: labelRef, children: label })] }), open && (hasItems || status) ? (jsxRuntime.jsxs("ul", { className: groupClass, role: "group", children: [items, status && (jsxRuntime.jsx(StatusRow, { kind: status, parent: value, level: branch.path.length + 2, message: loadError, onRetry: onRetry, labelRef: statusLabelRef }))] })) : (items)] }));
11163
11163
  });
11164
11164
  TreeViewItem.displayName = 'TreeView.Item';
11165
11165
  /**
@@ -11425,129 +11425,1729 @@ const ScrollArea = React.forwardRef(({ className, style, textColor, bgColor, hei
11425
11425
  });
11426
11426
  ScrollArea.displayName = 'ScrollArea';
11427
11427
 
11428
+ // INTERNAL: deliberately not exported from src/index.ts.
11429
+ /** An IME is composing, so the key belongs to it (`keyCode` 229 is Safari's word for it). */
11430
+ const isComposing$2 = (event) => event.nativeEvent.isComposing || event.nativeEvent.keyCode === 229;
11431
+ const sameValues = (a, b) => a.length === b.length && a.every((value, i) => value === b[i]);
11428
11432
  /**
11429
- * The `Block` component renders a simple container with Bulma's `.block` class, adding vertical margin between sections of content.
11430
- *
11431
- * @function
11432
- * @param {BlockProps} props - Props for the Block component.
11433
- * @returns {JSX.Element} The rendered block element.
11434
- * @see {@link https://bulma.io/documentation/elements/block/ | Bulma Block documentation}
11435
- */
11436
- const Block = ({ className, textColor, color, bgColor, children, ...props }) => {
11437
- /**
11438
- * Generates Bulma helper classes and separates out remaining props.
11439
- */
11440
- const { bulmaHelperClasses, rest } = useBulmaClasses({
11441
- color: textColor ?? color,
11442
- backgroundColor: bgColor,
11443
- ...props,
11444
- });
11445
- const bulmaClasses = usePrefixedClassNames('block');
11446
- const blockClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
11447
- return (jsxRuntime.jsx("div", { className: blockClasses, ...rest, children: children }));
11448
- };
11449
-
11450
- /**
11451
- * The `Box` component renders a bordered, padded container with an optional shadow using Bulma's `.box` class.
11452
- *
11453
- * @function
11454
- * @param {BoxProps} props - Props for the Box component.
11455
- * @returns {JSX.Element} The rendered box element.
11456
- * @see {@link https://bulma.io/documentation/elements/box/ | Bulma Box documentation}
11457
- */
11458
- const Box = ({ className, textColor, color, bgColor, hasShadow = true, children, style, ...props }) => {
11459
- /**
11460
- * Generates Bulma helper classes and separates out remaining props.
11461
- */
11462
- const { bulmaHelperClasses, bulmaHelperStyles, rest } = useBulmaClasses({
11463
- color: textColor ?? color,
11464
- backgroundColor: bgColor,
11465
- ...props,
11466
- });
11467
- const bulmaClasses = usePrefixedClassNames('box', {
11468
- 'is-shadowless': !hasShadow,
11469
- });
11470
- const boxClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
11471
- return (jsxRuntime.jsx("div", { className: boxClasses, style: mergeBulmaStyles(bulmaHelperStyles, style), ...rest, children: children }));
11472
- };
11473
-
11474
- /**
11475
- * The `Code` component renders a styled `<code>` element with Bulma helper class integration.
11476
- *
11477
- * @function
11478
- * @param {CodeProps} props - Props for the Code component.
11479
- * @returns {JSX.Element} The rendered code element.
11480
- */
11481
- const Code = ({ className, textColor, bgColor, children, ...props }) => {
11482
- const { bulmaHelperClasses, rest } = useBulmaClasses({
11483
- color: textColor,
11484
- backgroundColor: bgColor,
11485
- ...props,
11486
- });
11487
- const bulmaClasses = usePrefixedClassNames();
11488
- const codeClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
11489
- return (jsxRuntime.jsx("code", { className: codeClasses || undefined, ...rest, children: children }));
11490
- };
11491
-
11492
- // Valid size modifiers for the content class
11493
- const validSizes = ['small', 'medium', 'large'];
11494
- /**
11495
- * The `Content` component applies Bulma’s typographic styles to its children, enhancing the appearance of HTML elements like paragraphs, headings, lists, and tables.
11496
- *
11497
- * @function
11498
- * @param {ContentProps} props - Props for the Content component.
11499
- * @returns {JSX.Element} The rendered content block.
11500
- * @see {@link https://bulma.io/documentation/elements/content/ | Bulma Content documentation}
11501
- */
11502
- const Content = ({ className, textColor, color, bgColor, size, children, ...props }) => {
11503
- /**
11504
- * Generates Bulma helper classes and separates out remaining props.
11505
- */
11506
- const { bulmaHelperClasses, rest } = useBulmaClasses({
11507
- color: textColor ?? color,
11508
- backgroundColor: bgColor,
11509
- ...props,
11510
- });
11511
- const bulmaClasses = usePrefixedClassNames('content', {
11512
- [`is-${size}`]: size && size !== 'normal' && validSizes.includes(size),
11513
- });
11514
- const contentClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
11515
- return (jsxRuntime.jsx("div", { className: contentClasses, ...rest, children: children }));
11516
- };
11517
-
11518
- /**
11519
- * The `Delete` component provides a Bulma-styled close/delete button for dismissing modals, notifications, tags, messages, and more.
11433
+ * The keys, the open state, the highlight and the ARIA of an editable
11434
+ * combobox whose list the app renders and filters (#1083). DOM focus stays
11435
+ * in the field the whole time: the highlight is `aria-activedescendant`, as
11436
+ * the ARIA Authoring Practices ask of a combobox, where `useMenuKeys` moves
11437
+ * real focus, as a menu should.
11520
11438
  *
11521
- * It renders `type="button"`, so a delete button inside a form does not submit it.
11439
+ * The options are read from the listbox in document order whenever they're
11440
+ * needed, so options in groups and options that come and go are found, and
11441
+ * the highlight is remembered by value, so it survives a re-render. The text
11442
+ * in the field and any value are the app's: a pick only calls
11443
+ * `onSelect`.
11522
11444
  *
11523
- * @function
11524
- * @param {DeleteProps} props - Props for the Delete component.
11525
- * @returns {JSX.Element} The rendered delete button.
11526
- * @see {@link https://bulma.io/documentation/elements/delete/ | Bulma Delete documentation}
11445
+ * @function useComboboxEngine
11446
+ * @param options - The open state, the pick callback and the shape.
11447
+ * @returns The state and handlers the parts wire up.
11527
11448
  */
11528
- const Delete = ({ className, textColor, color, bgColor, onClick, size, ariaLabel, disabled = false, ...props }) => {
11529
- /**
11530
- * Generates Bulma helper classes and separates out remaining props.
11531
- */
11532
- const { bulmaHelperClasses, rest } = useBulmaClasses({
11533
- color: textColor ?? color,
11534
- backgroundColor: bgColor,
11535
- ...props,
11536
- });
11537
- const bulmaClasses = usePrefixedClassNames('delete', {
11538
- [`is-${size}`]: size,
11539
- 'is-disabled': disabled,
11449
+ function useComboboxEngine({ open: controlledOpen, defaultOpen, onOpenChange, onSelect, onSettle, keepFirst, multiple, inline, deferred, }) {
11450
+ const baseId = React.useId();
11451
+ const listboxId = `${baseId}-listbox`;
11452
+ const itemPrefix = `${baseId}-item-`;
11453
+ // The value goes into the id encoded, so a value with a space in it still
11454
+ // makes one id, not two.
11455
+ const itemId = React.useCallback((value) => itemPrefix + encodeURIComponent(value), [itemPrefix]);
11456
+ const rootRef = React.useRef(null);
11457
+ const inputRef = React.useRef(null);
11458
+ const listboxRef = React.useRef(null);
11459
+ const popupRef = React.useRef(null);
11460
+ const insidePressRef = React.useRef(null);
11461
+ const optionsRef = React.useRef(new Map());
11462
+ const landingRef = React.useRef(null);
11463
+ const wasOpenRef = React.useRef(false);
11464
+ const shownValuesRef = React.useRef([]);
11465
+ // Whether the next highlight change is one to scroll to: a key's or a
11466
+ // landing's. The pointer's isn't, since the pointer is already on that
11467
+ // option, and scrolling would move the list out from under it.
11468
+ const scrollToHighlightRef = React.useRef(false);
11469
+ const isControlled = controlledOpen !== undefined;
11470
+ const [internalOpen, setInternalOpen] = React.useState(defaultOpen);
11471
+ const open = inline || (controlledOpen ?? internalOpen);
11472
+ const [highlighted, setHighlighted] = React.useState(null);
11473
+ const [typed, setTyped] = React.useState(false);
11474
+ // Whether a listbox with an option in it is on the page, which only the
11475
+ // page can say, so it's read once the open list has rendered.
11476
+ const [listShown, setListShown] = React.useState(null);
11477
+ // Options register from their own effects, which needn't render the
11478
+ // component that holds this hook, so they ask for a render to be read.
11479
+ const [, refresh] = React.useReducer((n) => n + 1, 0);
11480
+ // Closing forgets the highlight, the typing and what was on the page,
11481
+ // during render, React's pattern for adjusting state when other state
11482
+ // changes.
11483
+ if (!open) {
11484
+ if (highlighted !== null)
11485
+ setHighlighted(null);
11486
+ if (typed)
11487
+ setTyped(false);
11488
+ if (listShown !== null)
11489
+ setListShown(null);
11490
+ }
11491
+ const expanded = open && (listShown ?? !deferred);
11492
+ /** The options on the page, in document order, groups included. */
11493
+ const shownOptions = React.useCallback(() => {
11494
+ const listbox = listboxRef.current;
11495
+ if (!listbox)
11496
+ return [];
11497
+ const shown = [];
11498
+ listbox
11499
+ .querySelectorAll('[role="option"]')
11500
+ .forEach(element => {
11501
+ // Only the Combobox's own options: an element given the role by
11502
+ // hand isn't one.
11503
+ const state = optionsRef.current.get(element);
11504
+ if (state)
11505
+ shown.push({ element, ...state });
11506
+ });
11507
+ return shown;
11508
+ }, []);
11509
+ const requestOpen = React.useCallback((next, landing = 'open') => {
11510
+ if (inline || next === open)
11511
+ return;
11512
+ landingRef.current = next ? landing : null;
11513
+ onOpenChange?.(next);
11514
+ if (!isControlled)
11515
+ setInternalOpen(next);
11516
+ }, [inline, open, onOpenChange, isControlled]);
11517
+ const close = React.useCallback(() => requestOpen(false), [requestOpen]);
11518
+ // After every render, read the list: whether it has options, and where
11519
+ // the highlight goes now. A highlighted option that has left takes the
11520
+ // highlight with it, or hands it to the first enabled one under
11521
+ // `keepFirst`, which also moves it there whenever the options change.
11522
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- after every render on purpose
11523
+ useClientLayoutEffect(() => {
11524
+ const opened = open && !wasOpenRef.current;
11525
+ wasOpenRef.current = open;
11526
+ if (!open) {
11527
+ landingRef.current = null;
11528
+ shownValuesRef.current = [];
11529
+ return;
11530
+ }
11531
+ const options = shownOptions();
11532
+ const values = options.map(option => option.value);
11533
+ const changed = !sameValues(values, shownValuesRef.current);
11534
+ shownValuesRef.current = values;
11535
+ setListShown(values.length > 0);
11536
+ const landing = landingRef.current ?? (opened ? 'open' : null);
11537
+ landingRef.current = null;
11538
+ const enabled = options.filter(option => !option.disabled);
11539
+ const checked = enabled.find(option => option.checked)?.value;
11540
+ const first = enabled.length > 0 ? enabled[0].value : null;
11541
+ const last = enabled.length > 0 ? enabled[enabled.length - 1].value : null;
11542
+ let next = highlighted;
11543
+ if (landing === 'first')
11544
+ next = checked ?? first;
11545
+ else if (landing === 'last')
11546
+ next = checked ?? last;
11547
+ else if (landing === 'open')
11548
+ next = checked ?? (keepFirst ? first : null);
11549
+ else if (landing === 'typed')
11550
+ next = keepFirst ? first : null;
11551
+ else if (landing === 'none')
11552
+ next = null;
11553
+ else if (keepFirst && changed)
11554
+ next = first;
11555
+ // Without `keepFirst`, which moved it above, a highlighted option
11556
+ // that left takes the highlight with it.
11557
+ else if (next !== null && !values.includes(next))
11558
+ next = null;
11559
+ // Only a change is set. StrictMode runs this twice on mount, and the
11560
+ // second run, with the landing used up, works out the rendered highlight
11561
+ // again, so it leaves the first run's landing in place.
11562
+ if (next !== highlighted) {
11563
+ scrollToHighlightRef.current = true;
11564
+ setHighlighted(next);
11565
+ }
11540
11566
  });
11541
- const classes = classNames(bulmaClasses, bulmaHelperClasses, className);
11542
- return (jsxRuntime.jsx("button", { className: classes, onClick: onClick, disabled: disabled, ...rest, "aria-label": rest['aria-label'] || ariaLabel || 'Close', type: buttonType(rest.type) }));
11543
- };
11544
-
11545
- /**
11546
- * The `Divider` component renders a styled horizontal rule (`<hr>`) element with Bulma helper class integration.
11547
- *
11548
- * @function
11549
- * @param {DividerProps} props - Props for the Divider component.
11550
- * @returns {JSX.Element} The rendered hr element.
11567
+ // A browser scrolls to what has focus, not to an active descendant, so
11568
+ // the highlighted option is brought into view here, when a key or a
11569
+ // landing moved it there.
11570
+ useClientLayoutEffect(() => {
11571
+ if (highlighted === null || !scrollToHighlightRef.current)
11572
+ return;
11573
+ scrollToHighlightRef.current = false;
11574
+ const option = shownOptions().find(shown => shown.value === highlighted);
11575
+ option?.element.scrollIntoView?.({ block: 'nearest' });
11576
+ }, [highlighted, shownOptions]);
11577
+ // A press outside the field and the list closes it. The path is checked
11578
+ // rather than the target, so it works in a shadow root.
11579
+ React.useEffect(() => {
11580
+ const root = rootRef.current;
11581
+ if (!open || inline || !root)
11582
+ return undefined;
11583
+ const onPress = (event) => {
11584
+ if (event === insidePressRef.current ||
11585
+ isEventInside(event, rootRef.current) ||
11586
+ isEventInside(event, popupRef.current)) {
11587
+ return;
11588
+ }
11589
+ close();
11590
+ };
11591
+ // Its document, an iframe's when the tree renders into one.
11592
+ const doc = root.ownerDocument;
11593
+ doc.addEventListener('pointerdown', onPress);
11594
+ return () => doc.removeEventListener('pointerdown', onPress);
11595
+ }, [open, inline, close]);
11596
+ const submit = React.useCallback((value) => {
11597
+ onSelect?.(value);
11598
+ if (!multiple)
11599
+ close();
11600
+ }, [onSelect, multiple, close]);
11601
+ const onKeyDown = (event) => {
11602
+ // The caller's handler ran first and can keep a key, and an IME that's
11603
+ // composing owns every key.
11604
+ if (event.defaultPrevented || isComposing$2(event))
11605
+ return;
11606
+ const { key, altKey } = event;
11607
+ if (!open) {
11608
+ if (key === 'ArrowDown' || (key === 'ArrowUp' && !altKey)) {
11609
+ event.preventDefault();
11610
+ requestOpen(true, altKey ? 'none' : key === 'ArrowDown' ? 'first' : 'last');
11611
+ }
11612
+ return;
11613
+ }
11614
+ switch (key) {
11615
+ case 'ArrowDown':
11616
+ case 'ArrowUp': {
11617
+ if (altKey) {
11618
+ if (key === 'ArrowUp' && !inline) {
11619
+ event.preventDefault();
11620
+ close();
11621
+ }
11622
+ return;
11623
+ }
11624
+ event.preventDefault();
11625
+ // Disabled options too, so a screen reader user finds them.
11626
+ const options = shownOptions();
11627
+ if (options.length === 0)
11628
+ return;
11629
+ const at = options.findIndex(option => option.value === highlighted);
11630
+ const step = key === 'ArrowDown' ? 1 : -1;
11631
+ const index = at === -1
11632
+ ? step === 1
11633
+ ? 0
11634
+ : options.length - 1
11635
+ : (at + step + options.length) % options.length;
11636
+ scrollToHighlightRef.current = true;
11637
+ setHighlighted(options[index].value);
11638
+ return;
11639
+ }
11640
+ case 'Enter': {
11641
+ const option = shownOptions().find(shown => shown.value === highlighted);
11642
+ if (option) {
11643
+ event.preventDefault();
11644
+ if (!option.disabled)
11645
+ submit(option.value);
11646
+ return;
11647
+ }
11648
+ // Nothing to pick: the key goes on, so a form submits.
11649
+ onSettle?.();
11650
+ close();
11651
+ return;
11652
+ }
11653
+ case 'Escape':
11654
+ if (inline)
11655
+ return;
11656
+ // It stops here only when there was something to close, so a
11657
+ // Popover or Modal around it keeps its own Escape otherwise. Nothing
11658
+ // stops it going on: a handler further out reads `defaultPrevented`.
11659
+ if (popupRef.current)
11660
+ event.preventDefault();
11661
+ close();
11662
+ return;
11663
+ case 'Tab':
11664
+ close();
11665
+ return;
11666
+ case 'Home':
11667
+ case 'End':
11668
+ case 'ArrowLeft':
11669
+ case 'ArrowRight':
11670
+ // The field's own keys, which take the field back from the list.
11671
+ setHighlighted(null);
11672
+ return;
11673
+ }
11674
+ };
11675
+ const onChange = () => {
11676
+ setTyped(true);
11677
+ if (open) {
11678
+ landingRef.current = 'typed';
11679
+ setHighlighted(null);
11680
+ refresh();
11681
+ }
11682
+ else {
11683
+ requestOpen(true, 'typed');
11684
+ }
11685
+ };
11686
+ const onClick = () => {
11687
+ requestOpen(true, 'open');
11688
+ };
11689
+ const onBlur = (event) => {
11690
+ const next = event.relatedTarget;
11691
+ if (next &&
11692
+ (rootRef.current?.contains(next) || popupRef.current?.contains(next))) {
11693
+ return;
11694
+ }
11695
+ close();
11696
+ };
11697
+ const hover = (value) => {
11698
+ scrollToHighlightRef.current = false;
11699
+ setHighlighted(value);
11700
+ };
11701
+ const pick = (value) => {
11702
+ submit(value);
11703
+ // A press in the list keeps focus in the field, but a browser that
11704
+ // ignores that, or a tap, can leave it elsewhere.
11705
+ const input = inputRef.current;
11706
+ if (input && input.isConnected && getActiveElementInTree(input) !== input) {
11707
+ input.focus();
11708
+ }
11709
+ };
11710
+ const keepFocus = (event) => event.preventDefault();
11711
+ const markInside = (event) => {
11712
+ insidePressRef.current = event.nativeEvent;
11713
+ };
11714
+ const registerPopup = React.useCallback((element) => {
11715
+ popupRef.current = element;
11716
+ return () => {
11717
+ popupRef.current = null;
11718
+ };
11719
+ }, []);
11720
+ const registerItem = React.useCallback((element, state) => {
11721
+ const options = optionsRef.current;
11722
+ options.set(element, state);
11723
+ refresh();
11724
+ return () => {
11725
+ options.delete(element);
11726
+ refresh();
11727
+ };
11728
+ }, []);
11729
+ return {
11730
+ open,
11731
+ expanded,
11732
+ highlighted,
11733
+ typed,
11734
+ listboxId,
11735
+ itemId,
11736
+ rootRef,
11737
+ inputRef,
11738
+ listboxRef,
11739
+ registerPopup,
11740
+ registerItem,
11741
+ onKeyDown,
11742
+ onChange,
11743
+ onClick,
11744
+ onBlur,
11745
+ hover,
11746
+ pick,
11747
+ keepFocus,
11748
+ markInside,
11749
+ };
11750
+ }
11751
+
11752
+ /**
11753
+ * Distinguishes an `IconProps` object from a plain custom node (an inline SVG, a
11754
+ * `react-icons` component, …) in a slot that accepts either — `Control`'s
11755
+ * `iconLeft`/`iconRight`, `IconText`'s `iconProps`.
11756
+ *
11757
+ * One copy, imported by both consumers: two hand-maintained copies, one per
11758
+ * consumer, is how the deprecated `icon` path came to crash both (#663).
11759
+ *
11760
+ * Its own module rather than `Icon.tsx` because `src/index.ts` wildcard-exports
11761
+ * that file, and everything published there is public forever. `polymorphic.ts`
11762
+ * makes the same call for `isCustomElement`: an internal discriminator is not
11763
+ * something to support for the life of the package.
11764
+ *
11765
+ * It tests each member's VALUE, not merely its key, and only after everything
11766
+ * React owns is out of the way. `'icon' in value` was enough
11767
+ * to claim a Font Awesome `IconDefinition` — `{ prefix, iconName, icon: [w, h, …,
11768
+ * path] }` — whose `icon` is a path array rather than a class string. That object
11769
+ * is not a renderable node either, so the honest outcome is the one this slot has
11770
+ * always had for it: the node branch, and React's own "Objects are not valid as a
11771
+ * React child". Claiming it here instead rendered a blank container that still
11772
+ * reserved layout.
11773
+ *
11774
+ * A member added to `IconProps` needs a line here, and a missed one is a crash
11775
+ * rather than a fallthrough. That is the cost of a slot taking either a props
11776
+ * object or a node; keeping the guard in one place is what keeps the cost to a
11777
+ * single line.
11778
+ */
11779
+ function isIconProps(value) {
11780
+ if (typeof value !== 'object' || value === null)
11781
+ return false;
11782
+ // Anything React owns. `isValidElement` covers elements only; a PORTAL is a
11783
+ // valid `ReactNode` that fails it and carries a `children` key, so the member
11784
+ // test below claimed it and `Control` spread it into `<Icon>` as props —
11785
+ // rendering the portal's child locally instead of in its container. Every
11786
+ // React internal carries `$$typeof`, so testing for it covers lazy, context
11787
+ // and whatever React adds next, rather than portals alone.
11788
+ if (React.isValidElement(value) || '$$typeof' in value)
11789
+ return false;
11790
+ const candidate = value;
11791
+ return (typeof candidate.name === 'string' ||
11792
+ typeof candidate.icon === 'string' ||
11793
+ 'children' in candidate);
11794
+ }
11795
+
11796
+ // Whether the nearest enclosing `Control` shows its `isLoading` spinner. The
11797
+ // picker bases read it so their right-side launcher gives way to that
11798
+ // spinner, which sits at the same edge, whether the `Control` is the one a
11799
+ // convenience input renders or one the caller wrapped around the input.
11800
+ // Autocomplete reads it for its clear button, which sits there too.
11801
+ // Kept apart from `FormContext`, whose contexts only report whether a `Field`
11802
+ // or `Control` is present. Internal; not part of the public API.
11803
+ const ControlLoadingContext = React.createContext(false);
11804
+ /** Provider for the loading state, used internally by `Control`. */
11805
+ const ControlLoadingProvider = ControlLoadingContext.Provider;
11806
+ /** Whether the nearest enclosing `Control` is loading; `false` outside one. */
11807
+ const useControlLoading = () => React.useContext(ControlLoadingContext);
11808
+
11809
+ const allowedColors = [...validColors, 'inherit', 'current'];
11810
+ /**
11811
+ * The `Control` component is a Bulma-styled wrapper for form controls (`Input`, `Select`, `TextArea`, etc.), supporting icons (left/right), loading state, expansion, size, and Bulma helper props for layout and color.
11812
+ *
11813
+ * @function
11814
+ * @param {ControlProps} props - Props for the Control component.
11815
+ * @returns {JSX.Element} The rendered control container.
11816
+ * @see {@link https://bulma.io/documentation/form/general/#control | Bulma Control documentation}
11817
+ *
11818
+ * @example
11819
+ * // Control with left icon
11820
+ * <Control iconLeftName="envelope" iconLeftSize="small">
11821
+ * <input className="input" type="email" placeholder="Email" />
11822
+ * </Control>
11823
+ */
11824
+ const Control = React.forwardRef(({ as = 'div', hasIconsLeft, hasIconsRight, isLoading, isExpanded, size, textColor, bgColor, iconLeft, iconRight, iconLeftName, iconLeftSize, iconRightName, iconRightSize, className, children, ...props }, ref) => {
11825
+ const Component = (as === 'p' ? 'p' : 'div');
11826
+ const { classPrefix } = useConfig();
11827
+ // Remove textColor/bgColor from props before spreading
11828
+ const { textColor: _ignoredTextColor, bgColor: _ignoredBgColor, ...restProps } = props;
11829
+ const safeTextColor = allowedColors.includes(textColor)
11830
+ ? textColor
11831
+ : undefined;
11832
+ const safeBgColor = allowedColors.includes(bgColor)
11833
+ ? bgColor
11834
+ : undefined;
11835
+ const { bulmaHelperClasses, rest } = useBulmaClasses({
11836
+ color: safeTextColor,
11837
+ backgroundColor: safeBgColor,
11838
+ ...restProps,
11839
+ });
11840
+ // Prepare icon props for the shortcut. A truthiness test (not `!== undefined`)
11841
+ // so a falsy node from the idiomatic `iconLeft={cond && <Node/>}` / `iconLeft={maybe ?? null}`
11842
+ // pattern counts as "no icon" — otherwise `false`/`null` would reserve the icon column and
11843
+ // mount an empty `.icon` span. A falsy `iconLeft` also correctly falls through to the
11844
+ // `iconLeftName` shortcut, matching the pre-node behavior.
11845
+ const leftIconValue = iconLeft ||
11846
+ (iconLeftName
11847
+ ? {
11848
+ name: iconLeftName,
11849
+ size: iconLeftSize,
11850
+ }
11851
+ : undefined);
11852
+ const rightIconValue = iconRight ||
11853
+ (iconRightName
11854
+ ? {
11855
+ name: iconRightName,
11856
+ size: iconRightSize,
11857
+ }
11858
+ : undefined);
11859
+ const mainClass = usePrefixedClassNames('control', {
11860
+ 'has-icons-left': hasIconsLeft || !!leftIconValue,
11861
+ 'has-icons-right': hasIconsRight || !!rightIconValue,
11862
+ 'is-loading': isLoading,
11863
+ 'is-expanded': isExpanded,
11864
+ [`is-${size}`]: !!size,
11865
+ });
11866
+ const controlClass = classNames(mainClass, bulmaHelperClasses, className);
11867
+ // --- FIX: Spread both restProps (for data-testid, etc) AND rest (from useBulmaClasses) ---
11868
+ return (jsxRuntime.jsx(ControlProvider, { value: true, children: jsxRuntime.jsxs(Component, { className: controlClass, ref: ref, ...restProps, ...rest, children: [jsxRuntime.jsx(ControlLoadingProvider, { value: !!isLoading, children: children }), leftIconValue &&
11869
+ (isIconProps(leftIconValue) ? (jsxRuntime.jsx(Icon, { ...leftIconValue, className: prefixedClassNames(classPrefix, 'is-left') })) : (jsxRuntime.jsx(Icon, { className: prefixedClassNames(classPrefix, 'is-left'), children: leftIconValue }))), rightIconValue &&
11870
+ (isIconProps(rightIconValue) ? (jsxRuntime.jsx(Icon, { ...rightIconValue, className: prefixedClassNames(classPrefix, 'is-right') })) : (jsxRuntime.jsx(Icon, { className: prefixedClassNames(classPrefix, 'is-right'), children: rightIconValue })))] }) }));
11871
+ });
11872
+ Control.displayName = 'Control';
11873
+
11874
+ /**
11875
+ * FieldLabel component for rendering a Bulma field label. It renders the
11876
+ * label column of a horizontal `Field` (a `div` with the `field-label` class),
11877
+ * not a `<label>`, so text placed straight in it names nothing. Put a `<label>`
11878
+ * with the `label` class inside it and point its `htmlFor` at the control's
11879
+ * `id`, or give the horizontal `Field` a `label` prop, which renders this
11880
+ * column and its `<label>` for you. A `<label>` you put here names the
11881
+ * control through its `htmlFor` alone: a range `Slider`'s thumbs and an
11882
+ * `Autocomplete`'s, a `Combobox`'s or a `Taginput`'s list point
11883
+ * `aria-labelledby` at a `Field`'s label only when its `label` prop renders it
11884
+ * and it names the control. Where it would not name the control on its own, as in an inner `Field`,
11885
+ * pass `labelProps={{ htmlFor, id }}` with a matching `id` on the control.
11886
+ *
11887
+ * @function
11888
+ * @param {FieldLabelProps} props - Props for the FieldLabel component.
11889
+ * @returns {JSX.Element} The rendered field label.
11890
+ *
11891
+ * @example
11892
+ * <FieldLabel size="normal">
11893
+ * <label className="label" htmlFor="name">Name</label>
11894
+ * </FieldLabel>
11895
+ */
11896
+ const FieldLabel = ({ size, textColor, bgColor, className, children, ...props }) => {
11897
+ const { bulmaHelperClasses, rest } = useBulmaClasses({
11898
+ color: textColor,
11899
+ backgroundColor: bgColor,
11900
+ ...props,
11901
+ });
11902
+ const mainClass = usePrefixedClassNames('field-label', {
11903
+ [`is-${size}`]: !!size,
11904
+ });
11905
+ const fieldLabelClass = classNames(mainClass, bulmaHelperClasses, className);
11906
+ // Spread ...props and ...rest so custom props like data-testid are included
11907
+ return (jsxRuntime.jsx("div", { className: fieldLabelClass, ...props, ...rest, children: children }));
11908
+ };
11909
+ /**
11910
+ * FieldBody component for rendering Bulma field body.
11911
+ *
11912
+ * @function
11913
+ * @param {FieldBodyProps} props - Props for the FieldBody component.
11914
+ * @returns {JSX.Element} The rendered field body.
11915
+ *
11916
+ * @example
11917
+ * <FieldBody><input className="input" /></FieldBody>
11918
+ */
11919
+ const FieldBody = ({ textColor, bgColor, className, children, ...props }) => {
11920
+ const { bulmaHelperClasses, rest } = useBulmaClasses({
11921
+ color: textColor,
11922
+ backgroundColor: bgColor,
11923
+ ...props,
11924
+ });
11925
+ const mainClass = usePrefixedClassNames('field-body');
11926
+ const fieldBodyClass = classNames(mainClass, bulmaHelperClasses, className);
11927
+ // Spread ...props and ...rest so custom props like data-testid are included
11928
+ return (jsxRuntime.jsx("div", { className: fieldBodyClass, ...props, ...rest, children: children }));
11929
+ };
11930
+ /**
11931
+ * The `Field` component is a Bulma-styled form field container.
11932
+ *
11933
+ * @function
11934
+ * @param {FieldProps} props - Props for the Field component.
11935
+ * @returns {JSX.Element} The rendered field container.
11936
+ * @see {@link https://bulma.io/documentation/form/general/#field | Bulma Field documentation}
11937
+ *
11938
+ * @example
11939
+ * // Labelled field — the label auto-associates with the composed base
11940
+ * <Field label="Email">
11941
+ * <Control>
11942
+ * <InputBase type="email" />
11943
+ * </Control>
11944
+ * </Field>
11945
+ *
11946
+ * @example
11947
+ * // Horizontal field
11948
+ * <Field horizontal label="Name">
11949
+ * <Control>
11950
+ * <InputBase />
11951
+ * </Control>
11952
+ * </Field>
11953
+ */
11954
+ const FieldComponent = ({ horizontal, grouped, hasAddons, narrow, label, labelSize, labelProps, textColor, color: _fieldColor, bgColor, className, children, ...props }) => {
11955
+ const { bulmaHelperClasses, rest } = useBulmaClasses({
11956
+ color: textColor,
11957
+ backgroundColor: bgColor,
11958
+ ...props,
11959
+ });
11960
+ const mainClass = usePrefixedClassNames('field', {
11961
+ 'is-horizontal': horizontal,
11962
+ 'has-addons': !!hasAddons,
11963
+ 'has-addons-centered': hasAddons === 'centered',
11964
+ 'has-addons-right': hasAddons === 'right',
11965
+ 'is-narrow': narrow,
11966
+ 'is-grouped': grouped === true ||
11967
+ grouped === 'centered' ||
11968
+ grouped === 'right' ||
11969
+ grouped === 'multiline',
11970
+ 'is-grouped-centered': grouped === 'centered',
11971
+ 'is-grouped-right': grouped === 'right',
11972
+ 'is-grouped-multiline': grouped === 'multiline',
11973
+ });
11974
+ const fieldClass = classNames(mainClass, bulmaHelperClasses, className);
11975
+ // Default labelSize to 'normal' when horizontal for proper baseline alignment
11976
+ const effectiveLabelSize = labelSize ?? (horizontal ? 'normal' : undefined);
11977
+ const labelClass = usePrefixedClassNames('label');
11978
+ // Auto-associate the label with a single composed base control (#495): the
11979
+ // label points at a generated id shared via context, which a composed base
11980
+ // adopts when the user supplied no id of their own.
11981
+ // Presence semantics on htmlFor — even an explicit `htmlFor: undefined`
11982
+ // means the caller owns the association. Grouped/addons fields hold several
11983
+ // controls, so no single association is generated for them.
11984
+ const generatedId = React.useId();
11985
+ const userWiredLabel = !!labelProps && 'htmlFor' in labelProps;
11986
+ const targetId = label && !userWiredLabel && !grouped && !hasAddons
11987
+ ? generatedId
11988
+ : undefined;
11989
+ // A group control (Radios, Checkboxes, Rate, DateRangeInput and its base)
11990
+ // cannot take that `for`, so it points `aria-labelledby` at the label
11991
+ // itself (#939).
11992
+ // The label gets an id whenever the association is on, unless the caller
11993
+ // gave it one.
11994
+ const labelId = labelProps?.id ?? (targetId ? `${targetId}-label` : undefined);
11995
+ // What the rendered label's `for` points at, generated or the caller's, and
11996
+ // the id it renders with, so a control can tell when a label wired by hand
11997
+ // names it and point at that label. An unlabeled Field passes on its
11998
+ // parent's. Unlike `targetId`, a label wired by hand passes on its own in
11999
+ // `grouped`/`hasAddons` Fields too, on purpose: those only stop the Field
12000
+ // picking one control out of several, and the caller's `for` has picked
12001
+ // one, so a range Slider or Autocomplete it names points at that label in
12002
+ // a row as well. A group reads the label element id below instead, which
12003
+ // stays off for a label wired by hand, since no group takes its `for`.
12004
+ const inheritedLabelFor = useFieldLabelFor();
12005
+ const labelForTarget = userWiredLabel ? labelProps.htmlFor : targetId;
12006
+ const ownLabelFor = React.useMemo(() => ({ htmlFor: labelForTarget, id: labelId }), [labelForTarget, labelId]);
12007
+ const labelFor = label ? ownLabelFor : inheritedLabelFor;
12008
+ // Whether the label keeps its generated `for` (#1004). Content that takes
12009
+ // the Field's id says so from a layout effect, and this Field's own layout
12010
+ // effect runs after its content's in the same commit, so by then every
12011
+ // report has landed. Until it runs, on the server and in the first render
12012
+ // while hydrating, the label writes the `for` as it always has. From then
12013
+ // on it keeps the `for` only while some content holds the id, and content
12014
+ // that mounts or changes later reports when it does. Counted in a ref, and
12015
+ // state is set only once settled, so content that takes the id renders
12016
+ // the Field just once.
12017
+ const takersRef = React.useRef(0);
12018
+ const settledRef = React.useRef(false);
12019
+ const [forTaken, setForTaken] = React.useState(true);
12020
+ const settle = React.useCallback(() => {
12021
+ if (settledRef.current)
12022
+ setForTaken(takersRef.current > 0);
12023
+ }, []);
12024
+ const reportLabelFor = React.useCallback(() => {
12025
+ takersRef.current += 1;
12026
+ settle();
12027
+ return () => {
12028
+ takersRef.current -= 1;
12029
+ settle();
12030
+ };
12031
+ }, [settle]);
12032
+ useClientLayoutEffect(() => {
12033
+ settledRef.current = true;
12034
+ if (targetId)
12035
+ settle();
12036
+ }, [targetId, settle]);
12037
+ const renderedTargetId = forTaken ? targetId : undefined;
12038
+ let renderedLabel = null;
12039
+ if (label) {
12040
+ if (horizontal) {
12041
+ renderedLabel = (jsxRuntime.jsx(FieldLabel, { size: effectiveLabelSize, children: jsxRuntime.jsx("label", { htmlFor: renderedTargetId, ...labelProps, id: labelId, className: classNames(labelClass, labelProps?.className), style: labelProps?.style, children: label }) }));
12042
+ }
12043
+ else {
12044
+ renderedLabel = (jsxRuntime.jsx("label", { htmlFor: renderedTargetId, ...labelProps, id: labelId, className: classNames(labelClass, labelProps?.className), style: { display: 'block', ...(labelProps?.style || {}) }, children: label }));
12045
+ }
12046
+ }
12047
+ // If horizontal, wrap children in FieldBody (unless the user already provided
12048
+ // a FieldBody — either as the single child, or as one element among siblings
12049
+ // like <Field.Label/> + <Field.Body/>).
12050
+ let content = children;
12051
+ if (horizontal) {
12052
+ const isFieldBody = (c) => React.isValidElement(c) &&
12053
+ // @ts-expect-error displayName isn't on the public type
12054
+ (c.type === FieldBody || c.type?.displayName === 'FieldBody');
12055
+ const isFieldLabel = (c) => React.isValidElement(c) &&
12056
+ // @ts-expect-error displayName isn't on the public type
12057
+ (c.type === FieldLabel || c.type?.displayName === 'FieldLabel');
12058
+ const childArray = React.Children.toArray(children);
12059
+ const userProvidedStructure = childArray.some(c => isFieldBody(c) || isFieldLabel(c));
12060
+ if (userProvidedStructure) {
12061
+ content = children;
12062
+ }
12063
+ else {
12064
+ content = jsxRuntime.jsx(FieldBody, { children: children });
12065
+ }
12066
+ }
12067
+ return (jsxRuntime.jsx(FieldProvider, { value: true, children: jsxRuntime.jsx(FieldLabelIdProvider, { value: targetId, children: jsxRuntime.jsx(FieldLabelElementIdProvider, { value: targetId ? labelId : undefined, children: jsxRuntime.jsx(FieldLabelForProvider, { value: labelFor, children: jsxRuntime.jsx(FieldLabelForReportProvider, { value: targetId ? reportLabelFor : undefined, children: jsxRuntime.jsxs("div", { className: fieldClass, ...rest, children: [renderedLabel, content] }) }) }) }) }) }));
12068
+ };
12069
+ FieldLabel.displayName = 'FieldLabel';
12070
+ FieldBody.displayName = 'FieldBody';
12071
+ const Field = withSubComponents(FieldComponent, {
12072
+ Label: FieldLabel,
12073
+ Body: FieldBody,
12074
+ Control,
12075
+ }, 'Field');
12076
+
12077
+ /**
12078
+ * Bulma Input component with full Bulma helper class support.
12079
+ *
12080
+ * @function
12081
+ * @param {InputBaseProps} props - Props for the Input component.
12082
+ * @returns {JSX.Element} The rendered input element.
12083
+ * @see {@link https://bulma.io/documentation/form/input/ | Bulma Input documentation}
12084
+ */
12085
+ const InputBase = React.forwardRef(({ color, size, isRounded, isStatic, isHovered, isFocused, isLoading, className, disabled, readOnly, ...props }, ref) => {
12086
+ const { bulmaHelperClasses, rest } = useBulmaClasses({
12087
+ color,
12088
+ ...props,
12089
+ });
12090
+ // A labeled Field's generated id when no user id arrived (rest.id may be
12091
+ // an undefined own key), reported to that Field (#1004).
12092
+ const id = useFieldLabelTarget(rest.id);
12093
+ const mainClass = usePrefixedClassNames('input', {
12094
+ [`is-${color}`]: !!color,
12095
+ [`is-${size}`]: !!size,
12096
+ 'is-rounded': isRounded,
12097
+ 'is-static': isStatic,
12098
+ 'is-hovered': isHovered,
12099
+ 'is-focused': isFocused,
12100
+ 'is-loading': isLoading,
12101
+ });
12102
+ const inputClass = classNames(mainClass, bulmaHelperClasses, className);
12103
+ return (jsxRuntime.jsx("input", { ref: ref, className: inputClass, disabled: disabled, readOnly: readOnly, ...rest,
12104
+ // After the spread, which may carry an undefined id key.
12105
+ id: id }));
12106
+ });
12107
+ InputBase.displayName = 'InputBase';
12108
+
12109
+ /**
12110
+ * Associates the convenience `label` prop with its control (#368): generates
12111
+ * an id for the control and returns labelProps carrying a matching `htmlFor`.
12112
+ * A user-supplied `id` is used as the target instead of the generated one, and
12113
+ * an explicit `htmlFor` key in labelProps — even set to `undefined` — disables
12114
+ * generation entirely: the user has taken over the association (#495 presence
12115
+ * semantics). When the control renders no label of its own, it adopts the id
12116
+ * a labeled Field around it offers, the way the bases do, so that Field's
12117
+ * label names it (#939); a user `id` still wins there. It tells that Field
12118
+ * when the control took its id, since the Field's label drops a `for`
12119
+ * nothing took (#1004). With `needsLabelId` it also returns the id of
12120
+ * whichever label names the control (#981).
12121
+ * Internal; not part of the public API.
12122
+ */
12123
+ function useAutoLabelId({ label, id, labelProps, rendersLabel, hasInput = true, needsLabelId = false, }) {
12124
+ // Called unconditionally per the rules of hooks; SSR-safe on React 18 and 19.
12125
+ const generatedId = React.useId();
12126
+ // Read outside the control's own Field, so it is set only when an outer
12127
+ // Field holds the control, which then renders no label of its own.
12128
+ const fieldLabelId = useFieldLabelId();
12129
+ // What an outer Field's label points at, even when wired by hand, and the
12130
+ // id that label renders with, read together so the id is that label's.
12131
+ // Set in `grouped`/`hasAddons` Fields too when the label is wired by hand,
12132
+ // since its `for` names this one control whatever the layout (see Field).
12133
+ const fieldLabelFor = useFieldLabelFor();
12134
+ // Truthiness mirrors Field's own `if (label)` render gate.
12135
+ const active = !!label && rendersLabel;
12136
+ // Presence, not truthiness: `htmlFor: undefined` is an explicit opt-out and
12137
+ // must not leave an orphan generated id on the control.
12138
+ const userWired = !!labelProps && 'htmlFor' in labelProps;
12139
+ const adopted = !active && hasInput ? fieldLabelId : undefined;
12140
+ const controlId = id ?? (active && !userWired ? generatedId : adopted);
12141
+ // Tells a labeled Field around the control when its label's `for` names
12142
+ // the control, since it drops a `for` nothing took (#1004).
12143
+ useReportFieldLabelFor(fieldLabelId !== undefined && controlId === fieldLabelId);
12144
+ const ownLabelProps = { htmlFor: controlId, ...labelProps };
12145
+ const labelTarget = active ? ownLabelProps.htmlFor : fieldLabelFor.htmlFor;
12146
+ const labelled = !!controlId && labelTarget === controlId;
12147
+ // Only a label that names the control gets an id to point at, with the
12148
+ // suffix Field gives its own, from an id that is already unique.
12149
+ const ownLabelId = needsLabelId && active && labelled
12150
+ ? (labelProps?.id ?? `${generatedId}-label`)
12151
+ : undefined;
12152
+ // Inactive with a label still means an own Field may render it (pickers'
12153
+ // inline mode). The explicit `htmlFor: undefined` tells Field the
12154
+ // association is owned here, so it must not generate one that would
12155
+ // dangle (#495 presence semantics).
12156
+ const fieldLabelProps = active
12157
+ ? ownLabelId
12158
+ ? { ...ownLabelProps, id: ownLabelId }
12159
+ : ownLabelProps
12160
+ : label
12161
+ ? { htmlFor: undefined, ...labelProps }
12162
+ : labelProps;
12163
+ const labelId = needsLabelId && labelled
12164
+ ? active
12165
+ ? ownLabelId
12166
+ : fieldLabelFor.id
12167
+ : undefined;
12168
+ return { controlId, fieldLabelProps, labelled, labelId };
12169
+ }
12170
+ /**
12171
+ * Group-input counterpart of {@link useAutoLabelId} (#494): a group of
12172
+ * controls cannot take a single `htmlFor`, so instead the rendered `<label>`
12173
+ * gets a generated id and the group container points at it with
12174
+ * `aria-labelledby`. A user-supplied `labelProps.id` is used as the target
12175
+ * instead of generating one. Any caller `htmlFor` is stripped — a group label
12176
+ * names the group, never a single control — so the merged labelProps always
12177
+ * carry an explicit `htmlFor: undefined`. When the group renders no label of
12178
+ * its own, it points at the label of a labeled Field around it instead
12179
+ * (#939). A caller who named the group with `aria-label` or
12180
+ * `aria-labelledby` gets no `aria-labelledby` from either label, so their
12181
+ * name wins wherever the label sits.
12182
+ * Internal; not part of the public API.
12183
+ */
12184
+ function useAutoLabelledBy({ label, labelProps, rendersLabel, callerProps, }) {
12185
+ // Called unconditionally per the rules of hooks; SSR-safe on React 18 and 19.
12186
+ const generatedId = React.useId();
12187
+ // Set only when an outer Field holds the group (see useAutoLabelId).
12188
+ const fieldLabelElementId = useFieldLabelElementId();
12189
+ const active = !!label && rendersLabel;
12190
+ const labelId = labelProps?.id ?? (active ? generatedId : undefined);
12191
+ const fieldLabelProps = active
12192
+ ? { ...labelProps, id: labelId, htmlFor: undefined }
12193
+ : labelProps;
12194
+ const aria = callerProps;
12195
+ // Presence for aria-labelledby: the caller's key replaces the attribute
12196
+ // through the spread whatever its value, and pointing it at a label would
12197
+ // only take a Rate's fallback name away. Checked before either label, so
12198
+ // the caller's name wins over the group's own `label` as it does over a
12199
+ // Field's, the way an aria-label beats a `<label for>` on a single input.
12200
+ const callerNamed = !!aria['aria-label'] || 'aria-labelledby' in aria;
12201
+ const ariaLabelledBy = callerNamed
12202
+ ? undefined
12203
+ : active
12204
+ ? labelId
12205
+ : fieldLabelElementId;
12206
+ return { ariaLabelledBy, fieldLabelProps };
12207
+ }
12208
+
12209
+ /**
12210
+ * The `Input` component provides a Bulma-styled text input, supporting colors, sizes, rounded corners, static state, hover/focus/loading states, and all Bulma helper props.
12211
+ *
12212
+ * @function
12213
+ * @param {InputProps} props - Props for Input.
12214
+ * @returns {JSX.Element} The composed field element.
12215
+ *
12216
+ * @example
12217
+ * <Input label="Username" placeholder="Enter username" iconLeftName="user" />
12218
+ *
12219
+ * @example
12220
+ * <Input
12221
+ * label="Email"
12222
+ * type="email"
12223
+ * message="Please enter a valid email"
12224
+ * messageColor="danger"
12225
+ * color="danger"
12226
+ * />
12227
+ */
12228
+ const Input = React.forwardRef(({
12229
+ // Field props
12230
+ label, labelSize, labelProps, horizontal,
12231
+ // Control props
12232
+ iconLeft, iconRight, iconLeftName, iconRightName, iconLeftSize, iconRightSize, hasIconsLeft, hasIconsRight, isLoading, isExpanded, controlSize,
12233
+ // Message props
12234
+ message, messageColor,
12235
+ // Container class overrides
12236
+ fieldClassName, controlClassName,
12237
+ // Everything else goes to Input
12238
+ ...inputProps }, ref) => {
12239
+ const insideField = useInsideField();
12240
+ const insideControl = useInsideControl();
12241
+ const ownField = rendersOwnField('Input', {
12242
+ insideField,
12243
+ insideControl,
12244
+ label,
12245
+ message,
12246
+ horizontal,
12247
+ fieldClassName,
12248
+ });
12249
+ const ownControl = rendersOwnControl('Input', {
12250
+ insideControl,
12251
+ isLoading,
12252
+ iconLeft,
12253
+ iconLeftName,
12254
+ iconLeftSize,
12255
+ iconRight,
12256
+ iconRightName,
12257
+ iconRightSize,
12258
+ hasIconsLeft,
12259
+ hasIconsRight,
12260
+ isExpanded,
12261
+ controlSize,
12262
+ controlClassName,
12263
+ });
12264
+ const { controlId, fieldLabelProps } = useAutoLabelId({
12265
+ label,
12266
+ id: inputProps.id,
12267
+ labelProps,
12268
+ rendersLabel: ownField,
12269
+ });
12270
+ const helpClass = usePrefixedClassNames('help', {
12271
+ [`is-${messageColor}`]: !!messageColor,
12272
+ });
12273
+ let content = jsxRuntime.jsx(InputBase, { ref: ref, ...inputProps, id: controlId });
12274
+ if (ownControl) {
12275
+ content = (jsxRuntime.jsx(Control, { iconLeft: iconLeft, iconRight: iconRight, iconLeftName: iconLeftName, iconRightName: iconRightName, iconLeftSize: iconLeftSize, iconRightSize: iconRightSize, hasIconsLeft: hasIconsLeft, hasIconsRight: hasIconsRight, isLoading: isLoading, isExpanded: isExpanded, size: controlSize, className: controlClassName, children: content }));
12276
+ }
12277
+ const messageEl = message ? jsxRuntime.jsx("p", { className: helpClass, children: message }) : null;
12278
+ if (ownField) {
12279
+ return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
12280
+ }
12281
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [content, messageEl] }));
12282
+ });
12283
+ Input.displayName = 'Input';
12284
+
12285
+ /** What names a Command that's given no `label`. */
12286
+ const defaultLabel = 'Commands';
12287
+ /** What a Command given `actions` shows when nothing matches, unless it's given `empty`. */
12288
+ const defaultEmpty = 'No results';
12289
+ // What the status region announces: the `Command.Empty` content, as the one
12290
+ // item it's handed while that content is on screen because of the user's
12291
+ // typing.
12292
+ const typedNoMatches$2 = [{ id: 'empty' }];
12293
+ const nothingToAnnounce$2 = [];
12294
+ /** Whether a slot was given something to show. */
12295
+ const isShown = (node) => node !== undefined && node !== null && node !== false && node !== '';
12296
+ const noop$1 = () => { };
12297
+ const registerNothing$1 = () => noop$1;
12298
+ // What a part rendered outside a Command reads: nothing to report to, so it
12299
+ // renders its markup and does nothing.
12300
+ const standalone$1 = {
12301
+ engine: {
12302
+ open: false,
12303
+ expanded: false,
12304
+ highlighted: null,
12305
+ typed: false,
12306
+ listboxId: '',
12307
+ itemId: () => '',
12308
+ rootRef: { current: null },
12309
+ inputRef: { current: null },
12310
+ listboxRef: { current: null },
12311
+ registerPopup: registerNothing$1,
12312
+ registerItem: registerNothing$1,
12313
+ onKeyDown: noop$1,
12314
+ onChange: noop$1,
12315
+ onClick: noop$1,
12316
+ onBlur: noop$1,
12317
+ hover: noop$1,
12318
+ pick: noop$1,
12319
+ keepFocus: noop$1,
12320
+ markInside: noop$1,
12321
+ },
12322
+ label: defaultLabel,
12323
+ query: '',
12324
+ onFieldChange: noop$1,
12325
+ reportLoading: noop$1,
12326
+ reportEmpty: registerNothing$1,
12327
+ registerHandler: registerNothing$1,
12328
+ };
12329
+ const CommandContext = React.createContext(null);
12330
+ const useCommand = () => React.useContext(CommandContext) ?? standalone$1;
12331
+ /** Hands a node to a forwarded ref and to one of the component's own. */
12332
+ function setRefs$1(node, own, forwarded) {
12333
+ own.current = node;
12334
+ if (typeof forwarded === 'function')
12335
+ forwarded(node);
12336
+ else if (forwarded)
12337
+ forwarded.current = node;
12338
+ }
12339
+ /**
12340
+ * The default filter: the actions whose label, keywords or description hold
12341
+ * the search, ignoring case and the spaces around it, label matches first and
12342
+ * the order otherwise kept. An empty search keeps them all.
12343
+ */
12344
+ const filterActions = (query, actions) => {
12345
+ const term = query.trim().toLowerCase();
12346
+ if (term === '')
12347
+ return actions;
12348
+ const byLabel = [];
12349
+ const byOther = [];
12350
+ for (const action of actions) {
12351
+ if (action.label.toLowerCase().includes(term)) {
12352
+ byLabel.push(action);
12353
+ }
12354
+ else if (action.description?.toLowerCase().includes(term) ||
12355
+ action.keywords?.some(keyword => keyword.toLowerCase().includes(term))) {
12356
+ byOther.push(action);
12357
+ }
12358
+ }
12359
+ return [...byLabel, ...byOther];
12360
+ };
12361
+ /** The search icon at the start of the field. */
12362
+ const SearchIcon = () => (jsxRuntime.jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", width: "1em", height: "1em", focusable: "false", children: [jsxRuntime.jsx("circle", { cx: "7", cy: "7", r: "4.75" }), jsxRuntime.jsx("path", { d: "M10.5 10.5L14 14" })] }));
12363
+ /**
12364
+ * The Command built from parts: the field, the list, the keys and the run,
12365
+ * with the items and their filtering left to the app.
12366
+ */
12367
+ const CommandParts = React.forwardRef(({ children, label, query: controlledQuery, defaultQuery = '', onQueryChange, onSelect, className, ...props }, ref) => {
12368
+ const [ownQuery, setOwnQuery] = React.useState(defaultQuery);
12369
+ const query = controlledQuery ?? ownQuery;
12370
+ // Each item's `onSelect`, by value, read when it runs.
12371
+ const handlersRef = React.useRef(new Map());
12372
+ const registerHandler = React.useCallback((value, handler) => {
12373
+ const handlers = handlersRef.current;
12374
+ handlers.set(value, handler);
12375
+ return () => {
12376
+ if (handlers.get(value) === handler)
12377
+ handlers.delete(value);
12378
+ };
12379
+ }, []);
12380
+ const run = (value) => {
12381
+ handlersRef.current.get(value)?.current?.();
12382
+ onSelect?.(value);
12383
+ };
12384
+ // Inline: the list is always on screen, nothing opens or closes it, and
12385
+ // Escape goes on to what's around it. `keepFirst` keeps the highlight on
12386
+ // the top match, so Enter runs it.
12387
+ const engine = useComboboxEngine({
12388
+ defaultOpen: true,
12389
+ onSelect: run,
12390
+ keepFirst: true,
12391
+ multiple: false,
12392
+ inline: true,
12393
+ deferred: false,
12394
+ });
12395
+ const { bulmaHelperClasses, rest } = useBulmaClasses(props);
12396
+ const rootClass = usePrefixedClassNames('command');
12397
+ // A polite status region, in the page from the start so it's there
12398
+ // before anything is written into it, announces `Command.Empty` when the
12399
+ // user's typing leaves it on screen: once each time that happens, not
12400
+ // again on each keystroke while it stays. An empty search isn't a search,
12401
+ // so it stays quiet then, and while the field's `isLoading` says results
12402
+ // are on their way, it waits.
12403
+ const [inputLoading, setInputLoading] = React.useState(false);
12404
+ const emptyRef = React.useRef(null);
12405
+ const [emptyShown, setEmptyShown] = React.useState(false);
12406
+ const announced = engine.typed && query !== '' && emptyShown && !inputLoading
12407
+ ? typedNoMatches$2
12408
+ : nothingToAnnounce$2;
12409
+ // The region reads the text on screen, whatever the message renders.
12410
+ const describeEmpty = React.useCallback(() => {
12411
+ const node = emptyRef.current;
12412
+ /* istanbul ignore if: it's only announced while it's on screen */
12413
+ if (!node)
12414
+ return null;
12415
+ return spokenText(node);
12416
+ }, []);
12417
+ const reportEmpty = React.useCallback((element) => {
12418
+ emptyRef.current = element;
12419
+ setEmptyShown(true);
12420
+ return () => {
12421
+ emptyRef.current = null;
12422
+ setEmptyShown(false);
12423
+ };
12424
+ }, []);
12425
+ const onFieldChange = (value) => {
12426
+ if (controlledQuery === undefined)
12427
+ setOwnQuery(value);
12428
+ onQueryChange?.(value);
12429
+ engine.onChange();
12430
+ };
12431
+ // A new value each render: the engine's state and handlers are, and the
12432
+ // parts read the highlight from it.
12433
+ const context = {
12434
+ engine,
12435
+ label: label || defaultLabel,
12436
+ query,
12437
+ onFieldChange,
12438
+ reportLoading: setInputLoading,
12439
+ reportEmpty,
12440
+ registerHandler,
12441
+ };
12442
+ const { rootRef } = engine;
12443
+ const rootRefs = React.useCallback((node) => setRefs$1(node, rootRef, ref), [rootRef, ref]);
12444
+ return (jsxRuntime.jsxs("div", { className: classNames(rootClass, bulmaHelperClasses, className), ...rest, ref: rootRefs, children: [jsxRuntime.jsx(CommandContext.Provider, { value: context, children: children }), jsxRuntime.jsx(StatusRegion, { items: announced, describe: describeEmpty })] }));
12445
+ });
12446
+ CommandParts.displayName = 'CommandParts';
12447
+ /**
12448
+ * The search field, with the combobox role and the keys, rendered as an `Input` with a search icon.
12449
+ *
12450
+ * @function
12451
+ * @param {CommandInputProps} props - Props for the Command.Input component.
12452
+ * @param {React.Ref<HTMLInputElement>} ref - Forwarded ref to the `<input>`.
12453
+ * @returns {JSX.Element} The field in its Control.
12454
+ */
12455
+ const CommandInput = React.forwardRef(({ isLoading, iconLeft, iconLeftName, onKeyDown, onChange, onClick, onBlur, 'aria-autocomplete': ariaAutocomplete, ...props }, ref) => {
12456
+ const context = useCommand();
12457
+ const { engine, reportLoading } = context;
12458
+ useClientLayoutEffect(() => {
12459
+ reportLoading(!!isLoading);
12460
+ return () => reportLoading(false);
12461
+ }, [reportLoading, isLoading]);
12462
+ const { inputRef } = engine;
12463
+ const inputRefs = React.useCallback((node) => setRefs$1(node, inputRef, ref), [inputRef, ref]);
12464
+ const { expanded, highlighted } = engine;
12465
+ // A name the caller gave the field wins over the Command's `label`.
12466
+ const callerNamed = !!props['aria-label'] || 'aria-labelledby' in props;
12467
+ return (jsxRuntime.jsx(Input, { autoCorrect: "off", spellCheck: false, ...props, ref: inputRefs, value: context.query, iconLeft: iconLeft === undefined && !iconLeftName ? jsxRuntime.jsx(SearchIcon, {}) : iconLeft, iconLeftName: iconLeftName, isLoading: isLoading, "aria-label": callerNamed ? props['aria-label'] : context.label, role: "combobox", "aria-autocomplete": ariaAutocomplete ?? 'list', "aria-haspopup": "listbox", "aria-expanded": expanded, "aria-controls": expanded ? engine.listboxId : undefined, "aria-activedescendant": expanded && highlighted !== null
12468
+ ? engine.itemId(highlighted)
12469
+ : undefined, autoComplete: "off", onKeyDown: event => {
12470
+ onKeyDown?.(event);
12471
+ engine.onKeyDown(event);
12472
+ }, onChange: event => {
12473
+ onChange?.(event);
12474
+ context.onFieldChange(event.target.value);
12475
+ }, onClick: event => {
12476
+ onClick?.(event);
12477
+ if (!event.defaultPrevented)
12478
+ engine.onClick();
12479
+ }, onBlur: event => {
12480
+ onBlur?.(event);
12481
+ engine.onBlur(event);
12482
+ } }));
12483
+ });
12484
+ CommandInput.displayName = 'CommandInput';
12485
+ /**
12486
+ * The list of items, a `listbox` named by the Command's `label`.
12487
+ *
12488
+ * @function
12489
+ * @param {CommandListProps} props - Props for the Command.List component.
12490
+ * @returns {JSX.Element} The listbox.
12491
+ */
12492
+ const CommandList = ({ children, className, onMouseDown, ...props }) => {
12493
+ const { engine, label } = useCommand();
12494
+ const { listboxRef, listboxId, keepFocus } = engine;
12495
+ const listClass = usePrefixedClassNames('command-list');
12496
+ // A name the caller gave the list wins over the Command's.
12497
+ const callerNamed = !!props['aria-label'] || 'aria-labelledby' in props;
12498
+ return (jsxRuntime.jsx("div", { className: classNames(listClass, className), ...props, ref: listboxRef, id: listboxId || undefined, role: "listbox", "aria-label": callerNamed ? props['aria-label'] : label, onMouseDown: event => {
12499
+ onMouseDown?.(event);
12500
+ keepFocus(event);
12501
+ }, children: children }));
12502
+ };
12503
+ CommandList.displayName = 'CommandList';
12504
+ /**
12505
+ * Items under a heading, a `group` named by it.
12506
+ *
12507
+ * @function
12508
+ * @param {CommandGroupProps} props - Props for the Command.Group component.
12509
+ * @returns {JSX.Element} The group and its heading.
12510
+ */
12511
+ const CommandGroup = ({ label, children, className, ...props }) => {
12512
+ const labelId = React.useId();
12513
+ const groupClass = usePrefixedClassNames('command-group');
12514
+ const labelClass = usePrefixedClassNames('command-group-label');
12515
+ // A name the caller gave the group wins over its heading's, as on the list.
12516
+ const callerNamed = !!props['aria-label'] || 'aria-labelledby' in props;
12517
+ return (jsxRuntime.jsxs("div", { className: classNames(groupClass, className), ...props, role: "group", "aria-labelledby": callerNamed ? props['aria-labelledby'] : labelId, children: [jsxRuntime.jsx("div", { className: labelClass, id: labelId, children: label }), children] }));
12518
+ };
12519
+ CommandGroup.displayName = 'CommandGroup';
12520
+ /**
12521
+ * One thing to do in the list, named by its label and described by its description and hint.
12522
+ *
12523
+ * @function
12524
+ * @param {CommandItemProps} props - Props for the Command.Item component.
12525
+ * @returns {JSX.Element} The item.
12526
+ */
12527
+ const CommandItem = ({ value, disabled = false, onSelect, icon, description, hint, children, className, onClick, onPointerMove, ...props }) => {
12528
+ const { engine, registerHandler } = useCommand();
12529
+ const itemRef = React.useRef(null);
12530
+ const { registerItem } = engine;
12531
+ useClientLayoutEffect(() => registerItem(itemRef.current, {
12532
+ value,
12533
+ disabled,
12534
+ checked: false,
12535
+ }), [registerItem, value, disabled]);
12536
+ const onSelectRef = useLatestRef(onSelect);
12537
+ useClientLayoutEffect(() => registerHandler(value, onSelectRef), [registerHandler, value, onSelectRef]);
12538
+ const highlighted = engine.highlighted === value;
12539
+ const itemClass = usePrefixedClassNames('command-item', {
12540
+ 'is-active': highlighted,
12541
+ 'is-disabled': disabled,
12542
+ });
12543
+ const iconClass = usePrefixedClassNames('command-item-icon');
12544
+ const bodyClass = usePrefixedClassNames('command-item-body');
12545
+ const labelClass = usePrefixedClassNames('command-item-label');
12546
+ const descriptionClass = usePrefixedClassNames('command-item-description');
12547
+ const hintClass = usePrefixedClassNames('command-item-hint');
12548
+ // Named by its label alone, so a screen reader says what it does before
12549
+ // the rest, and described by the description and the hint.
12550
+ const baseId = React.useId();
12551
+ const labelId = `${baseId}label`;
12552
+ const descriptionId = isShown(description)
12553
+ ? `${baseId}description`
12554
+ : undefined;
12555
+ const hintId = isShown(hint) ? `${baseId}hint` : undefined;
12556
+ const describedBy = [descriptionId, hintId].filter(Boolean).join(' ') || undefined;
12557
+ const callerNamed = !!props['aria-label'] || 'aria-labelledby' in props;
12558
+ return (jsxRuntime.jsxs("div", { className: classNames(itemClass, className), "aria-describedby": describedBy, ...props, ref: itemRef, id: engine.itemId(value) || undefined, role: "option", "aria-selected": disabled ? undefined : highlighted || undefined, "aria-disabled": disabled || undefined, "aria-labelledby": callerNamed ? props['aria-labelledby'] : labelId, onClick: event => {
12559
+ onClick?.(event);
12560
+ if (!disabled && !event.defaultPrevented)
12561
+ engine.pick(value);
12562
+ }, onPointerMove: event => {
12563
+ onPointerMove?.(event);
12564
+ if (!disabled && !highlighted)
12565
+ engine.hover(value);
12566
+ }, children: [isShown(icon) && (jsxRuntime.jsx("span", { className: iconClass, "aria-hidden": "true", children: icon })), jsxRuntime.jsxs("span", { className: bodyClass, children: [jsxRuntime.jsx("span", { className: labelClass, id: labelId, children: children }), descriptionId && (jsxRuntime.jsx("span", { className: descriptionClass, id: descriptionId, children: description }))] }), hintId && (jsxRuntime.jsx("span", { className: hintClass, id: hintId, children: hint }))] }));
12567
+ };
12568
+ CommandItem.displayName = 'CommandItem';
12569
+ /**
12570
+ * The message shown in place of the list when nothing matches, announced when typing brings it on screen.
12571
+ *
12572
+ * @function
12573
+ * @param {CommandEmptyProps} props - Props for the Command.Empty component.
12574
+ * @returns {JSX.Element} The message.
12575
+ */
12576
+ const CommandEmpty = ({ children, className, onMouseDown, ...props }) => {
12577
+ const { engine, reportEmpty } = useCommand();
12578
+ const emptyRef = React.useRef(null);
12579
+ useClientLayoutEffect(() => reportEmpty(emptyRef.current), [reportEmpty]);
12580
+ const emptyClass = usePrefixedClassNames('command-empty');
12581
+ return (jsxRuntime.jsx("div", { className: classNames(emptyClass, className), ...props, ref: emptyRef, onMouseDown: event => {
12582
+ onMouseDown?.(event);
12583
+ engine.keepFocus(event);
12584
+ }, children: children }));
12585
+ };
12586
+ CommandEmpty.displayName = 'CommandEmpty';
12587
+ /**
12588
+ * The field, and the actions that match the search, grouped, or the empty
12589
+ * message. The search is the parts root's, read from its context.
12590
+ */
12591
+ const CommandActionsContent = ({ actions, filter = filterActions, placeholder, empty, }) => {
12592
+ const { query } = useCommand();
12593
+ const shown = filter(query, actions);
12594
+ // Each group where the matches first name it, and the actions with none
12595
+ // together where the first of them is, so the best match stays on top.
12596
+ const sections = new Map();
12597
+ for (const action of shown) {
12598
+ const group = action.group ?? '';
12599
+ sections.set(group, [...(sections.get(group) ?? []), action]);
12600
+ }
12601
+ const renderItem = (action) => (jsxRuntime.jsx(CommandItem, { value: action.id, disabled: action.disabled, icon: action.icon, description: action.description, hint: action.hint, onSelect: action.onSelect, children: action.label }, action.id));
12602
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(CommandInput, { placeholder: placeholder }), shown.length > 0 ? (jsxRuntime.jsx(CommandList, { children: Array.from(sections, ([group, members]) => group ? (jsxRuntime.jsx(CommandGroup, { label: group, children: members.map(renderItem) }, group)) : (members.map(renderItem))) })) : isShown(empty) ? (jsxRuntime.jsx(CommandEmpty, { children: empty })) : null] }));
12603
+ };
12604
+ /**
12605
+ * The Command given `actions`. It renders the parts itself and filters the
12606
+ * actions as the user types, so a run goes through the same parts, keys and
12607
+ * ARIA as one built by hand.
12608
+ */
12609
+ const CommandFromActions = React.forwardRef(({ actions, filter, placeholder, empty = defaultEmpty, children, ...rest }, ref) => {
12610
+ if (flattenChildren(children).length > 0) {
12611
+ warnOnce('Command:actions-children', '[bestax-bulma] <Command actions> renders its own field and items, ' +
12612
+ 'so its children are ignored. Give it actions or build it from ' +
12613
+ 'parts, not both.');
12614
+ }
12615
+ return (jsxRuntime.jsx(CommandParts, { ...rest, ref: ref, children: jsxRuntime.jsx(CommandActionsContent, { actions: actions, filter: filter, placeholder: placeholder, empty: empty }) }));
12616
+ });
12617
+ CommandFromActions.displayName = 'CommandFromActions';
12618
+ /**
12619
+ * The `Command` component is a search field over a list of things to do, which narrows as you type and runs the top match on Enter: inline by default, and a command palette in a modal as `Command.Dialog`.
12620
+ *
12621
+ * Given `actions`, it's one element. It renders the field, the list, the
12622
+ * groups and the empty message, and filters the actions by label, keywords
12623
+ * and description as the user types, with label matches first. `filter`
12624
+ * takes over the matching, for fuzzy search.
12625
+ *
12626
+ * Built from parts, the app owns the items: what's in the list and how it's
12627
+ * filtered, from the search `onQueryChange` reports. Nothing filters them,
12628
+ * so a server's answer shows as it came.
12629
+ *
12630
+ * Either way, focus stays in the field, and the highlighted item is its
12631
+ * `aria-activedescendant`. The first enabled item is highlighted whenever
12632
+ * the items change, so Enter runs the top match. Running an item calls its
12633
+ * own `onSelect`, then the Command's.
12634
+ *
12635
+ * When typing leaves the empty message on screen, its text is announced
12636
+ * through a polite status region, once, after any loading ends. Result
12637
+ * counts aren't announced: a screen reader reads the list's size from the
12638
+ * markup, and a count on every keystroke talks over the typing.
12639
+ *
12640
+ * @function
12641
+ * @param {CommandProps} props - Props for the Command component.
12642
+ * @param {React.Ref<HTMLDivElement>} ref - Forwarded ref to the `div.command`.
12643
+ * @returns {JSX.Element} The field and its list.
12644
+ *
12645
+ * @example
12646
+ * <Command
12647
+ * label="Settings"
12648
+ * placeholder="Search settings"
12649
+ * actions={[
12650
+ * { id: 'profile', label: 'Edit profile', onSelect: openProfile },
12651
+ * { id: 'theme', label: 'Dark mode', keywords: ['theme'], onSelect: toggleTheme },
12652
+ * ]}
12653
+ * />
12654
+ *
12655
+ * @example
12656
+ * <Command label="People" onQueryChange={setQuery} onSelect={openProfile}>
12657
+ * <Command.Input placeholder="Find a person" />
12658
+ * {matches.length > 0 ? (
12659
+ * <Command.List>
12660
+ * {matches.map(person => (
12661
+ * <Command.Item key={person.id} value={person.id}>
12662
+ * {person.name}
12663
+ * </Command.Item>
12664
+ * ))}
12665
+ * </Command.List>
12666
+ * ) : (
12667
+ * <Command.Empty>No one matches</Command.Empty>
12668
+ * )}
12669
+ * </Command>
12670
+ */
12671
+ const CommandRoot = React.forwardRef((props, ref) => props.actions === undefined ? (jsxRuntime.jsx(CommandParts, { ...props, ref: ref })) : (jsxRuntime.jsx(CommandFromActions, { ...props, ref: ref })));
12672
+ /** Other names a key goes by in a `shortcut`, for the ones `key` spells otherwise. */
12673
+ const keyAliases = {
12674
+ space: ' ',
12675
+ esc: 'escape',
12676
+ };
12677
+ /**
12678
+ * Reads a `shortcut` such as `"mod+k"` or `"ctrl + shift + p"`.
12679
+ *
12680
+ * @param shortcut - Modifiers and a key, joined by `+`.
12681
+ * @returns The combination, or `null` when it doesn't read as one.
12682
+ */
12683
+ const parseShortcut = (shortcut) => {
12684
+ const parts = shortcut
12685
+ .toLowerCase()
12686
+ .split('+')
12687
+ .map(part => part.trim());
12688
+ const key = parts.pop();
12689
+ if (key === '')
12690
+ return null;
12691
+ const parsed = {
12692
+ key: keyAliases[key] ?? key,
12693
+ mod: false,
12694
+ ctrl: false,
12695
+ meta: false,
12696
+ alt: false,
12697
+ shift: false,
12698
+ };
12699
+ for (const part of parts) {
12700
+ if (part === 'mod')
12701
+ parsed.mod = true;
12702
+ else if (part === 'ctrl' || part === 'control')
12703
+ parsed.ctrl = true;
12704
+ else if (part === 'meta' || part === 'cmd' || part === 'command')
12705
+ parsed.meta = true;
12706
+ else if (part === 'alt' || part === 'option')
12707
+ parsed.alt = true;
12708
+ else if (part === 'shift')
12709
+ parsed.shift = true;
12710
+ else
12711
+ return null;
12712
+ }
12713
+ return parsed;
12714
+ };
12715
+ /**
12716
+ * Whether a keydown is the combination. `mod` takes Ctrl or ⌘, and any other
12717
+ * modifier has to be as the combination says, so `mod+k` isn't
12718
+ * `mod+shift+k`. A letter or digit is matched by its key, and by where it
12719
+ * sits on the keyboard only when the key isn't a Latin letter or digit at
12720
+ * all, as on a Cyrillic layout or with Option held on a Mac, so another
12721
+ * Latin layout's own letters keep their own shortcuts.
12722
+ */
12723
+ const matchesShortcut = (event, shortcut) => {
12724
+ if (event.altKey !== shortcut.alt)
12725
+ return false;
12726
+ // A symbol such as `?` already says whether Shift was down, and which key
12727
+ // takes Shift to type it depends on the layout, so Shift is only checked
12728
+ // for a letter, a digit or a named key, unless the combination names it.
12729
+ const shiftMatters = shortcut.shift ||
12730
+ shortcut.key.length > 1 ||
12731
+ /^[a-z0-9]$/.test(shortcut.key);
12732
+ if (shiftMatters && event.shiftKey !== shortcut.shift)
12733
+ return false;
12734
+ if (shortcut.mod) {
12735
+ if (!event.ctrlKey && !event.metaKey)
12736
+ return false;
12737
+ }
12738
+ else if (event.ctrlKey !== shortcut.ctrl ||
12739
+ event.metaKey !== shortcut.meta) {
12740
+ return false;
12741
+ }
12742
+ const key = (event.key ?? '').toLowerCase();
12743
+ if (key === shortcut.key)
12744
+ return true;
12745
+ if (/^[a-z0-9]$/.test(key))
12746
+ return false;
12747
+ if (/^[a-z]$/.test(shortcut.key)) {
12748
+ return event.code === `Key${shortcut.key.toUpperCase()}`;
12749
+ }
12750
+ if (/^[0-9]$/.test(shortcut.key)) {
12751
+ return event.code === `Digit${shortcut.key}`;
12752
+ }
12753
+ return false;
12754
+ };
12755
+ /** Input types that take no typing, so a shortcut pressed in one still counts. */
12756
+ const untypedInputs = new Set([
12757
+ 'button',
12758
+ 'checkbox',
12759
+ 'color',
12760
+ 'file',
12761
+ 'hidden',
12762
+ 'image',
12763
+ 'radio',
12764
+ 'range',
12765
+ 'reset',
12766
+ 'submit',
12767
+ ]);
12768
+ /**
12769
+ * Whether a keydown happened while the user was typing: in a text field, a
12770
+ * text area, a select, or editable content. The composed path's first entry
12771
+ * is the element itself, even inside a shadow root.
12772
+ */
12773
+ const isTyping = (event) => {
12774
+ const target = event.composedPath()[0];
12775
+ if (typeof target?.closest !== 'function')
12776
+ return false;
12777
+ const element = target;
12778
+ const editable = element.closest('[contenteditable]');
12779
+ if (editable && editable.getAttribute('contenteditable') !== 'false') {
12780
+ return true;
12781
+ }
12782
+ const tag = element.tagName;
12783
+ if (tag === 'TEXTAREA' || tag === 'SELECT')
12784
+ return true;
12785
+ return (tag === 'INPUT' &&
12786
+ !untypedInputs.has(element.type.toLowerCase()));
12787
+ };
12788
+ /**
12789
+ * The Command in a modal, a command palette, opened by its `shortcut` or by `open`, that closes once an item has run.
12790
+ *
12791
+ * It renders nothing while it's closed. Open, it's a `Modal` with a
12792
+ * background and the Command's box as the `dialog`, named by `label`:
12793
+ * focus goes to the search field, Tab stays inside, Escape and a click on
12794
+ * the background close it, the page behind it doesn't scroll, and focus goes
12795
+ * back to where it was when it closes.
12796
+ *
12797
+ * @function
12798
+ * @param {CommandDialogProps} props - Props for the Command.Dialog component.
12799
+ * @param {React.Ref<HTMLDivElement>} ref - Forwarded ref to the `div.command` that is the dialog.
12800
+ * @returns {JSX.Element | null} The dialog, or nothing while it's closed.
12801
+ */
12802
+ const CommandDialog = React.forwardRef(({ open: controlledOpen, defaultOpen = false, onOpenChange, shortcut, closeOnSelect = true, clearOnClose = true, portal, label, query: controlledQuery, defaultQuery = '', onQueryChange, onSelect, className, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, ...props }, ref) => {
12803
+ const [ownOpen, setOwnOpen] = React.useState(defaultOpen);
12804
+ const open = controlledOpen ?? ownOpen;
12805
+ // Only ever asked for the other state: the shortcut listens while it's
12806
+ // closed, and everything that closes it is there while it's open.
12807
+ const setOpen = (next) => {
12808
+ if (controlledOpen === undefined)
12809
+ setOwnOpen(next);
12810
+ onOpenChange?.(next);
12811
+ };
12812
+ // The search is held here rather than in the Command, which is only
12813
+ // mounted while the dialog is open, so it can outlive a close when
12814
+ // `clearOnClose` is off.
12815
+ const [ownQuery, setOwnQuery] = React.useState(defaultQuery);
12816
+ const query = controlledQuery ?? ownQuery;
12817
+ const setQuery = (next) => {
12818
+ if (controlledQuery === undefined)
12819
+ setOwnQuery(next);
12820
+ onQueryChange?.(next);
12821
+ };
12822
+ // Closing clears the search, however the dialog closed. Our own is
12823
+ // cleared during render, React's pattern for adjusting state when other
12824
+ // state changes, and a controlled one is asked for after the commit.
12825
+ const [shownOpen, setShownOpen] = React.useState(open);
12826
+ if (shownOpen !== open) {
12827
+ setShownOpen(open);
12828
+ if (!open && clearOnClose && controlledQuery === undefined) {
12829
+ setOwnQuery('');
12830
+ }
12831
+ }
12832
+ const wasOpenRef = React.useRef(open);
12833
+ const clearRef = useLatestRef(() => {
12834
+ if (clearOnClose && controlledQuery !== undefined && query !== '') {
12835
+ onQueryChange?.('');
12836
+ }
12837
+ });
12838
+ React.useEffect(() => {
12839
+ const wasOpen = wasOpenRef.current;
12840
+ wasOpenRef.current = open;
12841
+ if (wasOpen && !open)
12842
+ clearRef.current();
12843
+ }, [open, clearRef]);
12844
+ if (controlledOpen === undefined && !defaultOpen && !shortcut) {
12845
+ warnOnce('Command.Dialog:no-opener', '[bestax-bulma] <Command.Dialog> has no way to open. Give it a ' +
12846
+ 'shortcut, or open and onOpenChange.');
12847
+ }
12848
+ if (shortcut && !parseShortcut(shortcut)) {
12849
+ warnOnce(`Command.Dialog:shortcut:${shortcut}`, `[bestax-bulma] <Command.Dialog> can't read the shortcut ` +
12850
+ `"${shortcut}". Write modifiers and a key joined by +, such as ` +
12851
+ `"mod+k", where the modifiers are mod, ctrl, meta, alt and shift.`);
12852
+ }
12853
+ const openFromShortcut = useLatestRef(() => setOpen(true));
12854
+ React.useEffect(() => {
12855
+ const combination = shortcut ? parseShortcut(shortcut) : null;
12856
+ if (!combination || open)
12857
+ return undefined;
12858
+ const onKeyDown = (event) => {
12859
+ if (event.defaultPrevented ||
12860
+ event.isComposing ||
12861
+ event.keyCode === 229 ||
12862
+ !matchesShortcut(event, combination) ||
12863
+ isTyping(event) ||
12864
+ // A Modal, a Dialog, a Sidebar with its overlay or a full-page
12865
+ // Loading has the page shut off, so the key isn't the page's. A
12866
+ // Sidebar without one leaves the page in use, so it still opens.
12867
+ isModalOverlayOpen()) {
12868
+ return;
12869
+ }
12870
+ // The browser's own use of the combination, such as Ctrl+K's search
12871
+ // in the address bar, doesn't happen as well.
12872
+ event.preventDefault();
12873
+ openFromShortcut.current();
12874
+ };
12875
+ // On `window`, where a keydown arrives last, so anything on the page
12876
+ // that used the key first has said so with `preventDefault()`.
12877
+ window.addEventListener('keydown', onKeyDown);
12878
+ return () => window.removeEventListener('keydown', onKeyDown);
12879
+ }, [shortcut, open, openFromShortcut]);
12880
+ const dialogClass = usePrefixedClassNames('command-dialog');
12881
+ if (!open)
12882
+ return null;
12883
+ const close = () => setOpen(false);
12884
+ const run = (value) => {
12885
+ onSelect?.(value);
12886
+ if (closeOnSelect)
12887
+ close();
12888
+ };
12889
+ return (jsxRuntime.jsxs(Modal, { isActive: true, onClose: close, role: "presentation", portal: portal, children: [jsxRuntime.jsx(Modal.Background, { onClick: close }), jsxRuntime.jsx(CommandRoot, { ...props, ref: ref, className: classNames(dialogClass, className), label: label, query: query, onQueryChange: setQuery, onSelect: run, role: "dialog", "aria-modal": "true", "aria-label": ariaLabelledBy === undefined
12890
+ ? ariaLabel || label || defaultLabel
12891
+ : ariaLabel, "aria-labelledby": ariaLabelledBy })] }));
12892
+ });
12893
+ CommandDialog.displayName = 'CommandDialog';
12894
+ /** Command with Input, List, Group, Item, Empty and Dialog sub-components. */
12895
+ const Command = withSubComponents(CommandRoot, {
12896
+ Input: CommandInput,
12897
+ List: CommandList,
12898
+ Group: CommandGroup,
12899
+ Item: CommandItem,
12900
+ Empty: CommandEmpty,
12901
+ Dialog: CommandDialog,
12902
+ }, 'Command');
12903
+
12904
+ /**
12905
+ * The `Block` component renders a simple container with Bulma's `.block` class, adding vertical margin between sections of content.
12906
+ *
12907
+ * @function
12908
+ * @param {BlockProps} props - Props for the Block component.
12909
+ * @returns {JSX.Element} The rendered block element.
12910
+ * @see {@link https://bulma.io/documentation/elements/block/ | Bulma Block documentation}
12911
+ */
12912
+ const Block = ({ className, textColor, color, bgColor, children, ...props }) => {
12913
+ /**
12914
+ * Generates Bulma helper classes and separates out remaining props.
12915
+ */
12916
+ const { bulmaHelperClasses, rest } = useBulmaClasses({
12917
+ color: textColor ?? color,
12918
+ backgroundColor: bgColor,
12919
+ ...props,
12920
+ });
12921
+ const bulmaClasses = usePrefixedClassNames('block');
12922
+ const blockClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
12923
+ return (jsxRuntime.jsx("div", { className: blockClasses, ...rest, children: children }));
12924
+ };
12925
+
12926
+ /**
12927
+ * The `Box` component renders a bordered, padded container with an optional shadow using Bulma's `.box` class.
12928
+ *
12929
+ * @function
12930
+ * @param {BoxProps} props - Props for the Box component.
12931
+ * @returns {JSX.Element} The rendered box element.
12932
+ * @see {@link https://bulma.io/documentation/elements/box/ | Bulma Box documentation}
12933
+ */
12934
+ const Box = ({ className, textColor, color, bgColor, hasShadow = true, children, style, ...props }) => {
12935
+ /**
12936
+ * Generates Bulma helper classes and separates out remaining props.
12937
+ */
12938
+ const { bulmaHelperClasses, bulmaHelperStyles, rest } = useBulmaClasses({
12939
+ color: textColor ?? color,
12940
+ backgroundColor: bgColor,
12941
+ ...props,
12942
+ });
12943
+ const bulmaClasses = usePrefixedClassNames('box', {
12944
+ 'is-shadowless': !hasShadow,
12945
+ });
12946
+ const boxClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
12947
+ return (jsxRuntime.jsx("div", { className: boxClasses, style: mergeBulmaStyles(bulmaHelperStyles, style), ...rest, children: children }));
12948
+ };
12949
+
12950
+ /**
12951
+ * The `Code` component renders a styled `<code>` element with Bulma helper class integration.
12952
+ *
12953
+ * @function
12954
+ * @param {CodeProps} props - Props for the Code component.
12955
+ * @returns {JSX.Element} The rendered code element.
12956
+ */
12957
+ const Code = ({ className, textColor, bgColor, children, ...props }) => {
12958
+ const { bulmaHelperClasses, rest } = useBulmaClasses({
12959
+ color: textColor,
12960
+ backgroundColor: bgColor,
12961
+ ...props,
12962
+ });
12963
+ const bulmaClasses = usePrefixedClassNames();
12964
+ const codeClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
12965
+ return (jsxRuntime.jsx("code", { className: codeClasses || undefined, ...rest, children: children }));
12966
+ };
12967
+
12968
+ // Valid size modifiers for the content class
12969
+ const validSizes = ['small', 'medium', 'large'];
12970
+ /**
12971
+ * The `Content` component applies Bulma’s typographic styles to its children, enhancing the appearance of HTML elements like paragraphs, headings, lists, and tables.
12972
+ *
12973
+ * @function
12974
+ * @param {ContentProps} props - Props for the Content component.
12975
+ * @returns {JSX.Element} The rendered content block.
12976
+ * @see {@link https://bulma.io/documentation/elements/content/ | Bulma Content documentation}
12977
+ */
12978
+ const Content = ({ className, textColor, color, bgColor, size, children, ...props }) => {
12979
+ /**
12980
+ * Generates Bulma helper classes and separates out remaining props.
12981
+ */
12982
+ const { bulmaHelperClasses, rest } = useBulmaClasses({
12983
+ color: textColor ?? color,
12984
+ backgroundColor: bgColor,
12985
+ ...props,
12986
+ });
12987
+ const bulmaClasses = usePrefixedClassNames('content', {
12988
+ [`is-${size}`]: size && size !== 'normal' && validSizes.includes(size),
12989
+ });
12990
+ const contentClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
12991
+ return (jsxRuntime.jsx("div", { className: contentClasses, ...rest, children: children }));
12992
+ };
12993
+
12994
+ /**
12995
+ * The `Delete` component provides a Bulma-styled close/delete button for dismissing modals, notifications, tags, messages, and more.
12996
+ *
12997
+ * It renders `type="button"`, so a delete button inside a form does not submit it.
12998
+ *
12999
+ * @function
13000
+ * @param {DeleteProps} props - Props for the Delete component.
13001
+ * @returns {JSX.Element} The rendered delete button.
13002
+ * @see {@link https://bulma.io/documentation/elements/delete/ | Bulma Delete documentation}
13003
+ */
13004
+ const Delete = ({ className, textColor, color, bgColor, onClick, size, ariaLabel, disabled = false, ...props }) => {
13005
+ /**
13006
+ * Generates Bulma helper classes and separates out remaining props.
13007
+ */
13008
+ const { bulmaHelperClasses, rest } = useBulmaClasses({
13009
+ color: textColor ?? color,
13010
+ backgroundColor: bgColor,
13011
+ ...props,
13012
+ });
13013
+ const bulmaClasses = usePrefixedClassNames('delete', {
13014
+ [`is-${size}`]: size,
13015
+ 'is-disabled': disabled,
13016
+ });
13017
+ const classes = classNames(bulmaClasses, bulmaHelperClasses, className);
13018
+ return (jsxRuntime.jsx("button", { className: classes, onClick: onClick, disabled: disabled, ...rest, "aria-label": rest['aria-label'] || ariaLabel || 'Close', type: buttonType(rest.type) }));
13019
+ };
13020
+
13021
+ const validDescriptionListOrientations = ['vertical', 'horizontal'];
13022
+ const isOrientation = (orientation) => !!orientation &&
13023
+ validDescriptionListOrientations.includes(orientation);
13024
+ /** Whether a prop was given something to render. */
13025
+ const isGiven = (node) => node !== undefined &&
13026
+ node !== null &&
13027
+ typeof node !== 'boolean' &&
13028
+ node !== '';
13029
+ /**
13030
+ * A label, the name of a fact: a `<dt>`, in the weaker text color.
13031
+ *
13032
+ * @function
13033
+ * @param {DescriptionListLabelProps} props - Props for DescriptionList.Label.
13034
+ * @param {React.Ref<HTMLElement>} ref - Forwarded ref to the `<dt>`.
13035
+ * @returns {JSX.Element} The rendered label.
13036
+ *
13037
+ * @example
13038
+ * <DescriptionList.Label>Placed on</DescriptionList.Label>
13039
+ */
13040
+ const DescriptionListLabel = React.forwardRef(({ className, textColor, bgColor, children, ...props }, ref) => {
13041
+ const { bulmaHelperClasses, rest } = useBulmaClasses({
13042
+ color: textColor,
13043
+ backgroundColor: bgColor,
13044
+ ...props,
13045
+ });
13046
+ const labelClass = usePrefixedClassNames('description-list-label');
13047
+ return (jsxRuntime.jsx("dt", { ref: ref, className: classNames(labelClass, bulmaHelperClasses, className), ...rest, children: children }));
13048
+ });
13049
+ DescriptionListLabel.displayName = 'DescriptionList.Label';
13050
+ /**
13051
+ * A value, what the label says about the thing: a `<dd>`. An item may have several.
13052
+ *
13053
+ * @function
13054
+ * @param {DescriptionListValueProps} props - Props for DescriptionList.Value.
13055
+ * @param {React.Ref<HTMLElement>} ref - Forwarded ref to the `<dd>`.
13056
+ * @returns {JSX.Element} The rendered value.
13057
+ *
13058
+ * @example
13059
+ * <DescriptionList.Value>14 October 2026</DescriptionList.Value>
13060
+ */
13061
+ const DescriptionListValue = React.forwardRef(({ className, textColor, bgColor, children, ...props }, ref) => {
13062
+ const { bulmaHelperClasses, rest } = useBulmaClasses({
13063
+ color: textColor,
13064
+ backgroundColor: bgColor,
13065
+ ...props,
13066
+ });
13067
+ const valueClass = usePrefixedClassNames('description-list-value');
13068
+ return (jsxRuntime.jsx("dd", { ref: ref, className: classNames(valueClass, bulmaHelperClasses, className), ...rest, children: children }));
13069
+ });
13070
+ DescriptionListValue.displayName = 'DescriptionList.Value';
13071
+ /**
13072
+ * One fact: its label and its value, in a `<div>`, from the `label` and `value` props or from `Label` and `Value` children.
13073
+ *
13074
+ * @function
13075
+ * @param {DescriptionListItemProps} props - Props for DescriptionList.Item.
13076
+ * @param {React.Ref<HTMLDivElement>} ref - Forwarded ref to the `<div>`.
13077
+ * @returns {JSX.Element} The rendered item.
13078
+ *
13079
+ * @example
13080
+ * <DescriptionList.Item label="Order" value="#1234" />
13081
+ *
13082
+ * @example
13083
+ * <DescriptionList.Item label="Tags">
13084
+ * <DescriptionList.Value>Gift</DescriptionList.Value>
13085
+ * <DescriptionList.Value>Priority</DescriptionList.Value>
13086
+ * </DescriptionList.Item>
13087
+ */
13088
+ const DescriptionListItem = React.forwardRef(({ className, label, value, textColor, bgColor, children, ...props }, ref) => {
13089
+ const { bulmaHelperClasses, rest } = useBulmaClasses({
13090
+ color: textColor,
13091
+ backgroundColor: bgColor,
13092
+ ...props,
13093
+ });
13094
+ const itemClass = usePrefixedClassNames('description-list-item');
13095
+ return (jsxRuntime.jsxs("div", { ref: ref, className: classNames(itemClass, bulmaHelperClasses, className), ...rest, children: [isGiven(label) && jsxRuntime.jsx(DescriptionListLabel, { children: label }), children, isGiven(value) && jsxRuntime.jsx(DescriptionListValue, { children: value })] }));
13096
+ });
13097
+ DescriptionListItem.displayName = 'DescriptionList.Item';
13098
+ /**
13099
+ * The `DescriptionList` component lists facts about one thing, each a label and its value, such as an order's details or a profile.
13100
+ *
13101
+ * It's a `<dl>`, with a `<div>` for each item, a `<dt>` for each label and
13102
+ * a `<dd>` for each value, so assistive tech gets HTML's own list of terms
13103
+ * and definitions and no roles are added. Labels sit above their values on
13104
+ * a phone and beside them from Bulma's tablet breakpoint up, where every
13105
+ * label shares one column so the values line up. For data with two
13106
+ * dimensions, such as several records with the same fields, use a `Table`.
13107
+ *
13108
+ * @function
13109
+ * @param {DescriptionListProps} props - Props for the DescriptionList component.
13110
+ * @param {React.Ref<HTMLDListElement>} ref - Forwarded ref to the `<dl>`.
13111
+ * @returns {JSX.Element} The rendered list.
13112
+ *
13113
+ * @example
13114
+ * <DescriptionList aria-labelledby="order-heading">
13115
+ * <DescriptionList.Item label="Order" value="#1234" />
13116
+ * <DescriptionList.Item label="Placed on" value="14 October 2026" />
13117
+ * </DescriptionList>
13118
+ */
13119
+ const DescriptionListRoot = React.forwardRef(({ className, orientation, orientationMobile, orientationTablet, orientationDesktop, orientationWidescreen, orientationFullhd, hasDividers, textColor, bgColor, children, ...props }, ref) => {
13120
+ const { bulmaHelperClasses, rest } = useBulmaClasses({
13121
+ color: textColor,
13122
+ backgroundColor: bgColor,
13123
+ ...props,
13124
+ });
13125
+ // A class for each orientation given, which the stylesheet applies from
13126
+ // that viewport's breakpoint, so the layout is right on the first paint
13127
+ // and on the server. With none, the stylesheet's default is responsive.
13128
+ const rootClass = usePrefixedClassNames('description-list', {
13129
+ [`is-${orientation}`]: isOrientation(orientation),
13130
+ [`is-${orientationMobile}-mobile`]: isOrientation(orientationMobile),
13131
+ [`is-${orientationTablet}-tablet`]: isOrientation(orientationTablet),
13132
+ [`is-${orientationDesktop}-desktop`]: isOrientation(orientationDesktop),
13133
+ [`is-${orientationWidescreen}-widescreen`]: isOrientation(orientationWidescreen),
13134
+ [`is-${orientationFullhd}-fullhd`]: isOrientation(orientationFullhd),
13135
+ 'has-dividers': !!hasDividers,
13136
+ });
13137
+ return (jsxRuntime.jsx("dl", { ref: ref, className: classNames(rootClass, bulmaHelperClasses, className), ...rest, children: children }));
13138
+ });
13139
+ const DescriptionList = withSubComponents(DescriptionListRoot, {
13140
+ Item: DescriptionListItem,
13141
+ Label: DescriptionListLabel,
13142
+ Value: DescriptionListValue,
13143
+ }, 'DescriptionList');
13144
+
13145
+ /**
13146
+ * The `Divider` component renders a styled horizontal rule (`<hr>`) element with Bulma helper class integration.
13147
+ *
13148
+ * @function
13149
+ * @param {DividerProps} props - Props for the Divider component.
13150
+ * @returns {JSX.Element} The rendered hr element.
11551
13151
  */
11552
13152
  const Divider = ({ className, bgColor, ...props }) => {
11553
13153
  /**
@@ -11615,64 +13215,20 @@ const FigureComponent = ({ className, textColor, bgColor, children, ...props })
11615
13215
  });
11616
13216
  const bulmaClasses = usePrefixedClassNames();
11617
13217
  const figureClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
11618
- return (jsxRuntime.jsx("figure", { className: figureClasses || undefined, ...rest, children: children }));
11619
- };
11620
- /**
11621
- * Figure component with Caption subcomponent.
11622
- *
11623
- * @example
11624
- * <Figure>
11625
- * <img src="image.jpg" alt="Description" />
11626
- * <Figure.Caption>Image caption text</Figure.Caption>
11627
- * </Figure>
11628
- */
11629
- const Figure = withSubComponents(FigureComponent, {
11630
- Caption: FigureCaption,
11631
- }, 'Figure');
11632
-
11633
- /**
11634
- * Distinguishes an `IconProps` object from a plain custom node (an inline SVG, a
11635
- * `react-icons` component, …) in a slot that accepts either — `Control`'s
11636
- * `iconLeft`/`iconRight`, `IconText`'s `iconProps`.
11637
- *
11638
- * One copy, imported by both consumers: two hand-maintained copies, one per
11639
- * consumer, is how the deprecated `icon` path came to crash both (#663).
11640
- *
11641
- * Its own module rather than `Icon.tsx` because `src/index.ts` wildcard-exports
11642
- * that file, and everything published there is public forever. `polymorphic.ts`
11643
- * makes the same call for `isCustomElement`: an internal discriminator is not
11644
- * something to support for the life of the package.
11645
- *
11646
- * It tests each member's VALUE, not merely its key, and only after everything
11647
- * React owns is out of the way. `'icon' in value` was enough
11648
- * to claim a Font Awesome `IconDefinition` — `{ prefix, iconName, icon: [w, h, …,
11649
- * path] }` — whose `icon` is a path array rather than a class string. That object
11650
- * is not a renderable node either, so the honest outcome is the one this slot has
11651
- * always had for it: the node branch, and React's own "Objects are not valid as a
11652
- * React child". Claiming it here instead rendered a blank container that still
11653
- * reserved layout.
11654
- *
11655
- * A member added to `IconProps` needs a line here, and a missed one is a crash
11656
- * rather than a fallthrough. That is the cost of a slot taking either a props
11657
- * object or a node; keeping the guard in one place is what keeps the cost to a
11658
- * single line.
11659
- */
11660
- function isIconProps(value) {
11661
- if (typeof value !== 'object' || value === null)
11662
- return false;
11663
- // Anything React owns. `isValidElement` covers elements only; a PORTAL is a
11664
- // valid `ReactNode` that fails it and carries a `children` key, so the member
11665
- // test below claimed it and `Control` spread it into `<Icon>` as props —
11666
- // rendering the portal's child locally instead of in its container. Every
11667
- // React internal carries `$$typeof`, so testing for it covers lazy, context
11668
- // and whatever React adds next, rather than portals alone.
11669
- if (React.isValidElement(value) || '$$typeof' in value)
11670
- return false;
11671
- const candidate = value;
11672
- return (typeof candidate.name === 'string' ||
11673
- typeof candidate.icon === 'string' ||
11674
- 'children' in candidate);
11675
- }
13218
+ return (jsxRuntime.jsx("figure", { className: figureClasses || undefined, ...rest, children: children }));
13219
+ };
13220
+ /**
13221
+ * Figure component with Caption subcomponent.
13222
+ *
13223
+ * @example
13224
+ * <Figure>
13225
+ * <img src="image.jpg" alt="Description" />
13226
+ * <Figure.Caption>Image caption text</Figure.Caption>
13227
+ * </Figure>
13228
+ */
13229
+ const Figure = withSubComponents(FigureComponent, {
13230
+ Caption: FigureCaption,
13231
+ }, 'Figure');
11676
13232
 
11677
13233
  /**
11678
13234
  * The `IconText` component provides a Bulma-styled horizontal arrangement of one or more `Icon` components and optional text.
@@ -12757,392 +14313,150 @@ const UnorderedListComponent = ({ className, textColor, bgColor, children, ...pr
12757
14313
  backgroundColor: bgColor,
12758
14314
  ...props,
12759
14315
  });
12760
- const bulmaClasses = usePrefixedClassNames();
12761
- const listClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
12762
- return (jsxRuntime.jsx("ul", { className: listClasses || undefined, ...rest, children: children }));
12763
- };
12764
- const UnorderedList = withSubComponents(UnorderedListComponent, { Item: ListItem }, 'UnorderedList');
12765
-
12766
- // Whether the nearest enclosing `Control` shows its `isLoading` spinner. The
12767
- // picker bases read it so their right-side launcher gives way to that
12768
- // spinner, which sits at the same edge, whether the `Control` is the one a
12769
- // convenience input renders or one the caller wrapped around the input.
12770
- // Autocomplete reads it for its clear button, which sits there too.
12771
- // Kept apart from `FormContext`, whose contexts only report whether a `Field`
12772
- // or `Control` is present. Internal; not part of the public API.
12773
- const ControlLoadingContext = React.createContext(false);
12774
- /** Provider for the loading state, used internally by `Control`. */
12775
- const ControlLoadingProvider = ControlLoadingContext.Provider;
12776
- /** Whether the nearest enclosing `Control` is loading; `false` outside one. */
12777
- const useControlLoading = () => React.useContext(ControlLoadingContext);
12778
-
12779
- const allowedColors = [...validColors, 'inherit', 'current'];
12780
- /**
12781
- * The `Control` component is a Bulma-styled wrapper for form controls (`Input`, `Select`, `TextArea`, etc.), supporting icons (left/right), loading state, expansion, size, and Bulma helper props for layout and color.
12782
- *
12783
- * @function
12784
- * @param {ControlProps} props - Props for the Control component.
12785
- * @returns {JSX.Element} The rendered control container.
12786
- * @see {@link https://bulma.io/documentation/form/general/#control | Bulma Control documentation}
12787
- *
12788
- * @example
12789
- * // Control with left icon
12790
- * <Control iconLeftName="envelope" iconLeftSize="small">
12791
- * <input className="input" type="email" placeholder="Email" />
12792
- * </Control>
12793
- */
12794
- const Control = React.forwardRef(({ as = 'div', hasIconsLeft, hasIconsRight, isLoading, isExpanded, size, textColor, bgColor, iconLeft, iconRight, iconLeftName, iconLeftSize, iconRightName, iconRightSize, className, children, ...props }, ref) => {
12795
- const Component = (as === 'p' ? 'p' : 'div');
12796
- const { classPrefix } = useConfig();
12797
- // Remove textColor/bgColor from props before spreading
12798
- const { textColor: _ignoredTextColor, bgColor: _ignoredBgColor, ...restProps } = props;
12799
- const safeTextColor = allowedColors.includes(textColor)
12800
- ? textColor
12801
- : undefined;
12802
- const safeBgColor = allowedColors.includes(bgColor)
12803
- ? bgColor
12804
- : undefined;
12805
- const { bulmaHelperClasses, rest } = useBulmaClasses({
12806
- color: safeTextColor,
12807
- backgroundColor: safeBgColor,
12808
- ...restProps,
12809
- });
12810
- // Prepare icon props for the shortcut. A truthiness test (not `!== undefined`)
12811
- // so a falsy node from the idiomatic `iconLeft={cond && <Node/>}` / `iconLeft={maybe ?? null}`
12812
- // pattern counts as "no icon" — otherwise `false`/`null` would reserve the icon column and
12813
- // mount an empty `.icon` span. A falsy `iconLeft` also correctly falls through to the
12814
- // `iconLeftName` shortcut, matching the pre-node behavior.
12815
- const leftIconValue = iconLeft ||
12816
- (iconLeftName
12817
- ? {
12818
- name: iconLeftName,
12819
- size: iconLeftSize,
12820
- }
12821
- : undefined);
12822
- const rightIconValue = iconRight ||
12823
- (iconRightName
12824
- ? {
12825
- name: iconRightName,
12826
- size: iconRightSize,
12827
- }
12828
- : undefined);
12829
- const mainClass = usePrefixedClassNames('control', {
12830
- 'has-icons-left': hasIconsLeft || !!leftIconValue,
12831
- 'has-icons-right': hasIconsRight || !!rightIconValue,
12832
- 'is-loading': isLoading,
12833
- 'is-expanded': isExpanded,
12834
- [`is-${size}`]: !!size,
12835
- });
12836
- const controlClass = classNames(mainClass, bulmaHelperClasses, className);
12837
- // --- FIX: Spread both restProps (for data-testid, etc) AND rest (from useBulmaClasses) ---
12838
- return (jsxRuntime.jsx(ControlProvider, { value: true, children: jsxRuntime.jsxs(Component, { className: controlClass, ref: ref, ...restProps, ...rest, children: [jsxRuntime.jsx(ControlLoadingProvider, { value: !!isLoading, children: children }), leftIconValue &&
12839
- (isIconProps(leftIconValue) ? (jsxRuntime.jsx(Icon, { ...leftIconValue, className: prefixedClassNames(classPrefix, 'is-left') })) : (jsxRuntime.jsx(Icon, { className: prefixedClassNames(classPrefix, 'is-left'), children: leftIconValue }))), rightIconValue &&
12840
- (isIconProps(rightIconValue) ? (jsxRuntime.jsx(Icon, { ...rightIconValue, className: prefixedClassNames(classPrefix, 'is-right') })) : (jsxRuntime.jsx(Icon, { className: prefixedClassNames(classPrefix, 'is-right'), children: rightIconValue })))] }) }));
12841
- });
12842
- Control.displayName = 'Control';
12843
-
12844
- /**
12845
- * FieldLabel component for rendering a Bulma field label. It renders the
12846
- * label column of a horizontal `Field` (a `div` with the `field-label` class),
12847
- * not a `<label>`, so text placed straight in it names nothing. Put a `<label>`
12848
- * with the `label` class inside it and point its `htmlFor` at the control's
12849
- * `id`, or give the horizontal `Field` a `label` prop, which renders this
12850
- * column and its `<label>` for you. A `<label>` you put here names the
12851
- * control through its `htmlFor` alone: a range `Slider`'s thumbs and an
12852
- * `Autocomplete`'s, a `Combobox`'s or a `Taginput`'s list point
12853
- * `aria-labelledby` at a `Field`'s label only when its `label` prop renders it
12854
- * and it names the control. Where it would not name the control on its own, as in an inner `Field`,
12855
- * pass `labelProps={{ htmlFor, id }}` with a matching `id` on the control.
12856
- *
12857
- * @function
12858
- * @param {FieldLabelProps} props - Props for the FieldLabel component.
12859
- * @returns {JSX.Element} The rendered field label.
12860
- *
12861
- * @example
12862
- * <FieldLabel size="normal">
12863
- * <label className="label" htmlFor="name">Name</label>
12864
- * </FieldLabel>
12865
- */
12866
- const FieldLabel = ({ size, textColor, bgColor, className, children, ...props }) => {
12867
- const { bulmaHelperClasses, rest } = useBulmaClasses({
12868
- color: textColor,
12869
- backgroundColor: bgColor,
12870
- ...props,
12871
- });
12872
- const mainClass = usePrefixedClassNames('field-label', {
12873
- [`is-${size}`]: !!size,
12874
- });
12875
- const fieldLabelClass = classNames(mainClass, bulmaHelperClasses, className);
12876
- // Spread ...props and ...rest so custom props like data-testid are included
12877
- return (jsxRuntime.jsx("div", { className: fieldLabelClass, ...props, ...rest, children: children }));
12878
- };
12879
- /**
12880
- * FieldBody component for rendering Bulma field body.
12881
- *
12882
- * @function
12883
- * @param {FieldBodyProps} props - Props for the FieldBody component.
12884
- * @returns {JSX.Element} The rendered field body.
12885
- *
12886
- * @example
12887
- * <FieldBody><input className="input" /></FieldBody>
12888
- */
12889
- const FieldBody = ({ textColor, bgColor, className, children, ...props }) => {
12890
- const { bulmaHelperClasses, rest } = useBulmaClasses({
12891
- color: textColor,
12892
- backgroundColor: bgColor,
12893
- ...props,
12894
- });
12895
- const mainClass = usePrefixedClassNames('field-body');
12896
- const fieldBodyClass = classNames(mainClass, bulmaHelperClasses, className);
12897
- // Spread ...props and ...rest so custom props like data-testid are included
12898
- return (jsxRuntime.jsx("div", { className: fieldBodyClass, ...props, ...rest, children: children }));
12899
- };
12900
- /**
12901
- * The `Field` component is a Bulma-styled form field container.
12902
- *
12903
- * @function
12904
- * @param {FieldProps} props - Props for the Field component.
12905
- * @returns {JSX.Element} The rendered field container.
12906
- * @see {@link https://bulma.io/documentation/form/general/#field | Bulma Field documentation}
12907
- *
12908
- * @example
12909
- * // Labelled field — the label auto-associates with the composed base
12910
- * <Field label="Email">
12911
- * <Control>
12912
- * <InputBase type="email" />
12913
- * </Control>
12914
- * </Field>
12915
- *
12916
- * @example
12917
- * // Horizontal field
12918
- * <Field horizontal label="Name">
12919
- * <Control>
12920
- * <InputBase />
12921
- * </Control>
12922
- * </Field>
12923
- */
12924
- const FieldComponent = ({ horizontal, grouped, hasAddons, narrow, label, labelSize, labelProps, textColor, color: _fieldColor, bgColor, className, children, ...props }) => {
12925
- const { bulmaHelperClasses, rest } = useBulmaClasses({
12926
- color: textColor,
12927
- backgroundColor: bgColor,
12928
- ...props,
12929
- });
12930
- const mainClass = usePrefixedClassNames('field', {
12931
- 'is-horizontal': horizontal,
12932
- 'has-addons': !!hasAddons,
12933
- 'has-addons-centered': hasAddons === 'centered',
12934
- 'has-addons-right': hasAddons === 'right',
12935
- 'is-narrow': narrow,
12936
- 'is-grouped': grouped === true ||
12937
- grouped === 'centered' ||
12938
- grouped === 'right' ||
12939
- grouped === 'multiline',
12940
- 'is-grouped-centered': grouped === 'centered',
12941
- 'is-grouped-right': grouped === 'right',
12942
- 'is-grouped-multiline': grouped === 'multiline',
12943
- });
12944
- const fieldClass = classNames(mainClass, bulmaHelperClasses, className);
12945
- // Default labelSize to 'normal' when horizontal for proper baseline alignment
12946
- const effectiveLabelSize = labelSize ?? (horizontal ? 'normal' : undefined);
12947
- const labelClass = usePrefixedClassNames('label');
12948
- // Auto-associate the label with a single composed base control (#495): the
12949
- // label points at a generated id shared via context, which a composed base
12950
- // adopts when the user supplied no id of their own.
12951
- // Presence semantics on htmlFor — even an explicit `htmlFor: undefined`
12952
- // means the caller owns the association. Grouped/addons fields hold several
12953
- // controls, so no single association is generated for them.
12954
- const generatedId = React.useId();
12955
- const userWiredLabel = !!labelProps && 'htmlFor' in labelProps;
12956
- const targetId = label && !userWiredLabel && !grouped && !hasAddons
12957
- ? generatedId
12958
- : undefined;
12959
- // A group control (Radios, Checkboxes, Rate, DateRangeInput and its base)
12960
- // cannot take that `for`, so it points `aria-labelledby` at the label
12961
- // itself (#939).
12962
- // The label gets an id whenever the association is on, unless the caller
12963
- // gave it one.
12964
- const labelId = labelProps?.id ?? (targetId ? `${targetId}-label` : undefined);
12965
- // What the rendered label's `for` points at, generated or the caller's, and
12966
- // the id it renders with, so a control can tell when a label wired by hand
12967
- // names it and point at that label. An unlabeled Field passes on its
12968
- // parent's. Unlike `targetId`, a label wired by hand passes on its own in
12969
- // `grouped`/`hasAddons` Fields too, on purpose: those only stop the Field
12970
- // picking one control out of several, and the caller's `for` has picked
12971
- // one, so a range Slider or Autocomplete it names points at that label in
12972
- // a row as well. A group reads the label element id below instead, which
12973
- // stays off for a label wired by hand, since no group takes its `for`.
12974
- const inheritedLabelFor = useFieldLabelFor();
12975
- const labelForTarget = userWiredLabel ? labelProps.htmlFor : targetId;
12976
- const ownLabelFor = React.useMemo(() => ({ htmlFor: labelForTarget, id: labelId }), [labelForTarget, labelId]);
12977
- const labelFor = label ? ownLabelFor : inheritedLabelFor;
12978
- // Whether the label keeps its generated `for` (#1004). Content that takes
12979
- // the Field's id says so from a layout effect, and this Field's own layout
12980
- // effect runs after its content's in the same commit, so by then every
12981
- // report has landed. Until it runs, on the server and in the first render
12982
- // while hydrating, the label writes the `for` as it always has. From then
12983
- // on it keeps the `for` only while some content holds the id, and content
12984
- // that mounts or changes later reports when it does. Counted in a ref, and
12985
- // state is set only once settled, so content that takes the id renders
12986
- // the Field just once.
12987
- const takersRef = React.useRef(0);
12988
- const settledRef = React.useRef(false);
12989
- const [forTaken, setForTaken] = React.useState(true);
12990
- const settle = React.useCallback(() => {
12991
- if (settledRef.current)
12992
- setForTaken(takersRef.current > 0);
12993
- }, []);
12994
- const reportLabelFor = React.useCallback(() => {
12995
- takersRef.current += 1;
12996
- settle();
12997
- return () => {
12998
- takersRef.current -= 1;
12999
- settle();
13000
- };
13001
- }, [settle]);
14316
+ const bulmaClasses = usePrefixedClassNames();
14317
+ const listClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
14318
+ return (jsxRuntime.jsx("ul", { className: listClasses || undefined, ...rest, children: children }));
14319
+ };
14320
+ const UnorderedList = withSubComponents(UnorderedListComponent, { Item: ListItem }, 'UnorderedList');
14321
+
14322
+ // The colors `Checkbox` takes, as `checkboxColors` lists them. Declared here as
14323
+ // well, so the API page can read them off this module, and a type test holds
14324
+ // the two lists to each other.
14325
+ const checkboxCardColors = [
14326
+ 'primary',
14327
+ 'link',
14328
+ 'info',
14329
+ 'success',
14330
+ 'warning',
14331
+ 'danger',
14332
+ ];
14333
+ /**
14334
+ * The `CheckboxCard` component is a checkbox drawn as a card, with a title and a description, that toggles wherever you click it.
14335
+ *
14336
+ * It's a `<label>` around a real checkbox, so it sends its `value` with a
14337
+ * form and Space toggles it. The title names the checkbox and the
14338
+ * description describes it. Alone it's one choice; inside `Checkboxes` it
14339
+ * takes the group's name and selection, as `Checkbox` does.
14340
+ *
14341
+ * @function
14342
+ * @param {CheckboxCardProps} props - Props for the CheckboxCard component.
14343
+ * @param {React.Ref<HTMLInputElement>} ref - Forwarded ref to the `<input>`.
14344
+ * @returns {JSX.Element} The rendered card.
14345
+ *
14346
+ * @example
14347
+ * <Checkboxes label="Add-ons" name="extras" defaultValue={['backups']}>
14348
+ * <CheckboxCard value="backups" title="Daily backups" description="Kept for 30 days." />
14349
+ * <CheckboxCard value="domain" title="Custom domain" description="Bring your own." />
14350
+ * </Checkboxes>
14351
+ */
14352
+ const CheckboxCard = React.forwardRef(({ title, description, children, hasIndicator = true, color, className, textColor, disabled, name, value, checked, defaultChecked, indeterminate, onChange, ...props }, ref) => {
14353
+ const { bulmaHelperClasses, rest } = useBulmaClasses({
14354
+ color: textColor,
14355
+ ...props,
14356
+ });
14357
+ // The input's own ref, merged with the forwarded one, as Checkbox does,
14358
+ // so the mixed state can be set on it.
14359
+ const inputRef = React.useRef(null);
14360
+ const setRefs = React.useCallback((node) => {
14361
+ inputRef.current = node;
14362
+ if (typeof ref === 'function')
14363
+ return ref(node);
14364
+ if (ref)
14365
+ ref.current = node;
14366
+ }, [ref]);
14367
+ // `indeterminate` is a DOM property with no attribute, and a click clears
14368
+ // it, so it's set after every render while the prop is set, and cleared
14369
+ // once when the prop goes back to unset. Checkbox does the same.
14370
+ const ownsIndeterminateRef = React.useRef(false);
13002
14371
  useClientLayoutEffect(() => {
13003
- settledRef.current = true;
13004
- if (targetId)
13005
- settle();
13006
- }, [targetId, settle]);
13007
- const renderedTargetId = forTaken ? targetId : undefined;
13008
- let renderedLabel = null;
13009
- if (label) {
13010
- if (horizontal) {
13011
- renderedLabel = (jsxRuntime.jsx(FieldLabel, { size: effectiveLabelSize, children: jsxRuntime.jsx("label", { htmlFor: renderedTargetId, ...labelProps, id: labelId, className: classNames(labelClass, labelProps?.className), style: labelProps?.style, children: label }) }));
13012
- }
13013
- else {
13014
- renderedLabel = (jsxRuntime.jsx("label", { htmlFor: renderedTargetId, ...labelProps, id: labelId, className: classNames(labelClass, labelProps?.className), style: { display: 'block', ...(labelProps?.style || {}) }, children: label }));
14372
+ if (indeterminate !== undefined) {
14373
+ ownsIndeterminateRef.current = true;
14374
+ inputRef.current.indeterminate = indeterminate;
13015
14375
  }
13016
- }
13017
- // If horizontal, wrap children in FieldBody (unless the user already provided
13018
- // a FieldBody — either as the single child, or as one element among siblings
13019
- // like <Field.Label/> + <Field.Body/>).
13020
- let content = children;
13021
- if (horizontal) {
13022
- const isFieldBody = (c) => React.isValidElement(c) &&
13023
- // @ts-expect-error displayName isn't on the public type
13024
- (c.type === FieldBody || c.type?.displayName === 'FieldBody');
13025
- const isFieldLabel = (c) => React.isValidElement(c) &&
13026
- // @ts-expect-error displayName isn't on the public type
13027
- (c.type === FieldLabel || c.type?.displayName === 'FieldLabel');
13028
- const childArray = React.Children.toArray(children);
13029
- const userProvidedStructure = childArray.some(c => isFieldBody(c) || isFieldLabel(c));
13030
- if (userProvidedStructure) {
13031
- content = children;
14376
+ else if (ownsIndeterminateRef.current) {
14377
+ ownsIndeterminateRef.current = false;
14378
+ inputRef.current.indeterminate = false;
13032
14379
  }
13033
- else {
13034
- content = jsxRuntime.jsx(FieldBody, { children: children });
14380
+ });
14381
+ // A surrounding <Checkboxes> hands down its name and, when it manages
14382
+ // the selection, whether this card's value is in it. The card's own
14383
+ // props win, as they do on Checkbox.
14384
+ const group = useCheckboxesGroup();
14385
+ const effectiveName = name ?? group?.name;
14386
+ const groupManaged = group?.value !== undefined && value !== undefined;
14387
+ const effectiveChecked = checked !== undefined
14388
+ ? checked
14389
+ : groupManaged
14390
+ ? group.value.includes(String(value))
14391
+ : undefined;
14392
+ const effectiveDefaultChecked = groupManaged ? undefined : defaultChecked;
14393
+ const handleChange = React.useCallback((e) => {
14394
+ if (indeterminate !== undefined)
14395
+ e.target.indeterminate = indeterminate;
14396
+ onChange?.(e);
14397
+ if (group?.onChange && value !== undefined) {
14398
+ const valStr = String(value);
14399
+ const current = group.value ?? [];
14400
+ group.onChange(e.target.checked
14401
+ ? current.includes(valStr)
14402
+ ? current
14403
+ : [...current, valStr]
14404
+ : current.filter(v => v !== valStr));
13035
14405
  }
13036
- }
13037
- return (jsxRuntime.jsx(FieldProvider, { value: true, children: jsxRuntime.jsx(FieldLabelIdProvider, { value: targetId, children: jsxRuntime.jsx(FieldLabelElementIdProvider, { value: targetId ? labelId : undefined, children: jsxRuntime.jsx(FieldLabelForProvider, { value: labelFor, children: jsxRuntime.jsx(FieldLabelForReportProvider, { value: targetId ? reportLabelFor : undefined, children: jsxRuntime.jsxs("div", { className: fieldClass, ...rest, children: [renderedLabel, content] }) }) }) }) }) }));
13038
- };
13039
- FieldLabel.displayName = 'FieldLabel';
13040
- FieldBody.displayName = 'FieldBody';
13041
- const Field = withSubComponents(FieldComponent, {
13042
- Label: FieldLabel,
13043
- Body: FieldBody,
13044
- Control,
13045
- }, 'Field');
14406
+ }, [onChange, group, value, indeterminate]);
14407
+ // The title names the input and the description describes it. A name
14408
+ // passed in wins, and without one of either the label's own text names
14409
+ // the input, description included, so it isn't described again.
14410
+ const titleId = React.useId();
14411
+ const descriptionId = React.useId();
14412
+ const ariaLabel = rest['aria-label'];
14413
+ const ariaLabelledBy = rest['aria-labelledby'] || (!ariaLabel && title ? titleId : undefined);
14414
+ const ariaDescribedBy = rest['aria-describedby'] ||
14415
+ (description && (ariaLabel || ariaLabelledBy)
14416
+ ? descriptionId
14417
+ : undefined);
14418
+ const cardClass = usePrefixedClassNames('checkbox-card', 'card', {
14419
+ [`is-${color}`]: color && checkboxCardColors.includes(color),
14420
+ });
14421
+ const contentClass = usePrefixedClassNames('card-content', 'checkbox-card-content');
14422
+ const checkClass = usePrefixedClassNames('checkbox-card-check');
14423
+ const bodyClass = usePrefixedClassNames('checkbox-card-body');
14424
+ const titleClass = usePrefixedClassNames('checkbox-card-title');
14425
+ const descriptionClass = usePrefixedClassNames('checkbox-card-description');
14426
+ return (jsxRuntime.jsxs("label", { className: classNames(cardClass, bulmaHelperClasses, className), children: [jsxRuntime.jsx("input", { ref: setRefs, type: "checkbox", disabled: disabled, name: effectiveName, value: value, checked: effectiveChecked, defaultChecked: effectiveDefaultChecked, onChange: handleChange, ...rest, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy }), jsxRuntime.jsxs("span", { className: contentClass, children: [hasIndicator && jsxRuntime.jsx("span", { className: checkClass }), jsxRuntime.jsxs("span", { className: bodyClass, children: [title ? (jsxRuntime.jsx("span", { id: titleId, className: titleClass, children: title })) : null, description ? (jsxRuntime.jsx("span", { id: descriptionId, className: descriptionClass, children: description })) : null, children] })] })] }));
14427
+ });
14428
+ CheckboxCard.displayName = 'CheckboxCard';
13046
14429
 
13047
14430
  /**
13048
- * Associates the convenience `label` prop with its control (#368): generates
13049
- * an id for the control and returns labelProps carrying a matching `htmlFor`.
13050
- * A user-supplied `id` is used as the target instead of the generated one, and
13051
- * an explicit `htmlFor` key in labelProps — even set to `undefined` — disables
13052
- * generation entirely: the user has taken over the association (#495 presence
13053
- * semantics). When the control renders no label of its own, it adopts the id
13054
- * a labeled Field around it offers, the way the bases do, so that Field's
13055
- * label names it (#939); a user `id` still wins there. It tells that Field
13056
- * when the control took its id, since the Field's label drops a `for`
13057
- * nothing took (#1004). With `needsLabelId` it also returns the id of
13058
- * whichever label names the control (#981).
13059
- * Internal; not part of the public API.
13060
- */
13061
- function useAutoLabelId({ label, id, labelProps, rendersLabel, hasInput = true, needsLabelId = false, }) {
13062
- // Called unconditionally per the rules of hooks; SSR-safe on React 18 and 19.
13063
- const generatedId = React.useId();
13064
- // Read outside the control's own Field, so it is set only when an outer
13065
- // Field holds the control, which then renders no label of its own.
13066
- const fieldLabelId = useFieldLabelId();
13067
- // What an outer Field's label points at, even when wired by hand, and the
13068
- // id that label renders with, read together so the id is that label's.
13069
- // Set in `grouped`/`hasAddons` Fields too when the label is wired by hand,
13070
- // since its `for` names this one control whatever the layout (see Field).
13071
- const fieldLabelFor = useFieldLabelFor();
13072
- // Truthiness mirrors Field's own `if (label)` render gate.
13073
- const active = !!label && rendersLabel;
13074
- // Presence, not truthiness: `htmlFor: undefined` is an explicit opt-out and
13075
- // must not leave an orphan generated id on the control.
13076
- const userWired = !!labelProps && 'htmlFor' in labelProps;
13077
- const adopted = !active && hasInput ? fieldLabelId : undefined;
13078
- const controlId = id ?? (active && !userWired ? generatedId : adopted);
13079
- // Tells a labeled Field around the control when its label's `for` names
13080
- // the control, since it drops a `for` nothing took (#1004).
13081
- useReportFieldLabelFor(fieldLabelId !== undefined && controlId === fieldLabelId);
13082
- const ownLabelProps = { htmlFor: controlId, ...labelProps };
13083
- const labelTarget = active ? ownLabelProps.htmlFor : fieldLabelFor.htmlFor;
13084
- const labelled = !!controlId && labelTarget === controlId;
13085
- // Only a label that names the control gets an id to point at, with the
13086
- // suffix Field gives its own, from an id that is already unique.
13087
- const ownLabelId = needsLabelId && active && labelled
13088
- ? (labelProps?.id ?? `${generatedId}-label`)
13089
- : undefined;
13090
- // Inactive with a label still means an own Field may render it (pickers'
13091
- // inline mode). The explicit `htmlFor: undefined` tells Field the
13092
- // association is owned here, so it must not generate one that would
13093
- // dangle (#495 presence semantics).
13094
- const fieldLabelProps = active
13095
- ? ownLabelId
13096
- ? { ...ownLabelProps, id: ownLabelId }
13097
- : ownLabelProps
13098
- : label
13099
- ? { htmlFor: undefined, ...labelProps }
13100
- : labelProps;
13101
- const labelId = needsLabelId && labelled
13102
- ? active
13103
- ? ownLabelId
13104
- : fieldLabelFor.id
13105
- : undefined;
13106
- return { controlId, fieldLabelProps, labelled, labelId };
13107
- }
13108
- /**
13109
- * Group-input counterpart of {@link useAutoLabelId} (#494): a group of
13110
- * controls cannot take a single `htmlFor`, so instead the rendered `<label>`
13111
- * gets a generated id and the group container points at it with
13112
- * `aria-labelledby`. A user-supplied `labelProps.id` is used as the target
13113
- * instead of generating one. Any caller `htmlFor` is stripped — a group label
13114
- * names the group, never a single control — so the merged labelProps always
13115
- * carry an explicit `htmlFor: undefined`. When the group renders no label of
13116
- * its own, it points at the label of a labeled Field around it instead
13117
- * (#939). A caller who named the group with `aria-label` or
13118
- * `aria-labelledby` gets no `aria-labelledby` from either label, so their
13119
- * name wins wherever the label sits.
13120
- * Internal; not part of the public API.
14431
+ * The `Field` a `Checkboxes` or `Radios` with `as="fieldset"` renders of its
14432
+ * own, as a native `<fieldset>`: Bulma's field markup, with its label as the
14433
+ * `<legend>` (inside the `field-label` column when horizontal), so the legend
14434
+ * names the group and needs no id. It holds a `Field`'s place for what is
14435
+ * inside, so the choices read as inside a `Field`. Internal; not part of the
14436
+ * public API.
13121
14437
  */
13122
- function useAutoLabelledBy({ label, labelProps, rendersLabel, callerProps, }) {
13123
- // Called unconditionally per the rules of hooks; SSR-safe on React 18 and 19.
13124
- const generatedId = React.useId();
13125
- // Set only when an outer Field holds the group (see useAutoLabelId).
13126
- const fieldLabelElementId = useFieldLabelElementId();
13127
- const active = !!label && rendersLabel;
13128
- const labelId = labelProps?.id ?? (active ? generatedId : undefined);
13129
- const fieldLabelProps = active
13130
- ? { ...labelProps, id: labelId, htmlFor: undefined }
13131
- : labelProps;
13132
- const aria = callerProps;
13133
- // Presence for aria-labelledby: the caller's key replaces the attribute
13134
- // through the spread whatever its value, and pointing it at a label would
13135
- // only take a Rate's fallback name away. Checked before either label, so
13136
- // the caller's name wins over the group's own `label` as it does over a
13137
- // Field's, the way an aria-label beats a `<label for>` on a single input.
13138
- const callerNamed = !!aria['aria-label'] || 'aria-labelledby' in aria;
13139
- const ariaLabelledBy = callerNamed
13140
- ? undefined
13141
- : active
13142
- ? labelId
13143
- : fieldLabelElementId;
13144
- return { ariaLabelledBy, fieldLabelProps };
13145
- }
14438
+ const GroupFieldset = ({ label, labelSize, labelProps, horizontal, fieldClassName, role, attributes, message, children, }) => {
14439
+ const fieldClass = usePrefixedClassNames('field', {
14440
+ 'is-horizontal': !!horizontal,
14441
+ });
14442
+ const labelClass = usePrefixedClassNames('label');
14443
+ const fieldLabelClass = usePrefixedClassNames('field-label', {
14444
+ [`is-${labelSize || 'normal'}`]: true,
14445
+ });
14446
+ const fieldBodyClass = usePrefixedClassNames('field-body');
14447
+ // A legend names its fieldset with no `for`, so a `htmlFor` has nothing
14448
+ // to point at.
14449
+ const { htmlFor: _htmlFor, className: labelClassName, ...ownLabelProps } = labelProps ?? {};
14450
+ const shownLabel = {
14451
+ ...ownLabelProps,
14452
+ className: classNames(labelClass, labelClassName),
14453
+ };
14454
+ let legend = null;
14455
+ if (label) {
14456
+ legend = horizontal ? (jsxRuntime.jsx("legend", { className: fieldLabelClass, children: jsxRuntime.jsx("span", { ...shownLabel, children: label }) })) : (jsxRuntime.jsx("legend", { ...shownLabel, children: label }));
14457
+ }
14458
+ return (jsxRuntime.jsx(FieldProvider, { value: true, children: jsxRuntime.jsxs("fieldset", { className: classNames(fieldClass, fieldClassName), role: role, ...attributes, children: [legend, horizontal ? (jsxRuntime.jsxs("div", { className: fieldBodyClass, children: [children, message] })) : (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [children, message] }))] }) }));
14459
+ };
13146
14460
 
13147
14461
  /**
13148
14462
  * The `Checkboxes` component wraps multiple `Checkbox` components in a Bulma-styled group.
@@ -13169,7 +14483,7 @@ function useAutoLabelledBy({ label, labelProps, rendersLabel, callerProps, }) {
13169
14483
  * <Checkbox value="angular">Angular</Checkbox>
13170
14484
  * </Checkboxes>
13171
14485
  */
13172
- const CheckboxesComponent = ({ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, name, value, defaultValue, onChange, children, className, ...props }) => {
14486
+ const CheckboxesComponent = ({ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, name, value, defaultValue, onChange, children, className, as, ...props }) => {
13173
14487
  const insideField = useInsideField();
13174
14488
  const insideControl = useInsideControl();
13175
14489
  const ownField = rendersOwnField('Checkboxes', {
@@ -13180,10 +14494,14 @@ const CheckboxesComponent = ({ label, labelSize, labelProps, horizontal, message
13180
14494
  horizontal,
13181
14495
  fieldClassName,
13182
14496
  });
14497
+ // As a fieldset with a Field of its own, the group's label is its legend,
14498
+ // which names it with no id, so nothing points `aria-labelledby` at it.
14499
+ const asFieldset = as === 'fieldset';
14500
+ const fieldsetField = asFieldset && ownField;
13183
14501
  const { ariaLabelledBy, fieldLabelProps } = useAutoLabelledBy({
13184
14502
  label,
13185
14503
  labelProps,
13186
- rendersLabel: ownField,
14504
+ rendersLabel: ownField && !fieldsetField,
13187
14505
  callerProps: props,
13188
14506
  });
13189
14507
  const { bulmaHelperClasses, rest } = useBulmaClasses({
@@ -13208,11 +14526,17 @@ const CheckboxesComponent = ({ label, labelSize, labelProps, horizontal, message
13208
14526
  name,
13209
14527
  ...(groupActive ? { value: currentValue, onChange: handleChange } : {}),
13210
14528
  }), [name, groupActive, currentValue, handleChange]);
13211
- const checkboxesElement = (jsxRuntime.jsx("div", { className: wrapperClass, role: "group", "aria-labelledby": ariaLabelledBy, ...rest, children: jsxRuntime.jsx(CheckboxesProvider, { value: ctx, children: children }) }));
14529
+ const choices = (jsxRuntime.jsx(CheckboxesProvider, { value: ctx, children: children }));
14530
+ // A fieldset is a group already, so it takes no role of its own.
14531
+ const Group = asFieldset ? 'fieldset' : 'div';
14532
+ const checkboxesElement = fieldsetField ? (jsxRuntime.jsx("div", { className: wrapperClass, children: choices })) : (jsxRuntime.jsx(Group, { className: wrapperClass, role: asFieldset ? undefined : 'group', "aria-labelledby": ariaLabelledBy, ...rest, children: choices }));
13212
14533
  let content = checkboxesElement;
13213
14534
  if (rendersOwnControl('Checkboxes', { insideControl })) {
13214
14535
  content = jsxRuntime.jsx(Control, { children: content });
13215
14536
  }
14537
+ if (fieldsetField) {
14538
+ return (jsxRuntime.jsx(GroupFieldset, { label: label, labelSize: labelSize, labelProps: labelProps, horizontal: horizontal, fieldClassName: fieldClassName, attributes: rest, message: messageEl, children: content }));
14539
+ }
13216
14540
  if (ownField) {
13217
14541
  return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
13218
14542
  }
@@ -13389,36 +14713,112 @@ const Radio = React.forwardRef(({ color, size, className, children, textColor, d
13389
14713
  // Local props always win over the group (explicit > implicit).
13390
14714
  const group = useRadiosGroup();
13391
14715
  const effectiveName = name ?? group?.name;
13392
- // Group-managed checked state — only when the group is in
13393
- // controlled/uncontrolled mode (group.value is defined) AND this Radio
13394
- // has a value to compare against. Local `checked` always wins.
14716
+ // Group-managed checked state — only when the group is in
14717
+ // controlled/uncontrolled mode (group.value is defined) AND this Radio
14718
+ // has a value to compare against. Local `checked` always wins.
14719
+ const groupManaged = group?.value !== undefined && value !== undefined;
14720
+ const effectiveChecked = checked !== undefined
14721
+ ? checked
14722
+ : groupManaged
14723
+ ? group.value === value
14724
+ : undefined;
14725
+ // When the group manages checked, suppress local defaultChecked to avoid
14726
+ // the controlled/uncontrolled React warning.
14727
+ const effectiveDefaultChecked = groupManaged ? undefined : defaultChecked;
14728
+ const handleChange = React.useCallback((e) => {
14729
+ // Local onChange always fires.
14730
+ onChange?.(e);
14731
+ // Dispatch to the group additionally — keeps group state in sync.
14732
+ if (group?.onChange && value !== undefined) {
14733
+ group.onChange(String(value));
14734
+ }
14735
+ }, [onChange, group, value]);
14736
+ const mainClass = usePrefixedClassNames('styled-radio', 'radio', {
14737
+ [`is-${color}`]: color && radioColors.includes(color),
14738
+ [`is-${size}`]: size && radioSizes.includes(size),
14739
+ });
14740
+ const radioClass = classNames(mainClass, bulmaHelperClasses, className);
14741
+ const checkClass = usePrefixedClassNames('check');
14742
+ const controlLabelClass = usePrefixedClassNames('control-label');
14743
+ return (jsxRuntime.jsxs("label", { className: radioClass, children: [jsxRuntime.jsx("input", { ref: ref, type: "radio", disabled: disabled, name: effectiveName, value: value, checked: effectiveChecked, defaultChecked: effectiveDefaultChecked, onChange: handleChange, ...rest }), jsxRuntime.jsx("span", { className: checkClass }), children && jsxRuntime.jsx("span", { className: controlLabelClass, children: children })] }));
14744
+ });
14745
+ Radio.displayName = 'Radio';
14746
+
14747
+ // The colors `Radio` takes, as `radioColors` lists them. Declared here as
14748
+ // well, so the API page can read them off this module, and a type test holds
14749
+ // the two lists to each other.
14750
+ const radioCardColors = [
14751
+ 'primary',
14752
+ 'link',
14753
+ 'info',
14754
+ 'success',
14755
+ 'warning',
14756
+ 'danger',
14757
+ ];
14758
+ /**
14759
+ * The `RadioCard` component is a radio button drawn as a card, with a title and a description, picked wherever you click it.
14760
+ *
14761
+ * It's a `<label>` around a real radio, so it sends its `value` with a form,
14762
+ * and the cards that share a `name` are one choice the arrow keys move
14763
+ * through. The title names the radio and the description describes it.
14764
+ * Inside `Radios` it takes the group's name and selection, as `Radio` does.
14765
+ *
14766
+ * @function
14767
+ * @param {RadioCardProps} props - Props for the RadioCard component.
14768
+ * @param {React.Ref<HTMLInputElement>} ref - Forwarded ref to the `<input>`.
14769
+ * @returns {JSX.Element} The rendered card.
14770
+ *
14771
+ * @example
14772
+ * <Radios label="Shipping" name="shipping" defaultValue="standard">
14773
+ * <RadioCard value="standard" title="Standard" description="Arrives in 4 to 6 days." />
14774
+ * <RadioCard value="express" title="Express" description="Arrives tomorrow." />
14775
+ * </Radios>
14776
+ */
14777
+ const RadioCard = React.forwardRef(({ title, description, children, hasIndicator = true, color, className, textColor, disabled, name, value, checked, defaultChecked, onChange, ...props }, ref) => {
14778
+ const { bulmaHelperClasses, rest } = useBulmaClasses({
14779
+ color: textColor,
14780
+ ...props,
14781
+ });
14782
+ // A surrounding <Radios> hands down its name and, when it manages the
14783
+ // selection, whether this card is the one picked. The card's own props
14784
+ // win, as they do on Radio.
14785
+ const group = useRadiosGroup();
14786
+ const effectiveName = name ?? group?.name;
13395
14787
  const groupManaged = group?.value !== undefined && value !== undefined;
13396
14788
  const effectiveChecked = checked !== undefined
13397
14789
  ? checked
13398
14790
  : groupManaged
13399
14791
  ? group.value === value
13400
14792
  : undefined;
13401
- // When the group manages checked, suppress local defaultChecked to avoid
13402
- // the controlled/uncontrolled React warning.
13403
14793
  const effectiveDefaultChecked = groupManaged ? undefined : defaultChecked;
13404
14794
  const handleChange = React.useCallback((e) => {
13405
- // Local onChange always fires.
13406
14795
  onChange?.(e);
13407
- // Dispatch to the group additionally — keeps group state in sync.
13408
14796
  if (group?.onChange && value !== undefined) {
13409
14797
  group.onChange(String(value));
13410
14798
  }
13411
14799
  }, [onChange, group, value]);
13412
- const mainClass = usePrefixedClassNames('styled-radio', 'radio', {
13413
- [`is-${color}`]: color && radioColors.includes(color),
13414
- [`is-${size}`]: size && radioSizes.includes(size),
13415
- });
13416
- const radioClass = classNames(mainClass, bulmaHelperClasses, className);
13417
- const checkClass = usePrefixedClassNames('check');
13418
- const controlLabelClass = usePrefixedClassNames('control-label');
13419
- return (jsxRuntime.jsxs("label", { className: radioClass, children: [jsxRuntime.jsx("input", { ref: ref, type: "radio", disabled: disabled, name: effectiveName, value: value, checked: effectiveChecked, defaultChecked: effectiveDefaultChecked, onChange: handleChange, ...rest }), jsxRuntime.jsx("span", { className: checkClass }), children && jsxRuntime.jsx("span", { className: controlLabelClass, children: children })] }));
14800
+ // The title names the input and the description describes it. A name
14801
+ // passed in wins, and without one of either the label's own text names
14802
+ // the input, description included, so it isn't described again.
14803
+ const titleId = React.useId();
14804
+ const descriptionId = React.useId();
14805
+ const ariaLabel = rest['aria-label'];
14806
+ const ariaLabelledBy = rest['aria-labelledby'] || (!ariaLabel && title ? titleId : undefined);
14807
+ const ariaDescribedBy = rest['aria-describedby'] ||
14808
+ (description && (ariaLabel || ariaLabelledBy)
14809
+ ? descriptionId
14810
+ : undefined);
14811
+ const cardClass = usePrefixedClassNames('radio-card', 'card', {
14812
+ [`is-${color}`]: color && radioCardColors.includes(color),
14813
+ });
14814
+ const contentClass = usePrefixedClassNames('card-content', 'radio-card-content');
14815
+ const checkClass = usePrefixedClassNames('radio-card-check');
14816
+ const bodyClass = usePrefixedClassNames('radio-card-body');
14817
+ const titleClass = usePrefixedClassNames('radio-card-title');
14818
+ const descriptionClass = usePrefixedClassNames('radio-card-description');
14819
+ return (jsxRuntime.jsxs("label", { className: classNames(cardClass, bulmaHelperClasses, className), children: [jsxRuntime.jsx("input", { ref: ref, type: "radio", disabled: disabled, name: effectiveName, value: value, checked: effectiveChecked, defaultChecked: effectiveDefaultChecked, onChange: handleChange, ...rest, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy }), jsxRuntime.jsxs("span", { className: contentClass, children: [hasIndicator && jsxRuntime.jsx("span", { className: checkClass }), jsxRuntime.jsxs("span", { className: bodyClass, children: [title ? (jsxRuntime.jsx("span", { id: titleId, className: titleClass, children: title })) : null, description ? (jsxRuntime.jsx("span", { id: descriptionId, className: descriptionClass, children: description })) : null, children] })] })] }));
13420
14820
  });
13421
- Radio.displayName = 'Radio';
14821
+ RadioCard.displayName = 'RadioCard';
13422
14822
 
13423
14823
  /**
13424
14824
  * The `Radios` component wraps multiple `Radio` components in a Bulma-styled group.
@@ -13444,7 +14844,7 @@ Radio.displayName = 'Radio';
13444
14844
  * <Radio value="green">Green</Radio>
13445
14845
  * </Radios>
13446
14846
  */
13447
- const RadiosComponent = ({ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, name, value, defaultValue, onChange, children, className, ...props }) => {
14847
+ const RadiosComponent = ({ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, name, value, defaultValue, onChange, children, className, as, ...props }) => {
13448
14848
  const insideField = useInsideField();
13449
14849
  const insideControl = useInsideControl();
13450
14850
  const ownField = rendersOwnField('Radios', {
@@ -13455,10 +14855,14 @@ const RadiosComponent = ({ label, labelSize, labelProps, horizontal, message, me
13455
14855
  horizontal,
13456
14856
  fieldClassName,
13457
14857
  });
14858
+ // As a fieldset with a Field of its own, the group's label is its legend,
14859
+ // which names it with no id, so nothing points `aria-labelledby` at it.
14860
+ const asFieldset = as === 'fieldset';
14861
+ const fieldsetField = asFieldset && ownField;
13458
14862
  const { ariaLabelledBy, fieldLabelProps } = useAutoLabelledBy({
13459
14863
  label,
13460
14864
  labelProps,
13461
- rendersLabel: ownField,
14865
+ rendersLabel: ownField && !fieldsetField,
13462
14866
  callerProps: props,
13463
14867
  });
13464
14868
  const { bulmaHelperClasses, rest } = useBulmaClasses({
@@ -13486,11 +14890,17 @@ const RadiosComponent = ({ label, labelSize, labelProps, horizontal, message, me
13486
14890
  name,
13487
14891
  ...(groupActive ? { value: currentValue, onChange: handleChange } : {}),
13488
14892
  }), [name, groupActive, currentValue, handleChange]);
13489
- const radiosElement = (jsxRuntime.jsx("div", { className: wrapperClass, role: "radiogroup", "aria-labelledby": ariaLabelledBy, ...rest, children: jsxRuntime.jsx(RadiosProvider, { value: ctx, children: children }) }));
14893
+ const choices = jsxRuntime.jsx(RadiosProvider, { value: ctx, children: children });
14894
+ // ARIA in HTML allows `radiogroup` on a fieldset, so it keeps the role.
14895
+ const Group = asFieldset ? 'fieldset' : 'div';
14896
+ const radiosElement = fieldsetField ? (jsxRuntime.jsx("div", { className: wrapperClass, children: choices })) : (jsxRuntime.jsx(Group, { className: wrapperClass, role: "radiogroup", "aria-labelledby": ariaLabelledBy, ...rest, children: choices }));
13490
14897
  let content = radiosElement;
13491
14898
  if (rendersOwnControl('Radios', { insideControl })) {
13492
14899
  content = jsxRuntime.jsx(Control, { children: content });
13493
14900
  }
14901
+ if (fieldsetField) {
14902
+ return (jsxRuntime.jsx(GroupFieldset, { label: label, labelSize: labelSize, labelProps: labelProps, horizontal: horizontal, fieldClassName: fieldClassName, role: "radiogroup", attributes: rest, message: messageEl, children: content }));
14903
+ }
13494
14904
  if (ownField) {
13495
14905
  return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
13496
14906
  }
@@ -14327,1190 +15737,758 @@ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName,
14327
15737
  const keep = (text) => (toAsciiDigits(text).match(new RegExp(allowed, 'g')) ?? [])
14328
15738
  .join('')
14329
15739
  .slice(0, length);
14330
- const commit = (next) => {
14331
- if (next === code)
14332
- return;
14333
- if (controlledValue === undefined)
14334
- setInternalValue(next);
14335
- onChange?.(next);
14336
- if (next.length === length)
14337
- onComplete?.(next);
14338
- };
14339
- const inputRef = React.useRef(null);
14340
- const setRefs = React.useCallback((node) => {
14341
- inputRef.current = node;
14342
- if (typeof ref === 'function')
14343
- return ref(node);
14344
- if (ref)
14345
- ref.current = node;
14346
- }, [ref]);
14347
- // The input's selection while it has focus, which the boxes draw.
14348
- const [selection, setSelection] = React.useState(null);
14349
- const follow = React.useCallback(() => {
14350
- const input = inputRef.current;
14351
- const next = getActiveElementInTree(input) === input
14352
- ? [input.selectionStart, input.selectionEnd]
14353
- : null;
14354
- setSelection(prev => prev?.[0] === next?.[0] && prev?.[1] === next?.[1] ? prev : next);
14355
- }, []);
14356
- // A new code moves the caret, and so can a value set from outside.
14357
- useClientLayoutEffect(follow, [code, follow]);
14358
- // React's `onSelect` never fires inside a shadow root, so the boxes
14359
- // follow the browser's own `selectionchange`. A caret that moves fires it
14360
- // at the input, and it bubbles to the document, but in a shadow root some
14361
- // browsers fire it at the document alone and others at the input alone,
14362
- // so both are followed.
14363
- React.useEffect(() => {
14364
- const input = inputRef.current;
14365
- const doc = input.ownerDocument;
14366
- input.addEventListener('selectionchange', follow);
14367
- doc.addEventListener('selectionchange', follow);
14368
- return () => {
14369
- input.removeEventListener('selectionchange', follow);
14370
- doc.removeEventListener('selectionchange', follow);
14371
- };
14372
- }, [follow]);
14373
- const handleChange = (e) => {
14374
- const input = e.target;
14375
- const typed = input.value;
14376
- const next = keep(typed);
14377
- if (next !== typed) {
14378
- // Drop what the code doesn't take before React sees the value, so
14379
- // the caret stays where it was rather than jumping to the end.
14380
- const caret = keep(typed.slice(0, input.selectionStart)).length;
14381
- input.value = next;
14382
- input.setSelectionRange(caret, caret);
14383
- }
14384
- commit(next);
14385
- };
14386
- // The browser cuts a paste to `maxLength` before anything else sees it,
14387
- // so a code pasted as "123 456" would lose its last digit. What the code
14388
- // takes is kept first, and it replaces the whole code, since what you
14389
- // paste into a code field is the code.
14390
- const handlePaste = (e) => {
14391
- onPaste?.(e);
14392
- if (e.defaultPrevented)
14393
- return;
14394
- e.preventDefault();
14395
- const pasted = keep(e.clipboardData.getData('text'));
14396
- if (pasted && !e.currentTarget.readOnly)
14397
- commit(pasted);
14398
- };
14399
- const rootClass = classNames(usePrefixedClassNames('pin-input'), bulmaHelperClasses, className);
14400
- const fieldClass = usePrefixedClassNames('pin-input-field');
14401
- const helpClass = usePrefixedClassNames('help', {
14402
- [`is-${messageColor}`]: !!messageColor,
14403
- });
14404
- // The boxes the selection covers: the one at a caret, or every selected
14405
- // one. A caret after the last character marks the last box.
14406
- const [start, end] = selection ?? [-1, -1];
14407
- const first = Math.min(start, length - 1);
14408
- const last = Math.max(first + 1, Math.min(end, length));
14409
- const boxes = Array.from({ length }, (_, i) => {
14410
- const char = code[i];
14411
- const active = i >= first && i < last;
14412
- return (jsxRuntime.jsx("div", { "aria-hidden": "true", className: prefixedClassNames(classPrefix, 'input', 'pin-input-box', {
14413
- [`is-${color}`]: !!color,
14414
- [`is-${size}`]: !!size,
14415
- 'is-rounded': isRounded,
14416
- 'is-focused': active,
14417
- 'has-caret': active && start === end && !char,
14418
- 'is-disabled': disabled,
14419
- }), children: char && (isMasked ? MASK : char) }, i));
14420
- });
14421
- // Left to right on a right-to-left page too: a code is written that way,
14422
- // so its first character stays in the first box on the left, and the
14423
- // arrow keys move the caret the way the boxes run.
14424
- let content = (jsxRuntime.jsxs("div", { className: rootClass, dir: "ltr", children: [jsxRuntime.jsx("input", { ref: setRefs, className: fieldClass, type: isMasked ? 'password' : 'text', inputMode: type === 'alphanumeric' ? 'text' : 'numeric', autoComplete: "one-time-code", autoCorrect: "off", spellCheck: false,
14425
- // A partial code fails constraint validation. `minLength` would
14426
- // say so only after a keystroke, never after a paste or a filtered
14427
- // character, which set the value from script.
14428
- pattern: `${allowed}{${length}}`, ...rest, value: code, maxLength: length, disabled: disabled, onChange: handleChange, onPaste: handlePaste, onFocus: e => {
14429
- follow();
14430
- onFocus?.(e);
14431
- }, onBlur: e => {
14432
- setSelection(null);
14433
- onBlur?.(e);
14434
- },
14435
- // After the spread: an undefined `id` key in rest would wipe it.
14436
- id: controlId }), boxes] }));
14437
- if (ownControl) {
14438
- content = jsxRuntime.jsx(Control, { children: content });
14439
- }
14440
- const messageEl = message ? jsxRuntime.jsx("p", { className: helpClass, children: message }) : null;
14441
- if (ownField) {
14442
- return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
14443
- }
14444
- return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [content, messageEl] }));
14445
- });
14446
- PinInput.displayName = 'PinInput';
14447
-
14448
- /**
14449
- * Default star icon component.
14450
- */
14451
- const StarIcon = ({ filled }) => (jsxRuntime.jsx("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: filled ? 'currentColor' : 'none', stroke: "currentColor", strokeWidth: filled ? 0 : 2, "aria-hidden": "true", children: jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z" }) }));
14452
- /** Snap a value to the nearest precision step. */
14453
- function snapToPrecision(val, precision) {
14454
- return Math.round(val / precision) * precision;
14455
- }
14456
- /** Get fill percentage for a given icon index based on current value. */
14457
- function getFillPercent(iconIndex, value) {
14458
- if (value >= iconIndex + 1)
14459
- return 100;
14460
- if (value <= iconIndex)
14461
- return 0;
14462
- return Math.round((value - iconIndex) * 100);
14463
- }
14464
- /**
14465
- * The `Rate` component provides a star/icon-based rating system.
14466
- *
14467
- * @function
14468
- * @param {RateProps} props - Props for the Rate component.
14469
- * @param {React.Ref<HTMLDivElement>} ref - Forwarded ref to the container element.
14470
- * @returns {JSX.Element} The rendered rating component.
14471
- *
14472
- * @example
14473
- * // Basic 5-star rating
14474
- * <Rate defaultValue={3} />
14475
- *
14476
- * @example
14477
- * // With color and precision
14478
- * <Rate defaultValue={3.5} color="warning" precision={0.5} showScore />
14479
- *
14480
- * @example
14481
- * // With Font Awesome icons
14482
- * <Rate iconName="star" iconLibrary="fa" iconVariant="solid" />
14483
- */
14484
- const Rate = React.forwardRef(({ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, value: controlledValue, defaultValue = 0, max = 5, size, disabled = false, showScore = false, showText = false, texts, onChange, customIcon, spaced = false, rtl = false, iconName, iconLibrary: iconLibraryProp, iconVariant, iconFeatures, color, precision = 1, customText, name, form, className, ...props }, ref) => {
14485
- const insideField = useInsideField();
14486
- const insideControl = useInsideControl();
14487
- const ownField = rendersOwnField('Rate', {
14488
- insideField,
14489
- insideControl,
14490
- label,
14491
- message,
14492
- horizontal,
14493
- fieldClassName,
14494
- });
14495
- const { ariaLabelledBy, fieldLabelProps } = useAutoLabelledBy({
14496
- label,
14497
- labelProps,
14498
- rendersLabel: ownField,
14499
- callerProps: props,
14500
- });
14501
- const { bulmaHelperClasses, rest } = useBulmaClasses(props);
14502
- const starBaseId = React.useId();
14503
- const [internalValue, setInternalValue] = React.useState(defaultValue);
14504
- const [hoverValue, setHoverValue] = React.useState(null);
14505
- const defaultIconLibrary = useIconLibrary();
14506
- const resolvedLibrary = iconLibraryProp || defaultIconLibrary || 'fa';
14507
- // Determine if controlled
14508
- const isControlled = controlledValue !== undefined;
14509
- const currentValue = isControlled ? controlledValue : internalValue;
14510
- const displayValue = hoverValue !== null ? hoverValue : currentValue;
14511
- // Update value
14512
- const updateValue = React.useCallback((newValue) => {
14513
- if (disabled)
14514
- return;
14515
- // Allow clicking same value to deselect (set to 0)
14516
- const finalValue = newValue === currentValue ? 0 : newValue;
14517
- if (!isControlled) {
14518
- setInternalValue(finalValue);
14519
- }
14520
- onChange?.(finalValue);
14521
- }, [isControlled, currentValue, disabled, onChange]);
14522
- // Handle click with precision support
14523
- const handleClick = React.useCallback((iconIndex, e) => {
14524
- if (disabled)
14525
- return;
14526
- if (precision < 1) {
14527
- const rect = e.currentTarget.getBoundingClientRect();
14528
- let relativeX = (e.clientX - rect.left) / rect.width;
14529
- if (rtl)
14530
- relativeX = 1 - relativeX;
14531
- const rawValue = iconIndex + relativeX;
14532
- const snapped = snapToPrecision(rawValue, precision);
14533
- const clamped = Math.max(precision, Math.min(snapped, max));
14534
- updateValue(clamped);
14535
- }
14536
- else {
14537
- updateValue(iconIndex + 1);
14538
- }
14539
- }, [disabled, precision, rtl, max, updateValue]);
14540
- // Handle mouse enter on icon
14541
- const handleMouseEnter = React.useCallback((iconIndex) => {
14542
- if (disabled)
14543
- return;
14544
- setHoverValue(iconIndex + 1);
14545
- }, [disabled]);
14546
- // Handle mouse move with precision support
14547
- const handleMouseMove = React.useCallback((iconIndex, e) => {
14548
- if (disabled || precision >= 1)
14549
- return;
14550
- const rect = e.currentTarget.getBoundingClientRect();
14551
- let relativeX = (e.clientX - rect.left) / rect.width;
14552
- if (rtl)
14553
- relativeX = 1 - relativeX;
14554
- const rawValue = iconIndex + relativeX;
14555
- const snapped = snapToPrecision(rawValue, precision);
14556
- const clamped = Math.max(precision, Math.min(snapped, max));
14557
- setHoverValue(clamped);
14558
- }, [disabled, precision, rtl, max]);
14559
- // Handle mouse leave
14560
- const handleMouseLeave = React.useCallback(() => {
14561
- setHoverValue(null);
14562
- }, []);
14563
- // Handle keyboard
14564
- const handleKeyDown = React.useCallback((e) => {
14565
- if (disabled)
15740
+ const commit = (next) => {
15741
+ if (next === code)
14566
15742
  return;
14567
- let newValue = currentValue;
14568
- const step = precision < 1 ? precision : 1;
14569
- if (e.key === 'ArrowRight' || e.key === 'ArrowUp') {
14570
- e.preventDefault();
14571
- newValue = Math.min(currentValue + step, max);
14572
- }
14573
- else if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') {
14574
- e.preventDefault();
14575
- newValue = Math.max(currentValue - step, 0);
14576
- }
14577
- else if (e.key === 'Home') {
14578
- e.preventDefault();
14579
- newValue = 0;
14580
- }
14581
- else if (e.key === 'End') {
14582
- e.preventDefault();
14583
- newValue = max;
14584
- }
14585
- // Round to avoid floating point issues
14586
- newValue = Math.round(newValue * 1000) / 1000;
14587
- if (newValue !== currentValue) {
14588
- if (!isControlled) {
14589
- setInternalValue(newValue);
14590
- }
14591
- onChange?.(newValue);
14592
- }
14593
- }, [currentValue, max, precision, disabled, isControlled, onChange]);
14594
- // Get text for current value
14595
- const getText = () => {
14596
- if (!showText || !texts)
14597
- return null;
14598
- const wholeIndex = Math.ceil(displayValue) - 1;
14599
- const baseText = texts[wholeIndex] || null;
14600
- if (!baseText)
14601
- return null;
14602
- // For fractional hover values with precision < 1, append "+"
14603
- if (precision < 1 && hoverValue !== null && hoverValue % 1 !== 0) {
14604
- return `${baseText}+`;
14605
- }
14606
- return baseText;
14607
- };
14608
- // Format score display
14609
- const getScoreDisplay = () => {
14610
- if (precision < 1 && currentValue % 1 !== 0) {
14611
- return currentValue.toFixed(1);
14612
- }
14613
- return currentValue;
15743
+ if (controlledValue === undefined)
15744
+ setInternalValue(next);
15745
+ onChange?.(next);
15746
+ if (next.length === length)
15747
+ onComplete?.(next);
14614
15748
  };
14615
- // Generate classes
14616
- const classPrefix = useClassPrefix();
14617
- const rateClasses = usePrefixedClassNames('rate', {
14618
- [`is-${size}`]: size,
14619
- 'is-disabled': disabled,
14620
- 'is-spaced': spaced,
14621
- 'is-rtl': rtl,
14622
- [`is-${color}`]: color,
14623
- });
14624
- const combinedClasses = classNames(rateClasses, bulmaHelperClasses, className);
14625
- // Render a single icon based on priority: customIcon > iconName > default SVG
14626
- const renderIcon = (iconIndex, isActive, fillPercent) => {
14627
- const iconProps = {
14628
- index: iconIndex,
14629
- isActive,
14630
- isHovered: hoverValue !== null && iconIndex < hoverValue,
14631
- value: displayValue,
14632
- fillPercent,
15749
+ const inputRef = React.useRef(null);
15750
+ const setRefs = React.useCallback((node) => {
15751
+ inputRef.current = node;
15752
+ if (typeof ref === 'function')
15753
+ return ref(node);
15754
+ if (ref)
15755
+ ref.current = node;
15756
+ }, [ref]);
15757
+ // The input's selection while it has focus, which the boxes draw.
15758
+ const [selection, setSelection] = React.useState(null);
15759
+ const follow = React.useCallback(() => {
15760
+ const input = inputRef.current;
15761
+ const next = getActiveElementInTree(input) === input
15762
+ ? [input.selectionStart, input.selectionEnd]
15763
+ : null;
15764
+ setSelection(prev => prev?.[0] === next?.[0] && prev?.[1] === next?.[1] ? prev : next);
15765
+ }, []);
15766
+ // A new code moves the caret, and so can a value set from outside.
15767
+ useClientLayoutEffect(follow, [code, follow]);
15768
+ // React's `onSelect` never fires inside a shadow root, so the boxes
15769
+ // follow the browser's own `selectionchange`. A caret that moves fires it
15770
+ // at the input, and it bubbles to the document, but in a shadow root some
15771
+ // browsers fire it at the document alone and others at the input alone,
15772
+ // so both are followed.
15773
+ React.useEffect(() => {
15774
+ const input = inputRef.current;
15775
+ const doc = input.ownerDocument;
15776
+ input.addEventListener('selectionchange', follow);
15777
+ doc.addEventListener('selectionchange', follow);
15778
+ return () => {
15779
+ input.removeEventListener('selectionchange', follow);
15780
+ doc.removeEventListener('selectionchange', follow);
14633
15781
  };
14634
- if (customIcon) {
14635
- return customIcon(iconProps);
14636
- }
14637
- if (iconName) {
14638
- return (jsxRuntime.jsx(Icon, { name: iconName, library: resolvedLibrary, variant: iconVariant, features: iconFeatures }));
14639
- }
14640
- return jsxRuntime.jsx(StarIcon, { filled: isActive });
14641
- };
14642
- // Render partial icon with layered approach
14643
- const renderPartialIcon = (iconIndex, fillPercent) => {
14644
- if (customIcon) {
14645
- // For custom icons, pass fillPercent and let consumer handle it
14646
- const iconProps = {
14647
- index: iconIndex,
14648
- isActive: fillPercent > 0,
14649
- isHovered: hoverValue !== null && iconIndex < (hoverValue ?? 0),
14650
- value: displayValue,
14651
- fillPercent,
14652
- };
14653
- return customIcon(iconProps);
14654
- }
14655
- if (iconName) {
14656
- return (jsxRuntime.jsxs("span", { className: prefixedClassNames(classPrefix, 'rate-icon-partial'), style: {
14657
- '--rate-fill-percent': `${fillPercent}%`,
14658
- }, children: [jsxRuntime.jsx("span", { className: prefixedClassNames(classPrefix, 'rate-icon-bg'), children: jsxRuntime.jsx(Icon, { name: iconName, library: resolvedLibrary, variant: iconVariant, features: iconFeatures }) }), jsxRuntime.jsx("span", { className: prefixedClassNames(classPrefix, 'rate-icon-fg'), children: jsxRuntime.jsx(Icon, { name: iconName, library: resolvedLibrary, variant: iconVariant, features: iconFeatures }) })] }));
15782
+ }, [follow]);
15783
+ const handleChange = (e) => {
15784
+ const input = e.target;
15785
+ const typed = input.value;
15786
+ const next = keep(typed);
15787
+ if (next !== typed) {
15788
+ // Drop what the code doesn't take before React sees the value, so
15789
+ // the caret stays where it was rather than jumping to the end.
15790
+ const caret = keep(typed.slice(0, input.selectionStart)).length;
15791
+ input.value = next;
15792
+ input.setSelectionRange(caret, caret);
14659
15793
  }
14660
- // Default SVG with clipPath for partial fill
14661
- const clipId = `rate-clip-${iconIndex}`;
14662
- return (jsxRuntime.jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", "aria-hidden": "true", children: [jsxRuntime.jsx("defs", { children: jsxRuntime.jsx("clipPath", { id: clipId, children: jsxRuntime.jsx("rect", { x: "0", y: "0", width: `${(fillPercent / 100) * 24}`, height: "24" }) }) }), jsxRuntime.jsx("path", { fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", d: "M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z" }), jsxRuntime.jsx("path", { clipPath: `url(#${clipId})`, fill: "currentColor", d: "M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z" })] }));
15794
+ commit(next);
14663
15795
  };
14664
- // The star the value falls in is the checked radio, and it carries the
14665
- // value: a radiogroup takes no aria-value* of its own. A fractional value
14666
- // names that star by the value itself ("3.5 stars"). Both go by the value
14667
- // as that name writes it, to hundredths, so a value a hair over a whole
14668
- // number checks that number's star under its own name, and no two stars
14669
- // share a name.
14670
- const shownValue = Number(currentValue.toFixed(2));
14671
- const checkedIndex = shownValue > 0 ? Math.min(Math.ceil(shownValue), max) - 1 : -1;
14672
- // Render icons
14673
- const renderIcons = () => {
14674
- const icons = [];
14675
- for (let i = 0; i < max; i++) {
14676
- const iconIndex = rtl ? max - 1 - i : i;
14677
- const fillPercent = getFillPercent(iconIndex, displayValue);
14678
- const isActive = fillPercent === 100;
14679
- const isHovered = hoverValue !== null && iconIndex < hoverValue;
14680
- const isPartial = fillPercent > 0 && fillPercent < 100;
14681
- const icon = isPartial
14682
- ? renderPartialIcon(iconIndex, fillPercent)
14683
- : renderIcon(iconIndex, isActive, fillPercent);
14684
- icons.push(jsxRuntime.jsx("span", { className: prefixedClassNames(classPrefix, 'rate-item', {
14685
- 'is-active': isActive,
14686
- 'is-hovered': isHovered,
14687
- }), onClick: e => handleClick(iconIndex, e), onMouseEnter: () => handleMouseEnter(iconIndex), onMouseMove: e => handleMouseMove(iconIndex, e), onMouseLeave: handleMouseLeave, id: `${starBaseId}-${iconIndex}`, role: "radio", "aria-checked": iconIndex === checkedIndex, "aria-label": iconIndex === checkedIndex && shownValue % 1 !== 0
14688
- ? `${shownValue} stars`
14689
- : `${iconIndex + 1} star${iconIndex === 0 ? '' : 's'}`, tabIndex: -1, children: icon }, iconIndex));
14690
- }
14691
- return icons;
15796
+ // The browser cuts a paste to `maxLength` before anything else sees it,
15797
+ // so a code pasted as "123 456" would lose its last digit. What the code
15798
+ // takes is kept first, and it replaces the whole code, since what you
15799
+ // paste into a code field is the code.
15800
+ const handlePaste = (e) => {
15801
+ onPaste?.(e);
15802
+ if (e.defaultPrevented)
15803
+ return;
15804
+ e.preventDefault();
15805
+ const pasted = keep(e.clipboardData.getData('text'));
15806
+ if (pasted && !e.currentTarget.readOnly)
15807
+ commit(pasted);
14692
15808
  };
14693
- const text = getText();
15809
+ const rootClass = classNames(usePrefixedClassNames('pin-input'), bulmaHelperClasses, className);
15810
+ const fieldClass = usePrefixedClassNames('pin-input-field');
14694
15811
  const helpClass = usePrefixedClassNames('help', {
14695
15812
  [`is-${messageColor}`]: !!messageColor,
14696
15813
  });
14697
- const messageEl = message ? jsxRuntime.jsx("p", { className: helpClass, children: message }) : null;
14698
- const rateItemsClass = usePrefixedClassNames('rate-items');
14699
- const rateScoreClass = usePrefixedClassNames('rate-score');
14700
- const rateTextClass = usePrefixedClassNames('rate-text');
14701
- const rateCustomTextClass = usePrefixedClassNames('rate-custom-text');
14702
- const rateElement = (jsxRuntime.jsxs("div", { ref: ref, className: combinedClasses, role: "radiogroup", "aria-label": ariaLabelledBy ? undefined : 'Rating', "aria-labelledby": ariaLabelledBy, "aria-activedescendant": checkedIndex >= 0 ? `${starBaseId}-${checkedIndex}` : undefined, tabIndex: disabled ? -1 : 0, onKeyDown: handleKeyDown, ...rest, children: [jsxRuntime.jsx("div", { className: rateItemsClass, children: renderIcons() }), showScore && (jsxRuntime.jsx("span", { className: rateScoreClass, children: getScoreDisplay() })), text && jsxRuntime.jsx("span", { className: rateTextClass, children: text }), customText && (jsxRuntime.jsx("span", { className: rateCustomTextClass, children: customText })), name && (jsxRuntime.jsx("input", { type: "hidden", name: name, value: currentValue, form: form }))] }));
14703
- let content = rateElement;
14704
- if (rendersOwnControl('Rate', { insideControl })) {
15814
+ // The boxes the selection covers: the one at a caret, or every selected
15815
+ // one. A caret after the last character marks the last box.
15816
+ const [start, end] = selection ?? [-1, -1];
15817
+ const first = Math.min(start, length - 1);
15818
+ const last = Math.max(first + 1, Math.min(end, length));
15819
+ const boxes = Array.from({ length }, (_, i) => {
15820
+ const char = code[i];
15821
+ const active = i >= first && i < last;
15822
+ return (jsxRuntime.jsx("div", { "aria-hidden": "true", className: prefixedClassNames(classPrefix, 'input', 'pin-input-box', {
15823
+ [`is-${color}`]: !!color,
15824
+ [`is-${size}`]: !!size,
15825
+ 'is-rounded': isRounded,
15826
+ 'is-focused': active,
15827
+ 'has-caret': active && start === end && !char,
15828
+ 'is-disabled': disabled,
15829
+ }), children: char && (isMasked ? MASK : char) }, i));
15830
+ });
15831
+ // Left to right on a right-to-left page too: a code is written that way,
15832
+ // so its first character stays in the first box on the left, and the
15833
+ // arrow keys move the caret the way the boxes run.
15834
+ let content = (jsxRuntime.jsxs("div", { className: rootClass, dir: "ltr", children: [jsxRuntime.jsx("input", { ref: setRefs, className: fieldClass, type: isMasked ? 'password' : 'text', inputMode: type === 'alphanumeric' ? 'text' : 'numeric', autoComplete: "one-time-code", autoCorrect: "off", spellCheck: false,
15835
+ // A partial code fails constraint validation. `minLength` would
15836
+ // say so only after a keystroke, never after a paste or a filtered
15837
+ // character, which set the value from script.
15838
+ pattern: `${allowed}{${length}}`, ...rest, value: code, maxLength: length, disabled: disabled, onChange: handleChange, onPaste: handlePaste, onFocus: e => {
15839
+ follow();
15840
+ onFocus?.(e);
15841
+ }, onBlur: e => {
15842
+ setSelection(null);
15843
+ onBlur?.(e);
15844
+ },
15845
+ // After the spread: an undefined `id` key in rest would wipe it.
15846
+ id: controlId }), boxes] }));
15847
+ if (ownControl) {
14705
15848
  content = jsxRuntime.jsx(Control, { children: content });
14706
15849
  }
15850
+ const messageEl = message ? jsxRuntime.jsx("p", { className: helpClass, children: message }) : null;
14707
15851
  if (ownField) {
14708
15852
  return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
14709
15853
  }
14710
15854
  return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [content, messageEl] }));
14711
15855
  });
14712
- Rate.displayName = 'Rate';
15856
+ PinInput.displayName = 'PinInput';
14713
15857
 
14714
- // What the status region announces: the `empty` content, as the one item
14715
- // it's handed while that content is on screen because of the user's typing.
14716
- const typedNoMatches$1 = [{ id: 'empty' }];
14717
- const nothingToAnnounce$1 = [];
14718
- // A press on an option would take focus from the field, since an option
14719
- // can't hold it, and nothing would put it back. Cancelling the press keeps
14720
- // it there, as the ARIA Authoring Practices' combobox does, and the click
14721
- // still picks the option.
14722
- const keepFocus = (e) => e.preventDefault();
14723
- // An IME that's composing owns every key: Enter confirms what it composed,
14724
- // and the arrows move through its candidates. Safari can say so with
14725
- // `keyCode` 229 rather than `isComposing`.
14726
- const isComposing$2 = (e) => e.nativeEvent.isComposing || e.nativeEvent.keyCode === 229;
14727
- // Whether an option can be picked. The arrows still reach a disabled one, so
14728
- // a screen reader user finds it.
14729
- const isEnabled = (item) => typeof item === 'string' || !item.disabled;
14730
15858
  /**
14731
- * The `Autocomplete` component provides an input field with dropdown suggestions that filter based on user input.
15859
+ * Default star icon component.
15860
+ */
15861
+ const StarIcon = ({ filled }) => (jsxRuntime.jsx("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: filled ? 'currentColor' : 'none', stroke: "currentColor", strokeWidth: filled ? 0 : 2, "aria-hidden": "true", children: jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z" }) }));
15862
+ /** Snap a value to the nearest precision step. */
15863
+ function snapToPrecision(val, precision) {
15864
+ return Math.round(val / precision) * precision;
15865
+ }
15866
+ /** Get fill percentage for a given icon index based on current value. */
15867
+ function getFillPercent(iconIndex, value) {
15868
+ if (value >= iconIndex + 1)
15869
+ return 100;
15870
+ if (value <= iconIndex)
15871
+ return 0;
15872
+ return Math.round((value - iconIndex) * 100);
15873
+ }
15874
+ /**
15875
+ * The `Rate` component provides a star/icon-based rating system.
14732
15876
  *
14733
15877
  * @function
14734
- * @param {AutocompleteProps} props - Props for the Autocomplete component.
14735
- * @param {React.Ref<HTMLInputElement>} ref - Forwarded ref to the input element.
14736
- * @returns {JSX.Element} The rendered autocomplete component.
15878
+ * @param {RateProps} props - Props for the Rate component.
15879
+ * @param {React.Ref<HTMLDivElement>} ref - Forwarded ref to the container element.
15880
+ * @returns {JSX.Element} The rendered rating component.
14737
15881
  *
14738
15882
  * @example
14739
- * // Basic usage with string array
14740
- * <Autocomplete
14741
- * data={['Apple', 'Banana', 'Cherry']}
14742
- * placeholder="Search fruit..."
14743
- * onSelect={(item) => console.log(item)}
14744
- * />
15883
+ * // Basic 5-star rating
15884
+ * <Rate defaultValue={3} />
14745
15885
  *
14746
15886
  * @example
14747
- * // With object data
14748
- * <Autocomplete
14749
- * data={[{ value: '1', label: 'Option 1' }, { value: '2', label: 'Option 2' }]}
14750
- * field="label"
14751
- * onSelect={(item) => console.log(item)}
14752
- * />
15887
+ * // With color and precision
15888
+ * <Rate defaultValue={3.5} color="warning" precision={0.5} showScore />
15889
+ *
15890
+ * @example
15891
+ * // With Font Awesome icons
15892
+ * <Rate iconName="star" iconLibrary="fa" iconVariant="solid" />
14753
15893
  */
14754
- const Autocomplete = React.forwardRef(({ data = [], value: controlledValue, placeholder, field = 'label', clearable = false, openOnFocus = false, keepFirst = false, keepOpen = false, selectOnClickOutside = false, maxHeight = 200, loading = false, disabled = false, checkInfiniteScroll = false, infiniteScrollDistance = 50, color, size, onInput, onSelect, onActiveChange, onInfiniteScroll, itemTemplate, header, footer, empty, name, form, required, id,
14755
- // Deprecated no-ops, taken here so they stay off the wrapper.
14756
- // eslint-disable-next-line @typescript-eslint/no-unused-vars -- deprecated no-op
14757
- selected,
14758
- // eslint-disable-next-line @typescript-eslint/no-unused-vars -- deprecated no-op
14759
- dropdown,
14760
- // Field props
14761
- label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, className, ...props }, ref) => {
15894
+ const Rate = React.forwardRef(({ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, value: controlledValue, defaultValue = 0, max = 5, size, disabled = false, showScore = false, showText = false, texts, onChange, customIcon, spaced = false, rtl = false, iconName, iconLibrary: iconLibraryProp, iconVariant, iconFeatures, color, precision = 1, customText, name, form, className, ...props }, ref) => {
14762
15895
  const insideField = useInsideField();
14763
- // The suggestion list points `aria-labelledby` at the label that names
14764
- // the input, so it needs that label's id.
14765
- const { controlId, fieldLabelProps, labelId: listLabelId, } = useAutoLabelId({
15896
+ const insideControl = useInsideControl();
15897
+ const ownField = rendersOwnField('Rate', {
15898
+ insideField,
15899
+ insideControl,
15900
+ label,
15901
+ message,
15902
+ horizontal,
15903
+ fieldClassName,
15904
+ });
15905
+ const { ariaLabelledBy, fieldLabelProps } = useAutoLabelledBy({
14766
15906
  label,
14767
- id,
14768
15907
  labelProps,
14769
- rendersLabel: !insideField,
14770
- needsLabelId: true,
15908
+ rendersLabel: ownField,
15909
+ callerProps: props,
14771
15910
  });
14772
15911
  const { bulmaHelperClasses, rest } = useBulmaClasses(props);
14773
- const { classPrefix } = useConfig();
14774
- const containerRef = React.useRef(null);
14775
- const inputRef = React.useRef(null);
14776
- const dropdownRef = React.useRef(null);
14777
- const [internalValue, setInternalValue] = React.useState('');
14778
- const [isActive, setIsActive] = React.useState(false);
14779
- // Whether the user has typed since the list opened, since only typing
14780
- // gets the `empty` content announced. Closing the list resets it (during
14781
- // render, React's pattern for adjusting state when other state changes),
14782
- // so opening the list again without typing stays quiet.
14783
- const [typed, setTyped] = React.useState(false);
14784
- if (typed && !isActive)
14785
- setTyped(false);
14786
- const [highlightedIndex, setHighlightedIndex] = React.useState(-1);
14787
- const listboxId = `${React.useId()}-listbox`;
14788
- // Use controlled or internal value
14789
- const inputValue = controlledValue !== undefined ? controlledValue : internalValue;
14790
- // Get display value from item
14791
- const getDisplayValue = (item) => {
14792
- if (typeof item === 'string')
14793
- return item;
14794
- const fieldValue = item[field];
14795
- if (typeof fieldValue === 'string')
14796
- return fieldValue;
14797
- return item.value || '';
14798
- };
14799
- // Filter data based on input
14800
- const filteredData = data.filter(item => {
14801
- const displayValue = getDisplayValue(item);
14802
- return displayValue.toLowerCase().includes(inputValue.toLowerCase());
14803
- });
14804
- // The dropdown shows the options or, when nothing matches, the `empty`
14805
- // content. The listbox holds only options, so it's on screen only while
14806
- // there are some.
14807
- const menuOpen = isActive && (filteredData.length > 0 || !!empty);
14808
- const listOpen = isActive && filteredData.length > 0;
14809
- // Handle input change
14810
- const handleInputChange = React.useCallback((e) => {
14811
- const newValue = e.target.value;
14812
- if (controlledValue === undefined) {
14813
- setInternalValue(newValue);
14814
- }
14815
- onInput?.(newValue);
14816
- setTyped(true);
14817
- if (!isActive && newValue) {
14818
- setIsActive(true);
14819
- }
14820
- }, [controlledValue, isActive, onInput]);
14821
- // Handle item selection
14822
- const handleSelect = React.useCallback((item) => {
14823
- if (typeof item !== 'string' && item.disabled)
14824
- return;
14825
- const displayValue = getDisplayValue(item);
14826
- if (controlledValue === undefined) {
14827
- setInternalValue(displayValue);
14828
- }
14829
- onInput?.(displayValue);
14830
- onSelect?.(item);
14831
- if (!keepOpen) {
14832
- setIsActive(false);
14833
- }
14834
- setHighlightedIndex(-1);
14835
- }, [controlledValue, field, keepOpen, onInput, onSelect]);
14836
- // Handle clear
14837
- const handleClear = React.useCallback(() => {
14838
- if (controlledValue === undefined) {
14839
- setInternalValue('');
14840
- }
14841
- onInput?.('');
14842
- onSelect?.(null);
14843
- inputRef.current?.focus();
14844
- }, [controlledValue, onInput, onSelect]);
14845
- // Handle focus
14846
- const handleFocus = React.useCallback(() => {
14847
- if (openOnFocus && !disabled) {
14848
- setIsActive(true);
14849
- }
14850
- }, [openOnFocus, disabled]);
14851
- // Read when a click lands outside, so the listener stays put while the
14852
- // list is open. What it selects changes as the user types and arrows
14853
- // through suggestions, the filtered list is new on every render, and an
14854
- // inline `onInput` or `onSelect` changes on every render of the parent.
14855
- // Typing leaves the highlight where it was, so a list that narrowed can
14856
- // leave it past the last suggestion, with nothing there to select.
14857
- const selectHighlightedRef = useLatestRef(() => {
14858
- if (selectOnClickOutside &&
14859
- highlightedIndex >= 0 &&
14860
- highlightedIndex < filteredData.length) {
14861
- handleSelect(filteredData[highlightedIndex]);
14862
- }
14863
- });
14864
- // Handle blur (click outside)
14865
- React.useEffect(() => {
14866
- if (!isActive)
14867
- return undefined;
14868
- const handleClickOutside = (e) => {
14869
- if (!containerRef.current)
14870
- return;
14871
- // Use composedPath() to correctly detect clicks inside Shadow DOM
14872
- const path = e.composedPath();
14873
- const isInside = path.includes(containerRef.current);
14874
- if (!isInside) {
14875
- selectHighlightedRef.current();
14876
- setIsActive(false);
14877
- }
14878
- };
14879
- document.addEventListener('mousedown', handleClickOutside);
14880
- return () => document.removeEventListener('mousedown', handleClickOutside);
14881
- }, [isActive, selectHighlightedRef]);
14882
- // Read when the dropdown opens or closes, so an inline `onActiveChange`,
14883
- // new on every render of the parent, isn't told again about a state that
14884
- // didn't change.
14885
- const onActiveChangeRef = useLatestRef(onActiveChange);
14886
- // Notify active state change
14887
- React.useEffect(() => {
14888
- onActiveChangeRef.current?.(isActive);
14889
- }, [isActive, onActiveChangeRef]);
14890
- // Keep first highlighted
14891
- React.useEffect(() => {
14892
- if (keepFirst && isActive && filteredData.length > 0) {
14893
- setHighlightedIndex(0);
14894
- }
14895
- }, [keepFirst, isActive, filteredData.length, inputValue]);
14896
- // Handle keyboard navigation
14897
- const handleKeyDown = React.useCallback((e) => {
14898
- if (disabled || isComposing$2(e))
15912
+ const starBaseId = React.useId();
15913
+ const [internalValue, setInternalValue] = React.useState(defaultValue);
15914
+ const [hoverValue, setHoverValue] = React.useState(null);
15915
+ const defaultIconLibrary = useIconLibrary();
15916
+ const resolvedLibrary = iconLibraryProp || defaultIconLibrary || 'fa';
15917
+ // Determine if controlled
15918
+ const isControlled = controlledValue !== undefined;
15919
+ const currentValue = isControlled ? controlledValue : internalValue;
15920
+ const displayValue = hoverValue !== null ? hoverValue : currentValue;
15921
+ // Update value
15922
+ const updateValue = React.useCallback((newValue) => {
15923
+ if (disabled)
14899
15924
  return;
14900
- switch (e.key) {
14901
- case 'ArrowDown':
14902
- e.preventDefault();
14903
- if (!isActive) {
14904
- // A closed list opens on its first enabled option, as the ARIA
14905
- // Authoring Practices' combobox does. Alt+Down Arrow opens it
14906
- // without a highlight.
14907
- setIsActive(true);
14908
- setHighlightedIndex(e.altKey ? -1 : filteredData.map(isEnabled).indexOf(true));
14909
- }
14910
- else {
14911
- setHighlightedIndex(prev => prev < filteredData.length - 1 ? prev + 1 : prev);
14912
- }
14913
- break;
14914
- case 'ArrowUp':
14915
- e.preventDefault();
14916
- if (!isActive) {
14917
- // And Up Arrow opens it on its last enabled option.
14918
- setIsActive(true);
14919
- setHighlightedIndex(filteredData.map(isEnabled).lastIndexOf(true));
14920
- }
14921
- else {
14922
- setHighlightedIndex(prev => (prev > 0 ? prev - 1 : prev));
14923
- }
14924
- break;
14925
- case 'Enter':
14926
- // With the list open on a highlighted option it can pick, Enter
14927
- // is that option's. Otherwise it closes the list and goes on, so
14928
- // a form around the field submits, as it would from any text
14929
- // field.
14930
- if (isActive &&
14931
- highlightedIndex >= 0 &&
14932
- filteredData[highlightedIndex] &&
14933
- isEnabled(filteredData[highlightedIndex])) {
14934
- e.preventDefault();
14935
- handleSelect(filteredData[highlightedIndex]);
14936
- }
14937
- else {
14938
- setIsActive(false);
14939
- }
14940
- break;
14941
- case 'Escape':
14942
- // Only closing the dropdown claims the key. With it closed, the
14943
- // key goes on to a Modal around the field.
14944
- if (menuOpen)
14945
- e.preventDefault();
14946
- setIsActive(false);
14947
- setHighlightedIndex(-1);
14948
- break;
14949
- case 'Tab':
14950
- if (isActive &&
14951
- highlightedIndex >= 0 &&
14952
- filteredData[highlightedIndex]) {
14953
- handleSelect(filteredData[highlightedIndex]);
14954
- }
14955
- setIsActive(false);
14956
- break;
15925
+ // Allow clicking same value to deselect (set to 0)
15926
+ const finalValue = newValue === currentValue ? 0 : newValue;
15927
+ if (!isControlled) {
15928
+ setInternalValue(finalValue);
14957
15929
  }
14958
- }, [
14959
- disabled,
14960
- isActive,
14961
- menuOpen,
14962
- highlightedIndex,
14963
- filteredData,
14964
- handleSelect,
14965
- ]);
14966
- // Handle dropdown scroll for infinite scroll
14967
- const handleDropdownScroll = React.useCallback(() => {
14968
- if (!checkInfiniteScroll || !dropdownRef.current)
15930
+ onChange?.(finalValue);
15931
+ }, [isControlled, currentValue, disabled, onChange]);
15932
+ // Handle click with precision support
15933
+ const handleClick = React.useCallback((iconIndex, e) => {
15934
+ if (disabled)
14969
15935
  return;
14970
- const { scrollTop, scrollHeight, clientHeight } = dropdownRef.current;
14971
- if (scrollHeight - scrollTop - clientHeight <= infiniteScrollDistance) {
14972
- onInfiniteScroll?.();
15936
+ if (precision < 1) {
15937
+ const rect = e.currentTarget.getBoundingClientRect();
15938
+ let relativeX = (e.clientX - rect.left) / rect.width;
15939
+ if (rtl)
15940
+ relativeX = 1 - relativeX;
15941
+ const rawValue = iconIndex + relativeX;
15942
+ const snapped = snapToPrecision(rawValue, precision);
15943
+ const clamped = Math.max(precision, Math.min(snapped, max));
15944
+ updateValue(clamped);
14973
15945
  }
14974
- }, [checkInfiniteScroll, infiniteScrollDistance, onInfiniteScroll]);
14975
- // Scroll highlighted item into view
14976
- React.useEffect(() => {
14977
- if (highlightedIndex >= 0 && dropdownRef.current) {
14978
- const highlightedEl = dropdownRef.current.querySelector(`[data-index="${highlightedIndex}"]`);
14979
- if (highlightedEl &&
14980
- typeof highlightedEl.scrollIntoView === 'function') {
14981
- highlightedEl.scrollIntoView({ block: 'nearest' });
15946
+ else {
15947
+ updateValue(iconIndex + 1);
15948
+ }
15949
+ }, [disabled, precision, rtl, max, updateValue]);
15950
+ // Handle mouse enter on icon
15951
+ const handleMouseEnter = React.useCallback((iconIndex) => {
15952
+ if (disabled)
15953
+ return;
15954
+ setHoverValue(iconIndex + 1);
15955
+ }, [disabled]);
15956
+ // Handle mouse move with precision support
15957
+ const handleMouseMove = React.useCallback((iconIndex, e) => {
15958
+ if (disabled || precision >= 1)
15959
+ return;
15960
+ const rect = e.currentTarget.getBoundingClientRect();
15961
+ let relativeX = (e.clientX - rect.left) / rect.width;
15962
+ if (rtl)
15963
+ relativeX = 1 - relativeX;
15964
+ const rawValue = iconIndex + relativeX;
15965
+ const snapped = snapToPrecision(rawValue, precision);
15966
+ const clamped = Math.max(precision, Math.min(snapped, max));
15967
+ setHoverValue(clamped);
15968
+ }, [disabled, precision, rtl, max]);
15969
+ // Handle mouse leave
15970
+ const handleMouseLeave = React.useCallback(() => {
15971
+ setHoverValue(null);
15972
+ }, []);
15973
+ // Handle keyboard
15974
+ const handleKeyDown = React.useCallback((e) => {
15975
+ if (disabled)
15976
+ return;
15977
+ let newValue = currentValue;
15978
+ const step = precision < 1 ? precision : 1;
15979
+ if (e.key === 'ArrowRight' || e.key === 'ArrowUp') {
15980
+ e.preventDefault();
15981
+ newValue = Math.min(currentValue + step, max);
15982
+ }
15983
+ else if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') {
15984
+ e.preventDefault();
15985
+ newValue = Math.max(currentValue - step, 0);
15986
+ }
15987
+ else if (e.key === 'Home') {
15988
+ e.preventDefault();
15989
+ newValue = 0;
15990
+ }
15991
+ else if (e.key === 'End') {
15992
+ e.preventDefault();
15993
+ newValue = max;
15994
+ }
15995
+ // Round to avoid floating point issues
15996
+ newValue = Math.round(newValue * 1000) / 1000;
15997
+ if (newValue !== currentValue) {
15998
+ if (!isControlled) {
15999
+ setInternalValue(newValue);
14982
16000
  }
16001
+ onChange?.(newValue);
14983
16002
  }
14984
- }, [highlightedIndex]);
14985
- // Combined ref
14986
- const combinedRef = React.useCallback((node) => {
14987
- inputRef.current =
14988
- node;
14989
- if (typeof ref === 'function') {
14990
- ref(node);
16003
+ }, [currentValue, max, precision, disabled, isControlled, onChange]);
16004
+ // Get text for current value
16005
+ const getText = () => {
16006
+ if (!showText || !texts)
16007
+ return null;
16008
+ const wholeIndex = Math.ceil(displayValue) - 1;
16009
+ const baseText = texts[wholeIndex] || null;
16010
+ if (!baseText)
16011
+ return null;
16012
+ // For fractional hover values with precision < 1, append "+"
16013
+ if (precision < 1 && hoverValue !== null && hoverValue % 1 !== 0) {
16014
+ return `${baseText}+`;
14991
16015
  }
14992
- else if (ref) {
14993
- ref.current =
14994
- node;
16016
+ return baseText;
16017
+ };
16018
+ // Format score display
16019
+ const getScoreDisplay = () => {
16020
+ if (precision < 1 && currentValue % 1 !== 0) {
16021
+ return currentValue.toFixed(1);
14995
16022
  }
14996
- }, [ref]);
16023
+ return currentValue;
16024
+ };
14997
16025
  // Generate classes
14998
- const autocompleteClasses = usePrefixedClassNames('autocomplete', {
14999
- 'is-active': isActive,
15000
- [`is-${size}`]: !!size,
15001
- });
15002
- const inputClasses = usePrefixedClassNames('input', {
15003
- [`is-${color}`]: !!color,
15004
- [`is-${size}`]: !!size,
15005
- 'is-loading': loading,
15006
- });
15007
- const controlClasses = usePrefixedClassNames('control', 'is-expanded', 'has-icons-right');
15008
- const dropdownMenuClasses = usePrefixedClassNames('dropdown-menu', {
15009
- 'is-active': menuOpen,
15010
- });
15011
- const dropdownContentClass = usePrefixedClassNames('dropdown-content');
15012
- const dropdownHeaderClass = usePrefixedClassNames('dropdown-header');
15013
- const dropdownFooterClass = usePrefixedClassNames('dropdown-footer');
15014
- const emptyItemClasses = usePrefixedClassNames('dropdown-item', 'has-text-grey');
15015
- const iconRightClickableClass = usePrefixedClassNames('icon', 'is-right', 'is-clickable');
15016
- const iconRightClass = usePrefixedClassNames('icon', 'is-right');
15017
- const loaderClass = usePrefixedClassNames('loader', 'is-loading');
15018
- const combinedClasses = classNames(autocompleteClasses, bulmaHelperClasses, className);
15019
- const helpClass = usePrefixedClassNames('help', {
15020
- [`is-${messageColor}`]: !!messageColor,
16026
+ const classPrefix = useClassPrefix();
16027
+ const rateClasses = usePrefixedClassNames('rate', {
16028
+ [`is-${size}`]: size,
16029
+ 'is-disabled': disabled,
16030
+ 'is-spaced': spaced,
16031
+ 'is-rtl': rtl,
16032
+ [`is-${color}`]: color,
15021
16033
  });
15022
- const messageEl = message ? jsxRuntime.jsx("p", { className: helpClass, children: message }) : null;
15023
- // The clear button gives way to a spinner at its spot: the `loading` one,
15024
- // or that of a loading Control this sits in.
15025
- const controlLoading = useControlLoading();
15026
- const showClear = clearable && !!inputValue && !disabled && !loading && !controlLoading;
15027
- // The combobox controls its listbox and points at the highlighted option.
15028
- // The listbox takes its name from the label that names the combobox, the
15029
- // one this renders or a surrounding Field's, so the two say the same
15030
- // thing. When no label names the combobox, or the one that does has no
15031
- // id to point at, the list keeps a fallback name. While the dropdown
15032
- // shows only the `empty` content there's no listbox, so the combobox is
15033
- // collapsed and points at nothing.
15034
- const activeOptionId = listOpen &&
15035
- highlightedIndex >= 0 &&
15036
- highlightedIndex < filteredData.length
15037
- ? `${listboxId}-${highlightedIndex}`
15038
- : undefined;
15039
- // A polite status region, in the page from the start so it's there
15040
- // before anything is written into it, announces the `empty` content
15041
- // when the user's typing leaves nothing that matches what's in the
15042
- // field. It follows what's on screen, so results that come in after the
15043
- // typing count too. It's said once each time that happens, not again on
15044
- // each keystroke while nothing matches. An empty field isn't a search,
15045
- // so it stays quiet then, even with no data. While `loading`, or a
15046
- // loading Control around it, says results are on their way, it waits,
15047
- // and it's said once loading ends with nothing matching.
15048
- const emptyItems = typed &&
15049
- menuOpen &&
15050
- !listOpen &&
15051
- !!inputValue &&
15052
- !loading &&
15053
- !controlLoading
15054
- ? typedNoMatches$1
15055
- : nothingToAnnounce$1;
15056
- // The region reads the text on screen, whatever `empty` renders.
15057
- const emptyRef = React.useRef(null);
15058
- const describeEmpty = React.useCallback(() => {
15059
- const node = emptyRef.current;
15060
- /* istanbul ignore if: it's only announced while it's on screen */
15061
- if (!node)
15062
- return null;
15063
- return spokenText(node);
15064
- }, []);
15065
- const autocompleteElement = (jsxRuntime.jsxs("div", { ref: containerRef, className: combinedClasses, ...rest, children: [jsxRuntime.jsxs("div", { className: controlClasses, children: [jsxRuntime.jsx("input", { ref: combinedRef, type: "text", className: inputClasses, id: controlId, value: inputValue, placeholder: placeholder, disabled: disabled, name: name, form: form, required: required, onChange: handleInputChange, onFocus: handleFocus, onKeyDown: handleKeyDown, role: "combobox", "aria-expanded": listOpen, "aria-controls": listOpen ? listboxId : undefined, "aria-activedescendant": activeOptionId, "aria-haspopup": "listbox", "aria-autocomplete": "list", autoComplete: "off" }), showClear && (jsxRuntime.jsx("button", { type: "button", className: iconRightClickableClass, onClick: handleClear, "aria-label": "Clear", children: jsxRuntime.jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", width: "16", height: "16", "aria-hidden": "true", children: [jsxRuntime.jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }), jsxRuntime.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })] }) })), loading && (jsxRuntime.jsx("span", { className: iconRightClass, children: jsxRuntime.jsx("span", { className: loaderClass }) }))] }), menuOpen && (jsxRuntime.jsx("div", { className: dropdownMenuClasses, children: jsxRuntime.jsxs("div", { ref: dropdownRef, className: dropdownContentClass, style: { maxHeight: `${maxHeight}px`, overflowY: 'auto' }, onScroll: handleDropdownScroll, children: [header && jsxRuntime.jsx("div", { className: dropdownHeaderClass, children: header }), listOpen ? (jsxRuntime.jsx("div", { id: listboxId, role: "listbox", "aria-labelledby": listLabelId, "aria-label": listLabelId ? undefined : 'Suggestions', onMouseDown: keepFocus, children: filteredData.map((item, index) => {
15066
- const isDisabled = typeof item !== 'string' && item.disabled;
15067
- const isHighlighted = index === highlightedIndex;
15068
- const itemClasses = prefixedClassNames(classPrefix, 'dropdown-item', {
15069
- 'is-active': isHighlighted,
15070
- 'is-disabled': isDisabled,
15071
- });
15072
- return (jsxRuntime.jsx("a", { id: `${listboxId}-${index}`, "data-index": index, className: itemClasses, onClick: () => !isDisabled && handleSelect(item), onMouseEnter: () => !isDisabled && setHighlightedIndex(index), role: "option", "aria-selected": isHighlighted && !isDisabled, "aria-disabled": isDisabled, children: itemTemplate
15073
- ? itemTemplate(item)
15074
- : getDisplayValue(item) }, index));
15075
- }) })) : (jsxRuntime.jsx("div", { ref: emptyRef, className: emptyItemClasses, onMouseDown: keepFocus, children: empty })), footer && jsxRuntime.jsx("div", { className: dropdownFooterClass, children: footer })] }) })), jsxRuntime.jsx(StatusRegion, { items: emptyItems, describe: describeEmpty })] }));
15076
- if (!insideField) {
15077
- return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [autocompleteElement, messageEl] }));
16034
+ const combinedClasses = classNames(rateClasses, bulmaHelperClasses, className);
16035
+ // Render a single icon based on priority: customIcon > iconName > default SVG
16036
+ const renderIcon = (iconIndex, isActive, fillPercent) => {
16037
+ const iconProps = {
16038
+ index: iconIndex,
16039
+ isActive,
16040
+ isHovered: hoverValue !== null && iconIndex < hoverValue,
16041
+ value: displayValue,
16042
+ fillPercent,
16043
+ };
16044
+ if (customIcon) {
16045
+ return customIcon(iconProps);
16046
+ }
16047
+ if (iconName) {
16048
+ return (jsxRuntime.jsx(Icon, { name: iconName, library: resolvedLibrary, variant: iconVariant, features: iconFeatures }));
16049
+ }
16050
+ return jsxRuntime.jsx(StarIcon, { filled: isActive });
16051
+ };
16052
+ // Render partial icon with layered approach
16053
+ const renderPartialIcon = (iconIndex, fillPercent) => {
16054
+ if (customIcon) {
16055
+ // For custom icons, pass fillPercent and let consumer handle it
16056
+ const iconProps = {
16057
+ index: iconIndex,
16058
+ isActive: fillPercent > 0,
16059
+ isHovered: hoverValue !== null && iconIndex < (hoverValue ?? 0),
16060
+ value: displayValue,
16061
+ fillPercent,
16062
+ };
16063
+ return customIcon(iconProps);
16064
+ }
16065
+ if (iconName) {
16066
+ return (jsxRuntime.jsxs("span", { className: prefixedClassNames(classPrefix, 'rate-icon-partial'), style: {
16067
+ '--rate-fill-percent': `${fillPercent}%`,
16068
+ }, children: [jsxRuntime.jsx("span", { className: prefixedClassNames(classPrefix, 'rate-icon-bg'), children: jsxRuntime.jsx(Icon, { name: iconName, library: resolvedLibrary, variant: iconVariant, features: iconFeatures }) }), jsxRuntime.jsx("span", { className: prefixedClassNames(classPrefix, 'rate-icon-fg'), children: jsxRuntime.jsx(Icon, { name: iconName, library: resolvedLibrary, variant: iconVariant, features: iconFeatures }) })] }));
16069
+ }
16070
+ // Default SVG with clipPath for partial fill
16071
+ const clipId = `rate-clip-${iconIndex}`;
16072
+ return (jsxRuntime.jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", "aria-hidden": "true", children: [jsxRuntime.jsx("defs", { children: jsxRuntime.jsx("clipPath", { id: clipId, children: jsxRuntime.jsx("rect", { x: "0", y: "0", width: `${(fillPercent / 100) * 24}`, height: "24" }) }) }), jsxRuntime.jsx("path", { fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", d: "M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z" }), jsxRuntime.jsx("path", { clipPath: `url(#${clipId})`, fill: "currentColor", d: "M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z" })] }));
16073
+ };
16074
+ // The star the value falls in is the checked radio, and it carries the
16075
+ // value: a radiogroup takes no aria-value* of its own. A fractional value
16076
+ // names that star by the value itself ("3.5 stars"). Both go by the value
16077
+ // as that name writes it, to hundredths, so a value a hair over a whole
16078
+ // number checks that number's star under its own name, and no two stars
16079
+ // share a name.
16080
+ const shownValue = Number(currentValue.toFixed(2));
16081
+ const checkedIndex = shownValue > 0 ? Math.min(Math.ceil(shownValue), max) - 1 : -1;
16082
+ // Render icons
16083
+ const renderIcons = () => {
16084
+ const icons = [];
16085
+ for (let i = 0; i < max; i++) {
16086
+ const iconIndex = rtl ? max - 1 - i : i;
16087
+ const fillPercent = getFillPercent(iconIndex, displayValue);
16088
+ const isActive = fillPercent === 100;
16089
+ const isHovered = hoverValue !== null && iconIndex < hoverValue;
16090
+ const isPartial = fillPercent > 0 && fillPercent < 100;
16091
+ const icon = isPartial
16092
+ ? renderPartialIcon(iconIndex, fillPercent)
16093
+ : renderIcon(iconIndex, isActive, fillPercent);
16094
+ icons.push(jsxRuntime.jsx("span", { className: prefixedClassNames(classPrefix, 'rate-item', {
16095
+ 'is-active': isActive,
16096
+ 'is-hovered': isHovered,
16097
+ }), onClick: e => handleClick(iconIndex, e), onMouseEnter: () => handleMouseEnter(iconIndex), onMouseMove: e => handleMouseMove(iconIndex, e), onMouseLeave: handleMouseLeave, id: `${starBaseId}-${iconIndex}`, role: "radio", "aria-checked": iconIndex === checkedIndex, "aria-label": iconIndex === checkedIndex && shownValue % 1 !== 0
16098
+ ? `${shownValue} stars`
16099
+ : `${iconIndex + 1} star${iconIndex === 0 ? '' : 's'}`, tabIndex: -1, children: icon }, iconIndex));
16100
+ }
16101
+ return icons;
16102
+ };
16103
+ const text = getText();
16104
+ const helpClass = usePrefixedClassNames('help', {
16105
+ [`is-${messageColor}`]: !!messageColor,
16106
+ });
16107
+ const messageEl = message ? jsxRuntime.jsx("p", { className: helpClass, children: message }) : null;
16108
+ const rateItemsClass = usePrefixedClassNames('rate-items');
16109
+ const rateScoreClass = usePrefixedClassNames('rate-score');
16110
+ const rateTextClass = usePrefixedClassNames('rate-text');
16111
+ const rateCustomTextClass = usePrefixedClassNames('rate-custom-text');
16112
+ const rateElement = (jsxRuntime.jsxs("div", { ref: ref, className: combinedClasses, role: "radiogroup", "aria-label": ariaLabelledBy ? undefined : 'Rating', "aria-labelledby": ariaLabelledBy, "aria-activedescendant": checkedIndex >= 0 ? `${starBaseId}-${checkedIndex}` : undefined, tabIndex: disabled ? -1 : 0, onKeyDown: handleKeyDown, ...rest, children: [jsxRuntime.jsx("div", { className: rateItemsClass, children: renderIcons() }), showScore && (jsxRuntime.jsx("span", { className: rateScoreClass, children: getScoreDisplay() })), text && jsxRuntime.jsx("span", { className: rateTextClass, children: text }), customText && (jsxRuntime.jsx("span", { className: rateCustomTextClass, children: customText })), name && (jsxRuntime.jsx("input", { type: "hidden", name: name, value: currentValue, form: form }))] }));
16113
+ let content = rateElement;
16114
+ if (rendersOwnControl('Rate', { insideControl })) {
16115
+ content = jsxRuntime.jsx(Control, { children: content });
15078
16116
  }
15079
- return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [autocompleteElement, messageEl] }));
16117
+ if (ownField) {
16118
+ return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
16119
+ }
16120
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [content, messageEl] }));
15080
16121
  });
15081
- Autocomplete.displayName = 'Autocomplete';
16122
+ Rate.displayName = 'Rate';
15082
16123
 
15083
- // INTERNAL: deliberately not exported from src/index.ts.
15084
- /** An IME is composing, so the key belongs to it (`keyCode` 229 is Safari's word for it). */
15085
- const isComposing$1 = (event) => event.nativeEvent.isComposing || event.nativeEvent.keyCode === 229;
15086
- const sameValues = (a, b) => a.length === b.length && a.every((value, i) => value === b[i]);
16124
+ // What the status region announces: the `empty` content, as the one item
16125
+ // it's handed while that content is on screen because of the user's typing.
16126
+ const typedNoMatches$1 = [{ id: 'empty' }];
16127
+ const nothingToAnnounce$1 = [];
16128
+ // A press on an option would take focus from the field, since an option
16129
+ // can't hold it, and nothing would put it back. Cancelling the press keeps
16130
+ // it there, as the ARIA Authoring Practices' combobox does, and the click
16131
+ // still picks the option.
16132
+ const keepFocus = (e) => e.preventDefault();
16133
+ // An IME that's composing owns every key: Enter confirms what it composed,
16134
+ // and the arrows move through its candidates. Safari can say so with
16135
+ // `keyCode` 229 rather than `isComposing`.
16136
+ const isComposing$1 = (e) => e.nativeEvent.isComposing || e.nativeEvent.keyCode === 229;
16137
+ // Whether an option can be picked. The arrows still reach a disabled one, so
16138
+ // a screen reader user finds it.
16139
+ const isEnabled = (item) => typeof item === 'string' || !item.disabled;
15087
16140
  /**
15088
- * The keys, the open state, the highlight and the ARIA of an editable
15089
- * combobox whose list the app renders and filters (#1083). DOM focus stays
15090
- * in the field the whole time: the highlight is `aria-activedescendant`, as
15091
- * the ARIA Authoring Practices ask of a combobox, where `useMenuKeys` moves
15092
- * real focus, as a menu should.
16141
+ * The `Autocomplete` component provides an input field with dropdown suggestions that filter based on user input.
15093
16142
  *
15094
- * The options are read from the listbox in document order whenever they're
15095
- * needed, so options in groups and options that come and go are found, and
15096
- * the highlight is remembered by value, so it survives a re-render. The text
15097
- * in the field and any value are the app's: a pick only calls
15098
- * `onSelect`.
16143
+ * @function
16144
+ * @param {AutocompleteProps} props - Props for the Autocomplete component.
16145
+ * @param {React.Ref<HTMLInputElement>} ref - Forwarded ref to the input element.
16146
+ * @returns {JSX.Element} The rendered autocomplete component.
15099
16147
  *
15100
- * @function useComboboxEngine
15101
- * @param options - The open state, the pick callback and the shape.
15102
- * @returns The state and handlers the parts wire up.
16148
+ * @example
16149
+ * // Basic usage with string array
16150
+ * <Autocomplete
16151
+ * data={['Apple', 'Banana', 'Cherry']}
16152
+ * placeholder="Search fruit..."
16153
+ * onSelect={(item) => console.log(item)}
16154
+ * />
16155
+ *
16156
+ * @example
16157
+ * // With object data
16158
+ * <Autocomplete
16159
+ * data={[{ value: '1', label: 'Option 1' }, { value: '2', label: 'Option 2' }]}
16160
+ * field="label"
16161
+ * onSelect={(item) => console.log(item)}
16162
+ * />
15103
16163
  */
15104
- function useComboboxEngine({ open: controlledOpen, defaultOpen, onOpenChange, onSelect, onSettle, keepFirst, multiple, inline, deferred, }) {
15105
- const baseId = React.useId();
15106
- const listboxId = `${baseId}-listbox`;
15107
- const itemPrefix = `${baseId}-item-`;
15108
- // The value goes into the id encoded, so a value with a space in it still
15109
- // makes one id, not two.
15110
- const itemId = React.useCallback((value) => itemPrefix + encodeURIComponent(value), [itemPrefix]);
15111
- const rootRef = React.useRef(null);
16164
+ const Autocomplete = React.forwardRef(({ data = [], value: controlledValue, placeholder, field = 'label', clearable = false, openOnFocus = false, keepFirst = false, keepOpen = false, selectOnClickOutside = false, maxHeight = 200, loading = false, disabled = false, checkInfiniteScroll = false, infiniteScrollDistance = 50, color, size, onInput, onSelect, onActiveChange, onInfiniteScroll, itemTemplate, header, footer, empty, name, form, required, id,
16165
+ // Deprecated no-ops, taken here so they stay off the wrapper.
16166
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars -- deprecated no-op
16167
+ selected,
16168
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars -- deprecated no-op
16169
+ dropdown,
16170
+ // Field props
16171
+ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, className, ...props }, ref) => {
16172
+ const insideField = useInsideField();
16173
+ // The suggestion list points `aria-labelledby` at the label that names
16174
+ // the input, so it needs that label's id.
16175
+ const { controlId, fieldLabelProps, labelId: listLabelId, } = useAutoLabelId({
16176
+ label,
16177
+ id,
16178
+ labelProps,
16179
+ rendersLabel: !insideField,
16180
+ needsLabelId: true,
16181
+ });
16182
+ const { bulmaHelperClasses, rest } = useBulmaClasses(props);
16183
+ const { classPrefix } = useConfig();
16184
+ const containerRef = React.useRef(null);
15112
16185
  const inputRef = React.useRef(null);
15113
- const listboxRef = React.useRef(null);
15114
- const popupRef = React.useRef(null);
15115
- const insidePressRef = React.useRef(null);
15116
- const optionsRef = React.useRef(new Map());
15117
- const landingRef = React.useRef(null);
15118
- const wasOpenRef = React.useRef(false);
15119
- const shownValuesRef = React.useRef([]);
15120
- // Whether the next highlight change is one to scroll to: a key's or a
15121
- // landing's. The pointer's isn't, since the pointer is already on that
15122
- // option, and scrolling would move the list out from under it.
15123
- const scrollToHighlightRef = React.useRef(false);
15124
- const isControlled = controlledOpen !== undefined;
15125
- const [internalOpen, setInternalOpen] = React.useState(defaultOpen);
15126
- const open = inline || (controlledOpen ?? internalOpen);
15127
- const [highlighted, setHighlighted] = React.useState(null);
16186
+ const dropdownRef = React.useRef(null);
16187
+ const [internalValue, setInternalValue] = React.useState('');
16188
+ const [isActive, setIsActive] = React.useState(false);
16189
+ // Whether the user has typed since the list opened, since only typing
16190
+ // gets the `empty` content announced. Closing the list resets it (during
16191
+ // render, React's pattern for adjusting state when other state changes),
16192
+ // so opening the list again without typing stays quiet.
15128
16193
  const [typed, setTyped] = React.useState(false);
15129
- // Whether a listbox with an option in it is on the page, which only the
15130
- // page can say, so it's read once the open list has rendered.
15131
- const [listShown, setListShown] = React.useState(null);
15132
- // Options register from their own effects, which needn't render the
15133
- // component that holds this hook, so they ask for a render to be read.
15134
- const [, refresh] = React.useReducer((n) => n + 1, 0);
15135
- // Closing forgets the highlight, the typing and what was on the page,
15136
- // during render, React's pattern for adjusting state when other state
15137
- // changes.
15138
- if (!open) {
15139
- if (highlighted !== null)
15140
- setHighlighted(null);
15141
- if (typed)
15142
- setTyped(false);
15143
- if (listShown !== null)
15144
- setListShown(null);
15145
- }
15146
- const expanded = open && (listShown ?? !deferred);
15147
- /** The options on the page, in document order, groups included. */
15148
- const shownOptions = React.useCallback(() => {
15149
- const listbox = listboxRef.current;
15150
- if (!listbox)
15151
- return [];
15152
- const shown = [];
15153
- listbox
15154
- .querySelectorAll('[role="option"]')
15155
- .forEach(element => {
15156
- // Only the Combobox's own options: an element given the role by
15157
- // hand isn't one.
15158
- const state = optionsRef.current.get(element);
15159
- if (state)
15160
- shown.push({ element, ...state });
15161
- });
15162
- return shown;
15163
- }, []);
15164
- const requestOpen = React.useCallback((next, landing = 'open') => {
15165
- if (inline || next === open)
15166
- return;
15167
- landingRef.current = next ? landing : null;
15168
- onOpenChange?.(next);
15169
- if (!isControlled)
15170
- setInternalOpen(next);
15171
- }, [inline, open, onOpenChange, isControlled]);
15172
- const close = React.useCallback(() => requestOpen(false), [requestOpen]);
15173
- // After every render, read the list: whether it has options, and where
15174
- // the highlight goes now. A highlighted option that has left takes the
15175
- // highlight with it, or hands it to the first enabled one under
15176
- // `keepFirst`, which also moves it there whenever the options change.
15177
- // eslint-disable-next-line react-hooks/exhaustive-deps -- after every render on purpose
15178
- useClientLayoutEffect(() => {
15179
- const opened = open && !wasOpenRef.current;
15180
- wasOpenRef.current = open;
15181
- if (!open) {
15182
- landingRef.current = null;
15183
- shownValuesRef.current = [];
16194
+ if (typed && !isActive)
16195
+ setTyped(false);
16196
+ const [highlightedIndex, setHighlightedIndex] = React.useState(-1);
16197
+ const listboxId = `${React.useId()}-listbox`;
16198
+ // Use controlled or internal value
16199
+ const inputValue = controlledValue !== undefined ? controlledValue : internalValue;
16200
+ // Get display value from item
16201
+ const getDisplayValue = (item) => {
16202
+ if (typeof item === 'string')
16203
+ return item;
16204
+ const fieldValue = item[field];
16205
+ if (typeof fieldValue === 'string')
16206
+ return fieldValue;
16207
+ return item.value || '';
16208
+ };
16209
+ // Filter data based on input
16210
+ const filteredData = data.filter(item => {
16211
+ const displayValue = getDisplayValue(item);
16212
+ return displayValue.toLowerCase().includes(inputValue.toLowerCase());
16213
+ });
16214
+ // The dropdown shows the options or, when nothing matches, the `empty`
16215
+ // content. The listbox holds only options, so it's on screen only while
16216
+ // there are some.
16217
+ const menuOpen = isActive && (filteredData.length > 0 || !!empty);
16218
+ const listOpen = isActive && filteredData.length > 0;
16219
+ // Handle input change
16220
+ const handleInputChange = React.useCallback((e) => {
16221
+ const newValue = e.target.value;
16222
+ if (controlledValue === undefined) {
16223
+ setInternalValue(newValue);
16224
+ }
16225
+ onInput?.(newValue);
16226
+ setTyped(true);
16227
+ if (!isActive && newValue) {
16228
+ setIsActive(true);
16229
+ }
16230
+ }, [controlledValue, isActive, onInput]);
16231
+ // Handle item selection
16232
+ const handleSelect = React.useCallback((item) => {
16233
+ if (typeof item !== 'string' && item.disabled)
15184
16234
  return;
16235
+ const displayValue = getDisplayValue(item);
16236
+ if (controlledValue === undefined) {
16237
+ setInternalValue(displayValue);
16238
+ }
16239
+ onInput?.(displayValue);
16240
+ onSelect?.(item);
16241
+ if (!keepOpen) {
16242
+ setIsActive(false);
16243
+ }
16244
+ setHighlightedIndex(-1);
16245
+ }, [controlledValue, field, keepOpen, onInput, onSelect]);
16246
+ // Handle clear
16247
+ const handleClear = React.useCallback(() => {
16248
+ if (controlledValue === undefined) {
16249
+ setInternalValue('');
15185
16250
  }
15186
- const options = shownOptions();
15187
- const values = options.map(option => option.value);
15188
- const changed = !sameValues(values, shownValuesRef.current);
15189
- shownValuesRef.current = values;
15190
- setListShown(values.length > 0);
15191
- const landing = landingRef.current ?? (opened ? 'open' : null);
15192
- landingRef.current = null;
15193
- const enabled = options.filter(option => !option.disabled);
15194
- const checked = enabled.find(option => option.checked)?.value;
15195
- const first = enabled.length > 0 ? enabled[0].value : null;
15196
- const last = enabled.length > 0 ? enabled[enabled.length - 1].value : null;
15197
- let next = highlighted;
15198
- if (landing === 'first')
15199
- next = checked ?? first;
15200
- else if (landing === 'last')
15201
- next = checked ?? last;
15202
- else if (landing === 'open')
15203
- next = checked ?? (keepFirst ? first : null);
15204
- else if (landing === 'typed')
15205
- next = keepFirst ? first : null;
15206
- else if (landing === 'none')
15207
- next = null;
15208
- else if (keepFirst && changed)
15209
- next = first;
15210
- // Without `keepFirst`, which moved it above, a highlighted option
15211
- // that left takes the highlight with it.
15212
- else if (next !== null && !values.includes(next))
15213
- next = null;
15214
- // Only a change is set. StrictMode runs this twice on mount, and the
15215
- // second run, with the landing used up, works out the rendered highlight
15216
- // again, so it leaves the first run's landing in place.
15217
- if (next !== highlighted) {
15218
- scrollToHighlightRef.current = true;
15219
- setHighlighted(next);
16251
+ onInput?.('');
16252
+ onSelect?.(null);
16253
+ inputRef.current?.focus();
16254
+ }, [controlledValue, onInput, onSelect]);
16255
+ // Handle focus
16256
+ const handleFocus = React.useCallback(() => {
16257
+ if (openOnFocus && !disabled) {
16258
+ setIsActive(true);
16259
+ }
16260
+ }, [openOnFocus, disabled]);
16261
+ // Read when a click lands outside, so the listener stays put while the
16262
+ // list is open. What it selects changes as the user types and arrows
16263
+ // through suggestions, the filtered list is new on every render, and an
16264
+ // inline `onInput` or `onSelect` changes on every render of the parent.
16265
+ // Typing leaves the highlight where it was, so a list that narrowed can
16266
+ // leave it past the last suggestion, with nothing there to select.
16267
+ const selectHighlightedRef = useLatestRef(() => {
16268
+ if (selectOnClickOutside &&
16269
+ highlightedIndex >= 0 &&
16270
+ highlightedIndex < filteredData.length) {
16271
+ handleSelect(filteredData[highlightedIndex]);
15220
16272
  }
15221
16273
  });
15222
- // A browser scrolls to what has focus, not to an active descendant, so
15223
- // the highlighted option is brought into view here, when a key or a
15224
- // landing moved it there.
15225
- useClientLayoutEffect(() => {
15226
- if (highlighted === null || !scrollToHighlightRef.current)
15227
- return;
15228
- scrollToHighlightRef.current = false;
15229
- const option = shownOptions().find(shown => shown.value === highlighted);
15230
- option?.element.scrollIntoView?.({ block: 'nearest' });
15231
- }, [highlighted, shownOptions]);
15232
- // A press outside the field and the list closes it. The path is checked
15233
- // rather than the target, so it works in a shadow root.
16274
+ // Handle blur (click outside)
15234
16275
  React.useEffect(() => {
15235
- const root = rootRef.current;
15236
- if (!open || inline || !root)
16276
+ if (!isActive)
15237
16277
  return undefined;
15238
- const onPress = (event) => {
15239
- if (event === insidePressRef.current ||
15240
- isEventInside(event, rootRef.current) ||
15241
- isEventInside(event, popupRef.current)) {
16278
+ const handleClickOutside = (e) => {
16279
+ if (!containerRef.current)
15242
16280
  return;
16281
+ // Use composedPath() to correctly detect clicks inside Shadow DOM
16282
+ const path = e.composedPath();
16283
+ const isInside = path.includes(containerRef.current);
16284
+ if (!isInside) {
16285
+ selectHighlightedRef.current();
16286
+ setIsActive(false);
15243
16287
  }
15244
- close();
15245
16288
  };
15246
- // Its document, an iframe's when the tree renders into one.
15247
- const doc = root.ownerDocument;
15248
- doc.addEventListener('pointerdown', onPress);
15249
- return () => doc.removeEventListener('pointerdown', onPress);
15250
- }, [open, inline, close]);
15251
- const submit = React.useCallback((value) => {
15252
- onSelect?.(value);
15253
- if (!multiple)
15254
- close();
15255
- }, [onSelect, multiple, close]);
15256
- const onKeyDown = (event) => {
15257
- // The caller's handler ran first and can keep a key, and an IME that's
15258
- // composing owns every key.
15259
- if (event.defaultPrevented || isComposing$1(event))
15260
- return;
15261
- const { key, altKey } = event;
15262
- if (!open) {
15263
- if (key === 'ArrowDown' || (key === 'ArrowUp' && !altKey)) {
15264
- event.preventDefault();
15265
- requestOpen(true, altKey ? 'none' : key === 'ArrowDown' ? 'first' : 'last');
15266
- }
15267
- return;
16289
+ document.addEventListener('mousedown', handleClickOutside);
16290
+ return () => document.removeEventListener('mousedown', handleClickOutside);
16291
+ }, [isActive, selectHighlightedRef]);
16292
+ // Read when the dropdown opens or closes, so an inline `onActiveChange`,
16293
+ // new on every render of the parent, isn't told again about a state that
16294
+ // didn't change.
16295
+ const onActiveChangeRef = useLatestRef(onActiveChange);
16296
+ // Notify active state change
16297
+ React.useEffect(() => {
16298
+ onActiveChangeRef.current?.(isActive);
16299
+ }, [isActive, onActiveChangeRef]);
16300
+ // Keep first highlighted
16301
+ React.useEffect(() => {
16302
+ if (keepFirst && isActive && filteredData.length > 0) {
16303
+ setHighlightedIndex(0);
15268
16304
  }
15269
- switch (key) {
16305
+ }, [keepFirst, isActive, filteredData.length, inputValue]);
16306
+ // Handle keyboard navigation
16307
+ const handleKeyDown = React.useCallback((e) => {
16308
+ if (disabled || isComposing$1(e))
16309
+ return;
16310
+ switch (e.key) {
15270
16311
  case 'ArrowDown':
15271
- case 'ArrowUp': {
15272
- if (altKey) {
15273
- if (key === 'ArrowUp' && !inline) {
15274
- event.preventDefault();
15275
- close();
15276
- }
15277
- return;
16312
+ e.preventDefault();
16313
+ if (!isActive) {
16314
+ // A closed list opens on its first enabled option, as the ARIA
16315
+ // Authoring Practices' combobox does. Alt+Down Arrow opens it
16316
+ // without a highlight.
16317
+ setIsActive(true);
16318
+ setHighlightedIndex(e.altKey ? -1 : filteredData.map(isEnabled).indexOf(true));
15278
16319
  }
15279
- event.preventDefault();
15280
- // Disabled options too, so a screen reader user finds them.
15281
- const options = shownOptions();
15282
- if (options.length === 0)
15283
- return;
15284
- const at = options.findIndex(option => option.value === highlighted);
15285
- const step = key === 'ArrowDown' ? 1 : -1;
15286
- const index = at === -1
15287
- ? step === 1
15288
- ? 0
15289
- : options.length - 1
15290
- : (at + step + options.length) % options.length;
15291
- scrollToHighlightRef.current = true;
15292
- setHighlighted(options[index].value);
15293
- return;
15294
- }
15295
- case 'Enter': {
15296
- const option = shownOptions().find(shown => shown.value === highlighted);
15297
- if (option) {
15298
- event.preventDefault();
15299
- if (!option.disabled)
15300
- submit(option.value);
15301
- return;
16320
+ else {
16321
+ setHighlightedIndex(prev => prev < filteredData.length - 1 ? prev + 1 : prev);
15302
16322
  }
15303
- // Nothing to pick: the key goes on, so a form submits.
15304
- onSettle?.();
15305
- close();
15306
- return;
15307
- }
16323
+ break;
16324
+ case 'ArrowUp':
16325
+ e.preventDefault();
16326
+ if (!isActive) {
16327
+ // And Up Arrow opens it on its last enabled option.
16328
+ setIsActive(true);
16329
+ setHighlightedIndex(filteredData.map(isEnabled).lastIndexOf(true));
16330
+ }
16331
+ else {
16332
+ setHighlightedIndex(prev => (prev > 0 ? prev - 1 : prev));
16333
+ }
16334
+ break;
16335
+ case 'Enter':
16336
+ // With the list open on a highlighted option it can pick, Enter
16337
+ // is that option's. Otherwise it closes the list and goes on, so
16338
+ // a form around the field submits, as it would from any text
16339
+ // field.
16340
+ if (isActive &&
16341
+ highlightedIndex >= 0 &&
16342
+ filteredData[highlightedIndex] &&
16343
+ isEnabled(filteredData[highlightedIndex])) {
16344
+ e.preventDefault();
16345
+ handleSelect(filteredData[highlightedIndex]);
16346
+ }
16347
+ else {
16348
+ setIsActive(false);
16349
+ }
16350
+ break;
15308
16351
  case 'Escape':
15309
- if (inline)
15310
- return;
15311
- // It stops here only when there was something to close, so a
15312
- // Popover or Modal around it keeps its own Escape otherwise. Nothing
15313
- // stops it going on: a handler further out reads `defaultPrevented`.
15314
- if (popupRef.current)
15315
- event.preventDefault();
15316
- close();
15317
- return;
16352
+ // Only closing the dropdown claims the key. With it closed, the
16353
+ // key goes on to a Modal around the field.
16354
+ if (menuOpen)
16355
+ e.preventDefault();
16356
+ setIsActive(false);
16357
+ setHighlightedIndex(-1);
16358
+ break;
15318
16359
  case 'Tab':
15319
- close();
15320
- return;
15321
- case 'Home':
15322
- case 'End':
15323
- case 'ArrowLeft':
15324
- case 'ArrowRight':
15325
- // The field's own keys, which take the field back from the list.
15326
- setHighlighted(null);
15327
- return;
15328
- }
15329
- };
15330
- const onChange = () => {
15331
- setTyped(true);
15332
- if (open) {
15333
- landingRef.current = 'typed';
15334
- setHighlighted(null);
15335
- refresh();
15336
- }
15337
- else {
15338
- requestOpen(true, 'typed');
16360
+ if (isActive &&
16361
+ highlightedIndex >= 0 &&
16362
+ filteredData[highlightedIndex]) {
16363
+ handleSelect(filteredData[highlightedIndex]);
16364
+ }
16365
+ setIsActive(false);
16366
+ break;
15339
16367
  }
15340
- };
15341
- const onClick = () => {
15342
- requestOpen(true, 'open');
15343
- };
15344
- const onBlur = (event) => {
15345
- const next = event.relatedTarget;
15346
- if (next &&
15347
- (rootRef.current?.contains(next) || popupRef.current?.contains(next))) {
16368
+ }, [
16369
+ disabled,
16370
+ isActive,
16371
+ menuOpen,
16372
+ highlightedIndex,
16373
+ filteredData,
16374
+ handleSelect,
16375
+ ]);
16376
+ // Handle dropdown scroll for infinite scroll
16377
+ const handleDropdownScroll = React.useCallback(() => {
16378
+ if (!checkInfiniteScroll || !dropdownRef.current)
15348
16379
  return;
16380
+ const { scrollTop, scrollHeight, clientHeight } = dropdownRef.current;
16381
+ if (scrollHeight - scrollTop - clientHeight <= infiniteScrollDistance) {
16382
+ onInfiniteScroll?.();
15349
16383
  }
15350
- close();
15351
- };
15352
- const hover = (value) => {
15353
- scrollToHighlightRef.current = false;
15354
- setHighlighted(value);
15355
- };
15356
- const pick = (value) => {
15357
- submit(value);
15358
- // A press in the list keeps focus in the field, but a browser that
15359
- // ignores that, or a tap, can leave it elsewhere.
15360
- const input = inputRef.current;
15361
- if (input && input.isConnected && getActiveElementInTree(input) !== input) {
15362
- input.focus();
16384
+ }, [checkInfiniteScroll, infiniteScrollDistance, onInfiniteScroll]);
16385
+ // Scroll highlighted item into view
16386
+ React.useEffect(() => {
16387
+ if (highlightedIndex >= 0 && dropdownRef.current) {
16388
+ const highlightedEl = dropdownRef.current.querySelector(`[data-index="${highlightedIndex}"]`);
16389
+ if (highlightedEl &&
16390
+ typeof highlightedEl.scrollIntoView === 'function') {
16391
+ highlightedEl.scrollIntoView({ block: 'nearest' });
16392
+ }
15363
16393
  }
15364
- };
15365
- const keepFocus = (event) => event.preventDefault();
15366
- const markInside = (event) => {
15367
- insidePressRef.current = event.nativeEvent;
15368
- };
15369
- const registerPopup = React.useCallback((element) => {
15370
- popupRef.current = element;
15371
- return () => {
15372
- popupRef.current = null;
15373
- };
15374
- }, []);
15375
- const registerItem = React.useCallback((element, state) => {
15376
- const options = optionsRef.current;
15377
- options.set(element, state);
15378
- refresh();
15379
- return () => {
15380
- options.delete(element);
15381
- refresh();
15382
- };
15383
- }, []);
15384
- return {
15385
- open,
15386
- expanded,
15387
- highlighted,
15388
- typed,
15389
- listboxId,
15390
- itemId,
15391
- rootRef,
15392
- inputRef,
15393
- listboxRef,
15394
- registerPopup,
15395
- registerItem,
15396
- onKeyDown,
15397
- onChange,
15398
- onClick,
15399
- onBlur,
15400
- hover,
15401
- pick,
15402
- keepFocus,
15403
- markInside,
15404
- };
15405
- }
15406
-
15407
- /**
15408
- * Bulma Input component with full Bulma helper class support.
15409
- *
15410
- * @function
15411
- * @param {InputBaseProps} props - Props for the Input component.
15412
- * @returns {JSX.Element} The rendered input element.
15413
- * @see {@link https://bulma.io/documentation/form/input/ | Bulma Input documentation}
15414
- */
15415
- const InputBase = React.forwardRef(({ color, size, isRounded, isStatic, isHovered, isFocused, isLoading, className, disabled, readOnly, ...props }, ref) => {
15416
- const { bulmaHelperClasses, rest } = useBulmaClasses({
15417
- color,
15418
- ...props,
16394
+ }, [highlightedIndex]);
16395
+ // Combined ref
16396
+ const combinedRef = React.useCallback((node) => {
16397
+ inputRef.current =
16398
+ node;
16399
+ if (typeof ref === 'function') {
16400
+ ref(node);
16401
+ }
16402
+ else if (ref) {
16403
+ ref.current =
16404
+ node;
16405
+ }
16406
+ }, [ref]);
16407
+ // Generate classes
16408
+ const autocompleteClasses = usePrefixedClassNames('autocomplete', {
16409
+ 'is-active': isActive,
16410
+ [`is-${size}`]: !!size,
15419
16411
  });
15420
- // A labeled Field's generated id when no user id arrived (rest.id may be
15421
- // an undefined own key), reported to that Field (#1004).
15422
- const id = useFieldLabelTarget(rest.id);
15423
- const mainClass = usePrefixedClassNames('input', {
16412
+ const inputClasses = usePrefixedClassNames('input', {
15424
16413
  [`is-${color}`]: !!color,
15425
16414
  [`is-${size}`]: !!size,
15426
- 'is-rounded': isRounded,
15427
- 'is-static': isStatic,
15428
- 'is-hovered': isHovered,
15429
- 'is-focused': isFocused,
15430
- 'is-loading': isLoading,
15431
- });
15432
- const inputClass = classNames(mainClass, bulmaHelperClasses, className);
15433
- return (jsxRuntime.jsx("input", { ref: ref, className: inputClass, disabled: disabled, readOnly: readOnly, ...rest,
15434
- // After the spread, which may carry an undefined id key.
15435
- id: id }));
15436
- });
15437
- InputBase.displayName = 'InputBase';
15438
-
15439
- /**
15440
- * The `Input` component provides a Bulma-styled text input, supporting colors, sizes, rounded corners, static state, hover/focus/loading states, and all Bulma helper props.
15441
- *
15442
- * @function
15443
- * @param {InputProps} props - Props for Input.
15444
- * @returns {JSX.Element} The composed field element.
15445
- *
15446
- * @example
15447
- * <Input label="Username" placeholder="Enter username" iconLeftName="user" />
15448
- *
15449
- * @example
15450
- * <Input
15451
- * label="Email"
15452
- * type="email"
15453
- * message="Please enter a valid email"
15454
- * messageColor="danger"
15455
- * color="danger"
15456
- * />
15457
- */
15458
- const Input = React.forwardRef(({
15459
- // Field props
15460
- label, labelSize, labelProps, horizontal,
15461
- // Control props
15462
- iconLeft, iconRight, iconLeftName, iconRightName, iconLeftSize, iconRightSize, hasIconsLeft, hasIconsRight, isLoading, isExpanded, controlSize,
15463
- // Message props
15464
- message, messageColor,
15465
- // Container class overrides
15466
- fieldClassName, controlClassName,
15467
- // Everything else goes to Input
15468
- ...inputProps }, ref) => {
15469
- const insideField = useInsideField();
15470
- const insideControl = useInsideControl();
15471
- const ownField = rendersOwnField('Input', {
15472
- insideField,
15473
- insideControl,
15474
- label,
15475
- message,
15476
- horizontal,
15477
- fieldClassName,
15478
- });
15479
- const ownControl = rendersOwnControl('Input', {
15480
- insideControl,
15481
- isLoading,
15482
- iconLeft,
15483
- iconLeftName,
15484
- iconLeftSize,
15485
- iconRight,
15486
- iconRightName,
15487
- iconRightSize,
15488
- hasIconsLeft,
15489
- hasIconsRight,
15490
- isExpanded,
15491
- controlSize,
15492
- controlClassName,
16415
+ 'is-loading': loading,
15493
16416
  });
15494
- const { controlId, fieldLabelProps } = useAutoLabelId({
15495
- label,
15496
- id: inputProps.id,
15497
- labelProps,
15498
- rendersLabel: ownField,
16417
+ const controlClasses = usePrefixedClassNames('control', 'is-expanded', 'has-icons-right');
16418
+ const dropdownMenuClasses = usePrefixedClassNames('dropdown-menu', {
16419
+ 'is-active': menuOpen,
15499
16420
  });
16421
+ const dropdownContentClass = usePrefixedClassNames('dropdown-content');
16422
+ const dropdownHeaderClass = usePrefixedClassNames('dropdown-header');
16423
+ const dropdownFooterClass = usePrefixedClassNames('dropdown-footer');
16424
+ const emptyItemClasses = usePrefixedClassNames('dropdown-item', 'has-text-grey');
16425
+ const iconRightClickableClass = usePrefixedClassNames('icon', 'is-right', 'is-clickable');
16426
+ const iconRightClass = usePrefixedClassNames('icon', 'is-right');
16427
+ const loaderClass = usePrefixedClassNames('loader', 'is-loading');
16428
+ const combinedClasses = classNames(autocompleteClasses, bulmaHelperClasses, className);
15500
16429
  const helpClass = usePrefixedClassNames('help', {
15501
16430
  [`is-${messageColor}`]: !!messageColor,
15502
16431
  });
15503
- let content = jsxRuntime.jsx(InputBase, { ref: ref, ...inputProps, id: controlId });
15504
- if (ownControl) {
15505
- content = (jsxRuntime.jsx(Control, { iconLeft: iconLeft, iconRight: iconRight, iconLeftName: iconLeftName, iconRightName: iconRightName, iconLeftSize: iconLeftSize, iconRightSize: iconRightSize, hasIconsLeft: hasIconsLeft, hasIconsRight: hasIconsRight, isLoading: isLoading, isExpanded: isExpanded, size: controlSize, className: controlClassName, children: content }));
15506
- }
15507
16432
  const messageEl = message ? jsxRuntime.jsx("p", { className: helpClass, children: message }) : null;
15508
- if (ownField) {
15509
- return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
16433
+ // The clear button gives way to a spinner at its spot: the `loading` one,
16434
+ // or that of a loading Control this sits in.
16435
+ const controlLoading = useControlLoading();
16436
+ const showClear = clearable && !!inputValue && !disabled && !loading && !controlLoading;
16437
+ // The combobox controls its listbox and points at the highlighted option.
16438
+ // The listbox takes its name from the label that names the combobox, the
16439
+ // one this renders or a surrounding Field's, so the two say the same
16440
+ // thing. When no label names the combobox, or the one that does has no
16441
+ // id to point at, the list keeps a fallback name. While the dropdown
16442
+ // shows only the `empty` content there's no listbox, so the combobox is
16443
+ // collapsed and points at nothing.
16444
+ const activeOptionId = listOpen &&
16445
+ highlightedIndex >= 0 &&
16446
+ highlightedIndex < filteredData.length
16447
+ ? `${listboxId}-${highlightedIndex}`
16448
+ : undefined;
16449
+ // A polite status region, in the page from the start so it's there
16450
+ // before anything is written into it, announces the `empty` content
16451
+ // when the user's typing leaves nothing that matches what's in the
16452
+ // field. It follows what's on screen, so results that come in after the
16453
+ // typing count too. It's said once each time that happens, not again on
16454
+ // each keystroke while nothing matches. An empty field isn't a search,
16455
+ // so it stays quiet then, even with no data. While `loading`, or a
16456
+ // loading Control around it, says results are on their way, it waits,
16457
+ // and it's said once loading ends with nothing matching.
16458
+ const emptyItems = typed &&
16459
+ menuOpen &&
16460
+ !listOpen &&
16461
+ !!inputValue &&
16462
+ !loading &&
16463
+ !controlLoading
16464
+ ? typedNoMatches$1
16465
+ : nothingToAnnounce$1;
16466
+ // The region reads the text on screen, whatever `empty` renders.
16467
+ const emptyRef = React.useRef(null);
16468
+ const describeEmpty = React.useCallback(() => {
16469
+ const node = emptyRef.current;
16470
+ /* istanbul ignore if: it's only announced while it's on screen */
16471
+ if (!node)
16472
+ return null;
16473
+ return spokenText(node);
16474
+ }, []);
16475
+ const autocompleteElement = (jsxRuntime.jsxs("div", { ref: containerRef, className: combinedClasses, ...rest, children: [jsxRuntime.jsxs("div", { className: controlClasses, children: [jsxRuntime.jsx("input", { ref: combinedRef, type: "text", className: inputClasses, id: controlId, value: inputValue, placeholder: placeholder, disabled: disabled, name: name, form: form, required: required, onChange: handleInputChange, onFocus: handleFocus, onKeyDown: handleKeyDown, role: "combobox", "aria-expanded": listOpen, "aria-controls": listOpen ? listboxId : undefined, "aria-activedescendant": activeOptionId, "aria-haspopup": "listbox", "aria-autocomplete": "list", autoComplete: "off" }), showClear && (jsxRuntime.jsx("button", { type: "button", className: iconRightClickableClass, onClick: handleClear, "aria-label": "Clear", children: jsxRuntime.jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", width: "16", height: "16", "aria-hidden": "true", children: [jsxRuntime.jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }), jsxRuntime.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })] }) })), loading && (jsxRuntime.jsx("span", { className: iconRightClass, children: jsxRuntime.jsx("span", { className: loaderClass }) }))] }), menuOpen && (jsxRuntime.jsx("div", { className: dropdownMenuClasses, children: jsxRuntime.jsxs("div", { ref: dropdownRef, className: dropdownContentClass, style: { maxHeight: `${maxHeight}px`, overflowY: 'auto' }, onScroll: handleDropdownScroll, children: [header && jsxRuntime.jsx("div", { className: dropdownHeaderClass, children: header }), listOpen ? (jsxRuntime.jsx("div", { id: listboxId, role: "listbox", "aria-labelledby": listLabelId, "aria-label": listLabelId ? undefined : 'Suggestions', onMouseDown: keepFocus, children: filteredData.map((item, index) => {
16476
+ const isDisabled = typeof item !== 'string' && item.disabled;
16477
+ const isHighlighted = index === highlightedIndex;
16478
+ const itemClasses = prefixedClassNames(classPrefix, 'dropdown-item', {
16479
+ 'is-active': isHighlighted,
16480
+ 'is-disabled': isDisabled,
16481
+ });
16482
+ return (jsxRuntime.jsx("a", { id: `${listboxId}-${index}`, "data-index": index, className: itemClasses, onClick: () => !isDisabled && handleSelect(item), onMouseEnter: () => !isDisabled && setHighlightedIndex(index), role: "option", "aria-selected": isHighlighted && !isDisabled, "aria-disabled": isDisabled, children: itemTemplate
16483
+ ? itemTemplate(item)
16484
+ : getDisplayValue(item) }, index));
16485
+ }) })) : (jsxRuntime.jsx("div", { ref: emptyRef, className: emptyItemClasses, onMouseDown: keepFocus, children: empty })), footer && jsxRuntime.jsx("div", { className: dropdownFooterClass, children: footer })] }) })), jsxRuntime.jsx(StatusRegion, { items: emptyItems, describe: describeEmpty })] }));
16486
+ if (!insideField) {
16487
+ return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [autocompleteElement, messageEl] }));
15510
16488
  }
15511
- return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [content, messageEl] }));
16489
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [autocompleteElement, messageEl] }));
15512
16490
  });
15513
- Input.displayName = 'Input';
16491
+ Autocomplete.displayName = 'Autocomplete';
15514
16492
 
15515
16493
  const validComboboxSizes = ['small', 'medium', 'large'];
15516
16494
  // What the status region announces: the `Combobox.Empty` content, as the one
@@ -23338,6 +24316,7 @@ exports.Carousel = Carousel;
23338
24316
  exports.CarouselItem = CarouselItem;
23339
24317
  exports.Cell = Cell;
23340
24318
  exports.Checkbox = Checkbox;
24319
+ exports.CheckboxCard = CheckboxCard;
23341
24320
  exports.Checkboxes = Checkboxes;
23342
24321
  exports.ClientOnly = ClientOnly;
23343
24322
  exports.Code = Code;
@@ -23352,6 +24331,13 @@ exports.ComboboxInput = ComboboxInput;
23352
24331
  exports.ComboboxItem = ComboboxItem;
23353
24332
  exports.ComboboxList = ComboboxList;
23354
24333
  exports.ComboboxMenu = ComboboxMenu;
24334
+ exports.Command = Command;
24335
+ exports.CommandDialog = CommandDialog;
24336
+ exports.CommandEmpty = CommandEmpty;
24337
+ exports.CommandGroup = CommandGroup;
24338
+ exports.CommandInput = CommandInput;
24339
+ exports.CommandItem = CommandItem;
24340
+ exports.CommandList = CommandList;
23355
24341
  exports.ConfigProvider = ConfigProvider;
23356
24342
  exports.Container = Container;
23357
24343
  exports.Content = Content;
@@ -23364,6 +24350,10 @@ exports.DateRangeInputBase = DateRangeInputBase;
23364
24350
  exports.DateTimeInput = DateTimeInput;
23365
24351
  exports.DateTimeInputBase = DateTimeInputBase;
23366
24352
  exports.Delete = Delete;
24353
+ exports.DescriptionList = DescriptionList;
24354
+ exports.DescriptionListItem = DescriptionListItem;
24355
+ exports.DescriptionListLabel = DescriptionListLabel;
24356
+ exports.DescriptionListValue = DescriptionListValue;
23367
24357
  exports.Dialog = Dialog;
23368
24358
  exports.DialogContainer = DialogContainer;
23369
24359
  exports.Divider = Divider;
@@ -23456,6 +24446,7 @@ exports.Portal = Portal;
23456
24446
  exports.Pre = Pre;
23457
24447
  exports.Progress = Progress;
23458
24448
  exports.Radio = Radio;
24449
+ exports.RadioCard = RadioCard;
23459
24450
  exports.Radios = Radios;
23460
24451
  exports.Rate = Rate;
23461
24452
  exports.Reveal = Reveal;