@allxsmith/bestax-bulma 5.51.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 (55) 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/form/Autocomplete.js +2 -2
  6. package/dist/esm/form/CheckboxCard.js +118 -0
  7. package/dist/esm/form/CheckboxCard.js.map +1 -0
  8. package/dist/esm/form/Checkboxes.js +14 -3
  9. package/dist/esm/form/Checkboxes.js.map +1 -1
  10. package/dist/esm/form/RadioCard.js +85 -0
  11. package/dist/esm/form/RadioCard.js.map +1 -0
  12. package/dist/esm/form/Radios.js +14 -3
  13. package/dist/esm/form/Radios.js.map +1 -1
  14. package/dist/esm/form/groupFieldset.js +38 -0
  15. package/dist/esm/form/groupFieldset.js.map +1 -0
  16. package/dist/esm/form/useAutoLabelId.js +1 -1
  17. package/dist/esm/helpers/useComboboxEngine.js +3 -3
  18. package/dist/esm/helpers/useFocusTrap.js +2 -2
  19. package/dist/esm/helpers/useFocusTrap.js.map +1 -1
  20. package/dist/extras.css +1 -1
  21. package/dist/extras.css.map +1 -1
  22. package/dist/index.cjs +2429 -1566
  23. package/dist/index.cjs.map +1 -1
  24. package/dist/index.esm.js +3 -0
  25. package/dist/index.esm.js.map +1 -1
  26. package/dist/types/components/Command.d.ts +318 -0
  27. package/dist/types/form/CheckboxCard.d.ts +94 -0
  28. package/dist/types/form/Checkboxes.d.ts +13 -0
  29. package/dist/types/form/RadioCard.d.ts +81 -0
  30. package/dist/types/form/Radios.d.ts +13 -0
  31. package/dist/types/form/groupFieldset.d.ts +32 -0
  32. package/dist/types/index.d.ts +3 -0
  33. package/dist/types-cjs/components/Command.d.ts +318 -0
  34. package/dist/types-cjs/form/CheckboxCard.d.ts +94 -0
  35. package/dist/types-cjs/form/Checkboxes.d.ts +13 -0
  36. package/dist/types-cjs/form/RadioCard.d.ts +81 -0
  37. package/dist/types-cjs/form/Radios.d.ts +13 -0
  38. package/dist/types-cjs/form/groupFieldset.d.ts +32 -0
  39. package/dist/types-cjs/index.d.ts +3 -0
  40. package/dist/versions/bestax-no-dark-mode.css +1 -1
  41. package/dist/versions/bestax-no-dark-mode.css.map +1 -1
  42. package/dist/versions/bestax-no-helpers-prefixed.css +1 -1
  43. package/dist/versions/bestax-no-helpers-prefixed.css.map +1 -1
  44. package/dist/versions/bestax-no-helpers.css +1 -1
  45. package/dist/versions/bestax-no-helpers.css.map +1 -1
  46. package/dist/versions/bestax-prefixed.css +1 -1
  47. package/dist/versions/bestax-prefixed.css.map +1 -1
  48. package/package.json +1 -1
  49. package/src/scss/components/_command.scss +259 -0
  50. package/src/scss/components/_index.scss +1 -0
  51. package/src/scss/form/_checkbox.scss +44 -0
  52. package/src/scss/form/_checkboxcard.scss +355 -0
  53. package/src/scss/form/_index.scss +2 -0
  54. package/src/scss/form/_radio.scss +44 -0
  55. package/src/scss/form/_radiocard.scss +291 -0
package/dist/index.cjs CHANGED
@@ -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' &&
@@ -11425,6 +11425,1482 @@ 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]);
11432
+ /**
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.
11438
+ *
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`.
11444
+ *
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.
11448
+ */
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
+ }
11566
+ });
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
+
11428
12904
  /**
11429
12905
  * The `Block` component renders a simple container with Bulma's `.block` class, adding vertical margin between sections of content.
11430
12906
  *
@@ -11739,64 +13215,20 @@ const FigureComponent = ({ className, textColor, bgColor, children, ...props })
11739
13215
  });
11740
13216
  const bulmaClasses = usePrefixedClassNames();
11741
13217
  const figureClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
11742
- return (jsxRuntime.jsx("figure", { className: figureClasses || undefined, ...rest, children: children }));
11743
- };
11744
- /**
11745
- * Figure component with Caption subcomponent.
11746
- *
11747
- * @example
11748
- * <Figure>
11749
- * <img src="image.jpg" alt="Description" />
11750
- * <Figure.Caption>Image caption text</Figure.Caption>
11751
- * </Figure>
11752
- */
11753
- const Figure = withSubComponents(FigureComponent, {
11754
- Caption: FigureCaption,
11755
- }, 'Figure');
11756
-
11757
- /**
11758
- * Distinguishes an `IconProps` object from a plain custom node (an inline SVG, a
11759
- * `react-icons` component, …) in a slot that accepts either — `Control`'s
11760
- * `iconLeft`/`iconRight`, `IconText`'s `iconProps`.
11761
- *
11762
- * One copy, imported by both consumers: two hand-maintained copies, one per
11763
- * consumer, is how the deprecated `icon` path came to crash both (#663).
11764
- *
11765
- * Its own module rather than `Icon.tsx` because `src/index.ts` wildcard-exports
11766
- * that file, and everything published there is public forever. `polymorphic.ts`
11767
- * makes the same call for `isCustomElement`: an internal discriminator is not
11768
- * something to support for the life of the package.
11769
- *
11770
- * It tests each member's VALUE, not merely its key, and only after everything
11771
- * React owns is out of the way. `'icon' in value` was enough
11772
- * to claim a Font Awesome `IconDefinition` — `{ prefix, iconName, icon: [w, h, …,
11773
- * path] }` — whose `icon` is a path array rather than a class string. That object
11774
- * is not a renderable node either, so the honest outcome is the one this slot has
11775
- * always had for it: the node branch, and React's own "Objects are not valid as a
11776
- * React child". Claiming it here instead rendered a blank container that still
11777
- * reserved layout.
11778
- *
11779
- * A member added to `IconProps` needs a line here, and a missed one is a crash
11780
- * rather than a fallthrough. That is the cost of a slot taking either a props
11781
- * object or a node; keeping the guard in one place is what keeps the cost to a
11782
- * single line.
11783
- */
11784
- function isIconProps(value) {
11785
- if (typeof value !== 'object' || value === null)
11786
- return false;
11787
- // Anything React owns. `isValidElement` covers elements only; a PORTAL is a
11788
- // valid `ReactNode` that fails it and carries a `children` key, so the member
11789
- // test below claimed it and `Control` spread it into `<Icon>` as props —
11790
- // rendering the portal's child locally instead of in its container. Every
11791
- // React internal carries `$$typeof`, so testing for it covers lazy, context
11792
- // and whatever React adds next, rather than portals alone.
11793
- if (React.isValidElement(value) || '$$typeof' in value)
11794
- return false;
11795
- const candidate = value;
11796
- return (typeof candidate.name === 'string' ||
11797
- typeof candidate.icon === 'string' ||
11798
- 'children' in candidate);
11799
- }
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');
11800
13232
 
11801
13233
  /**
11802
13234
  * The `IconText` component provides a Bulma-styled horizontal arrangement of one or more `Icon` components and optional text.
@@ -12881,392 +14313,150 @@ const UnorderedListComponent = ({ className, textColor, bgColor, children, ...pr
12881
14313
  backgroundColor: bgColor,
12882
14314
  ...props,
12883
14315
  });
12884
- const bulmaClasses = usePrefixedClassNames();
12885
- const listClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
12886
- return (jsxRuntime.jsx("ul", { className: listClasses || undefined, ...rest, children: children }));
12887
- };
12888
- const UnorderedList = withSubComponents(UnorderedListComponent, { Item: ListItem }, 'UnorderedList');
12889
-
12890
- // Whether the nearest enclosing `Control` shows its `isLoading` spinner. The
12891
- // picker bases read it so their right-side launcher gives way to that
12892
- // spinner, which sits at the same edge, whether the `Control` is the one a
12893
- // convenience input renders or one the caller wrapped around the input.
12894
- // Autocomplete reads it for its clear button, which sits there too.
12895
- // Kept apart from `FormContext`, whose contexts only report whether a `Field`
12896
- // or `Control` is present. Internal; not part of the public API.
12897
- const ControlLoadingContext = React.createContext(false);
12898
- /** Provider for the loading state, used internally by `Control`. */
12899
- const ControlLoadingProvider = ControlLoadingContext.Provider;
12900
- /** Whether the nearest enclosing `Control` is loading; `false` outside one. */
12901
- const useControlLoading = () => React.useContext(ControlLoadingContext);
12902
-
12903
- const allowedColors = [...validColors, 'inherit', 'current'];
12904
- /**
12905
- * 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.
12906
- *
12907
- * @function
12908
- * @param {ControlProps} props - Props for the Control component.
12909
- * @returns {JSX.Element} The rendered control container.
12910
- * @see {@link https://bulma.io/documentation/form/general/#control | Bulma Control documentation}
12911
- *
12912
- * @example
12913
- * // Control with left icon
12914
- * <Control iconLeftName="envelope" iconLeftSize="small">
12915
- * <input className="input" type="email" placeholder="Email" />
12916
- * </Control>
12917
- */
12918
- const Control = React.forwardRef(({ as = 'div', hasIconsLeft, hasIconsRight, isLoading, isExpanded, size, textColor, bgColor, iconLeft, iconRight, iconLeftName, iconLeftSize, iconRightName, iconRightSize, className, children, ...props }, ref) => {
12919
- const Component = (as === 'p' ? 'p' : 'div');
12920
- const { classPrefix } = useConfig();
12921
- // Remove textColor/bgColor from props before spreading
12922
- const { textColor: _ignoredTextColor, bgColor: _ignoredBgColor, ...restProps } = props;
12923
- const safeTextColor = allowedColors.includes(textColor)
12924
- ? textColor
12925
- : undefined;
12926
- const safeBgColor = allowedColors.includes(bgColor)
12927
- ? bgColor
12928
- : undefined;
12929
- const { bulmaHelperClasses, rest } = useBulmaClasses({
12930
- color: safeTextColor,
12931
- backgroundColor: safeBgColor,
12932
- ...restProps,
12933
- });
12934
- // Prepare icon props for the shortcut. A truthiness test (not `!== undefined`)
12935
- // so a falsy node from the idiomatic `iconLeft={cond && <Node/>}` / `iconLeft={maybe ?? null}`
12936
- // pattern counts as "no icon" — otherwise `false`/`null` would reserve the icon column and
12937
- // mount an empty `.icon` span. A falsy `iconLeft` also correctly falls through to the
12938
- // `iconLeftName` shortcut, matching the pre-node behavior.
12939
- const leftIconValue = iconLeft ||
12940
- (iconLeftName
12941
- ? {
12942
- name: iconLeftName,
12943
- size: iconLeftSize,
12944
- }
12945
- : undefined);
12946
- const rightIconValue = iconRight ||
12947
- (iconRightName
12948
- ? {
12949
- name: iconRightName,
12950
- size: iconRightSize,
12951
- }
12952
- : undefined);
12953
- const mainClass = usePrefixedClassNames('control', {
12954
- 'has-icons-left': hasIconsLeft || !!leftIconValue,
12955
- 'has-icons-right': hasIconsRight || !!rightIconValue,
12956
- 'is-loading': isLoading,
12957
- 'is-expanded': isExpanded,
12958
- [`is-${size}`]: !!size,
12959
- });
12960
- const controlClass = classNames(mainClass, bulmaHelperClasses, className);
12961
- // --- FIX: Spread both restProps (for data-testid, etc) AND rest (from useBulmaClasses) ---
12962
- 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 &&
12963
- (isIconProps(leftIconValue) ? (jsxRuntime.jsx(Icon, { ...leftIconValue, className: prefixedClassNames(classPrefix, 'is-left') })) : (jsxRuntime.jsx(Icon, { className: prefixedClassNames(classPrefix, 'is-left'), children: leftIconValue }))), rightIconValue &&
12964
- (isIconProps(rightIconValue) ? (jsxRuntime.jsx(Icon, { ...rightIconValue, className: prefixedClassNames(classPrefix, 'is-right') })) : (jsxRuntime.jsx(Icon, { className: prefixedClassNames(classPrefix, 'is-right'), children: rightIconValue })))] }) }));
12965
- });
12966
- Control.displayName = 'Control';
12967
-
12968
- /**
12969
- * FieldLabel component for rendering a Bulma field label. It renders the
12970
- * label column of a horizontal `Field` (a `div` with the `field-label` class),
12971
- * not a `<label>`, so text placed straight in it names nothing. Put a `<label>`
12972
- * with the `label` class inside it and point its `htmlFor` at the control's
12973
- * `id`, or give the horizontal `Field` a `label` prop, which renders this
12974
- * column and its `<label>` for you. A `<label>` you put here names the
12975
- * control through its `htmlFor` alone: a range `Slider`'s thumbs and an
12976
- * `Autocomplete`'s, a `Combobox`'s or a `Taginput`'s list point
12977
- * `aria-labelledby` at a `Field`'s label only when its `label` prop renders it
12978
- * and it names the control. Where it would not name the control on its own, as in an inner `Field`,
12979
- * pass `labelProps={{ htmlFor, id }}` with a matching `id` on the control.
12980
- *
12981
- * @function
12982
- * @param {FieldLabelProps} props - Props for the FieldLabel component.
12983
- * @returns {JSX.Element} The rendered field label.
12984
- *
12985
- * @example
12986
- * <FieldLabel size="normal">
12987
- * <label className="label" htmlFor="name">Name</label>
12988
- * </FieldLabel>
12989
- */
12990
- const FieldLabel = ({ size, textColor, bgColor, className, children, ...props }) => {
12991
- const { bulmaHelperClasses, rest } = useBulmaClasses({
12992
- color: textColor,
12993
- backgroundColor: bgColor,
12994
- ...props,
12995
- });
12996
- const mainClass = usePrefixedClassNames('field-label', {
12997
- [`is-${size}`]: !!size,
12998
- });
12999
- const fieldLabelClass = classNames(mainClass, bulmaHelperClasses, className);
13000
- // Spread ...props and ...rest so custom props like data-testid are included
13001
- return (jsxRuntime.jsx("div", { className: fieldLabelClass, ...props, ...rest, children: children }));
13002
- };
13003
- /**
13004
- * FieldBody component for rendering Bulma field body.
13005
- *
13006
- * @function
13007
- * @param {FieldBodyProps} props - Props for the FieldBody component.
13008
- * @returns {JSX.Element} The rendered field body.
13009
- *
13010
- * @example
13011
- * <FieldBody><input className="input" /></FieldBody>
13012
- */
13013
- const FieldBody = ({ textColor, bgColor, className, children, ...props }) => {
13014
- const { bulmaHelperClasses, rest } = useBulmaClasses({
13015
- color: textColor,
13016
- backgroundColor: bgColor,
13017
- ...props,
13018
- });
13019
- const mainClass = usePrefixedClassNames('field-body');
13020
- const fieldBodyClass = classNames(mainClass, bulmaHelperClasses, className);
13021
- // Spread ...props and ...rest so custom props like data-testid are included
13022
- return (jsxRuntime.jsx("div", { className: fieldBodyClass, ...props, ...rest, children: children }));
13023
- };
13024
- /**
13025
- * The `Field` component is a Bulma-styled form field container.
13026
- *
13027
- * @function
13028
- * @param {FieldProps} props - Props for the Field component.
13029
- * @returns {JSX.Element} The rendered field container.
13030
- * @see {@link https://bulma.io/documentation/form/general/#field | Bulma Field documentation}
13031
- *
13032
- * @example
13033
- * // Labelled field — the label auto-associates with the composed base
13034
- * <Field label="Email">
13035
- * <Control>
13036
- * <InputBase type="email" />
13037
- * </Control>
13038
- * </Field>
13039
- *
13040
- * @example
13041
- * // Horizontal field
13042
- * <Field horizontal label="Name">
13043
- * <Control>
13044
- * <InputBase />
13045
- * </Control>
13046
- * </Field>
13047
- */
13048
- const FieldComponent = ({ horizontal, grouped, hasAddons, narrow, label, labelSize, labelProps, textColor, color: _fieldColor, bgColor, className, children, ...props }) => {
13049
- const { bulmaHelperClasses, rest } = useBulmaClasses({
13050
- color: textColor,
13051
- backgroundColor: bgColor,
13052
- ...props,
13053
- });
13054
- const mainClass = usePrefixedClassNames('field', {
13055
- 'is-horizontal': horizontal,
13056
- 'has-addons': !!hasAddons,
13057
- 'has-addons-centered': hasAddons === 'centered',
13058
- 'has-addons-right': hasAddons === 'right',
13059
- 'is-narrow': narrow,
13060
- 'is-grouped': grouped === true ||
13061
- grouped === 'centered' ||
13062
- grouped === 'right' ||
13063
- grouped === 'multiline',
13064
- 'is-grouped-centered': grouped === 'centered',
13065
- 'is-grouped-right': grouped === 'right',
13066
- 'is-grouped-multiline': grouped === 'multiline',
13067
- });
13068
- const fieldClass = classNames(mainClass, bulmaHelperClasses, className);
13069
- // Default labelSize to 'normal' when horizontal for proper baseline alignment
13070
- const effectiveLabelSize = labelSize ?? (horizontal ? 'normal' : undefined);
13071
- const labelClass = usePrefixedClassNames('label');
13072
- // Auto-associate the label with a single composed base control (#495): the
13073
- // label points at a generated id shared via context, which a composed base
13074
- // adopts when the user supplied no id of their own.
13075
- // Presence semantics on htmlFor — even an explicit `htmlFor: undefined`
13076
- // means the caller owns the association. Grouped/addons fields hold several
13077
- // controls, so no single association is generated for them.
13078
- const generatedId = React.useId();
13079
- const userWiredLabel = !!labelProps && 'htmlFor' in labelProps;
13080
- const targetId = label && !userWiredLabel && !grouped && !hasAddons
13081
- ? generatedId
13082
- : undefined;
13083
- // A group control (Radios, Checkboxes, Rate, DateRangeInput and its base)
13084
- // cannot take that `for`, so it points `aria-labelledby` at the label
13085
- // itself (#939).
13086
- // The label gets an id whenever the association is on, unless the caller
13087
- // gave it one.
13088
- const labelId = labelProps?.id ?? (targetId ? `${targetId}-label` : undefined);
13089
- // What the rendered label's `for` points at, generated or the caller's, and
13090
- // the id it renders with, so a control can tell when a label wired by hand
13091
- // names it and point at that label. An unlabeled Field passes on its
13092
- // parent's. Unlike `targetId`, a label wired by hand passes on its own in
13093
- // `grouped`/`hasAddons` Fields too, on purpose: those only stop the Field
13094
- // picking one control out of several, and the caller's `for` has picked
13095
- // one, so a range Slider or Autocomplete it names points at that label in
13096
- // a row as well. A group reads the label element id below instead, which
13097
- // stays off for a label wired by hand, since no group takes its `for`.
13098
- const inheritedLabelFor = useFieldLabelFor();
13099
- const labelForTarget = userWiredLabel ? labelProps.htmlFor : targetId;
13100
- const ownLabelFor = React.useMemo(() => ({ htmlFor: labelForTarget, id: labelId }), [labelForTarget, labelId]);
13101
- const labelFor = label ? ownLabelFor : inheritedLabelFor;
13102
- // Whether the label keeps its generated `for` (#1004). Content that takes
13103
- // the Field's id says so from a layout effect, and this Field's own layout
13104
- // effect runs after its content's in the same commit, so by then every
13105
- // report has landed. Until it runs, on the server and in the first render
13106
- // while hydrating, the label writes the `for` as it always has. From then
13107
- // on it keeps the `for` only while some content holds the id, and content
13108
- // that mounts or changes later reports when it does. Counted in a ref, and
13109
- // state is set only once settled, so content that takes the id renders
13110
- // the Field just once.
13111
- const takersRef = React.useRef(0);
13112
- const settledRef = React.useRef(false);
13113
- const [forTaken, setForTaken] = React.useState(true);
13114
- const settle = React.useCallback(() => {
13115
- if (settledRef.current)
13116
- setForTaken(takersRef.current > 0);
13117
- }, []);
13118
- const reportLabelFor = React.useCallback(() => {
13119
- takersRef.current += 1;
13120
- settle();
13121
- return () => {
13122
- takersRef.current -= 1;
13123
- settle();
13124
- };
13125
- }, [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);
13126
14371
  useClientLayoutEffect(() => {
13127
- settledRef.current = true;
13128
- if (targetId)
13129
- settle();
13130
- }, [targetId, settle]);
13131
- const renderedTargetId = forTaken ? targetId : undefined;
13132
- let renderedLabel = null;
13133
- if (label) {
13134
- if (horizontal) {
13135
- 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 }) }));
13136
- }
13137
- else {
13138
- 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;
13139
14375
  }
13140
- }
13141
- // If horizontal, wrap children in FieldBody (unless the user already provided
13142
- // a FieldBody — either as the single child, or as one element among siblings
13143
- // like <Field.Label/> + <Field.Body/>).
13144
- let content = children;
13145
- if (horizontal) {
13146
- const isFieldBody = (c) => React.isValidElement(c) &&
13147
- // @ts-expect-error displayName isn't on the public type
13148
- (c.type === FieldBody || c.type?.displayName === 'FieldBody');
13149
- const isFieldLabel = (c) => React.isValidElement(c) &&
13150
- // @ts-expect-error displayName isn't on the public type
13151
- (c.type === FieldLabel || c.type?.displayName === 'FieldLabel');
13152
- const childArray = React.Children.toArray(children);
13153
- const userProvidedStructure = childArray.some(c => isFieldBody(c) || isFieldLabel(c));
13154
- if (userProvidedStructure) {
13155
- content = children;
14376
+ else if (ownsIndeterminateRef.current) {
14377
+ ownsIndeterminateRef.current = false;
14378
+ inputRef.current.indeterminate = false;
13156
14379
  }
13157
- else {
13158
- 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));
13159
14405
  }
13160
- }
13161
- 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] }) }) }) }) }) }));
13162
- };
13163
- FieldLabel.displayName = 'FieldLabel';
13164
- FieldBody.displayName = 'FieldBody';
13165
- const Field = withSubComponents(FieldComponent, {
13166
- Label: FieldLabel,
13167
- Body: FieldBody,
13168
- Control,
13169
- }, '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';
13170
14429
 
13171
14430
  /**
13172
- * Associates the convenience `label` prop with its control (#368): generates
13173
- * an id for the control and returns labelProps carrying a matching `htmlFor`.
13174
- * A user-supplied `id` is used as the target instead of the generated one, and
13175
- * an explicit `htmlFor` key in labelProps — even set to `undefined` — disables
13176
- * generation entirely: the user has taken over the association (#495 presence
13177
- * semantics). When the control renders no label of its own, it adopts the id
13178
- * a labeled Field around it offers, the way the bases do, so that Field's
13179
- * label names it (#939); a user `id` still wins there. It tells that Field
13180
- * when the control took its id, since the Field's label drops a `for`
13181
- * nothing took (#1004). With `needsLabelId` it also returns the id of
13182
- * whichever label names the control (#981).
13183
- * Internal; not part of the public API.
13184
- */
13185
- function useAutoLabelId({ label, id, labelProps, rendersLabel, hasInput = true, needsLabelId = false, }) {
13186
- // Called unconditionally per the rules of hooks; SSR-safe on React 18 and 19.
13187
- const generatedId = React.useId();
13188
- // Read outside the control's own Field, so it is set only when an outer
13189
- // Field holds the control, which then renders no label of its own.
13190
- const fieldLabelId = useFieldLabelId();
13191
- // What an outer Field's label points at, even when wired by hand, and the
13192
- // id that label renders with, read together so the id is that label's.
13193
- // Set in `grouped`/`hasAddons` Fields too when the label is wired by hand,
13194
- // since its `for` names this one control whatever the layout (see Field).
13195
- const fieldLabelFor = useFieldLabelFor();
13196
- // Truthiness mirrors Field's own `if (label)` render gate.
13197
- const active = !!label && rendersLabel;
13198
- // Presence, not truthiness: `htmlFor: undefined` is an explicit opt-out and
13199
- // must not leave an orphan generated id on the control.
13200
- const userWired = !!labelProps && 'htmlFor' in labelProps;
13201
- const adopted = !active && hasInput ? fieldLabelId : undefined;
13202
- const controlId = id ?? (active && !userWired ? generatedId : adopted);
13203
- // Tells a labeled Field around the control when its label's `for` names
13204
- // the control, since it drops a `for` nothing took (#1004).
13205
- useReportFieldLabelFor(fieldLabelId !== undefined && controlId === fieldLabelId);
13206
- const ownLabelProps = { htmlFor: controlId, ...labelProps };
13207
- const labelTarget = active ? ownLabelProps.htmlFor : fieldLabelFor.htmlFor;
13208
- const labelled = !!controlId && labelTarget === controlId;
13209
- // Only a label that names the control gets an id to point at, with the
13210
- // suffix Field gives its own, from an id that is already unique.
13211
- const ownLabelId = needsLabelId && active && labelled
13212
- ? (labelProps?.id ?? `${generatedId}-label`)
13213
- : undefined;
13214
- // Inactive with a label still means an own Field may render it (pickers'
13215
- // inline mode). The explicit `htmlFor: undefined` tells Field the
13216
- // association is owned here, so it must not generate one that would
13217
- // dangle (#495 presence semantics).
13218
- const fieldLabelProps = active
13219
- ? ownLabelId
13220
- ? { ...ownLabelProps, id: ownLabelId }
13221
- : ownLabelProps
13222
- : label
13223
- ? { htmlFor: undefined, ...labelProps }
13224
- : labelProps;
13225
- const labelId = needsLabelId && labelled
13226
- ? active
13227
- ? ownLabelId
13228
- : fieldLabelFor.id
13229
- : undefined;
13230
- return { controlId, fieldLabelProps, labelled, labelId };
13231
- }
13232
- /**
13233
- * Group-input counterpart of {@link useAutoLabelId} (#494): a group of
13234
- * controls cannot take a single `htmlFor`, so instead the rendered `<label>`
13235
- * gets a generated id and the group container points at it with
13236
- * `aria-labelledby`. A user-supplied `labelProps.id` is used as the target
13237
- * instead of generating one. Any caller `htmlFor` is stripped — a group label
13238
- * names the group, never a single control — so the merged labelProps always
13239
- * carry an explicit `htmlFor: undefined`. When the group renders no label of
13240
- * its own, it points at the label of a labeled Field around it instead
13241
- * (#939). A caller who named the group with `aria-label` or
13242
- * `aria-labelledby` gets no `aria-labelledby` from either label, so their
13243
- * name wins wherever the label sits.
13244
- * 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.
13245
14437
  */
13246
- function useAutoLabelledBy({ label, labelProps, rendersLabel, callerProps, }) {
13247
- // Called unconditionally per the rules of hooks; SSR-safe on React 18 and 19.
13248
- const generatedId = React.useId();
13249
- // Set only when an outer Field holds the group (see useAutoLabelId).
13250
- const fieldLabelElementId = useFieldLabelElementId();
13251
- const active = !!label && rendersLabel;
13252
- const labelId = labelProps?.id ?? (active ? generatedId : undefined);
13253
- const fieldLabelProps = active
13254
- ? { ...labelProps, id: labelId, htmlFor: undefined }
13255
- : labelProps;
13256
- const aria = callerProps;
13257
- // Presence for aria-labelledby: the caller's key replaces the attribute
13258
- // through the spread whatever its value, and pointing it at a label would
13259
- // only take a Rate's fallback name away. Checked before either label, so
13260
- // the caller's name wins over the group's own `label` as it does over a
13261
- // Field's, the way an aria-label beats a `<label for>` on a single input.
13262
- const callerNamed = !!aria['aria-label'] || 'aria-labelledby' in aria;
13263
- const ariaLabelledBy = callerNamed
13264
- ? undefined
13265
- : active
13266
- ? labelId
13267
- : fieldLabelElementId;
13268
- return { ariaLabelledBy, fieldLabelProps };
13269
- }
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
+ };
13270
14460
 
13271
14461
  /**
13272
14462
  * The `Checkboxes` component wraps multiple `Checkbox` components in a Bulma-styled group.
@@ -13293,7 +14483,7 @@ function useAutoLabelledBy({ label, labelProps, rendersLabel, callerProps, }) {
13293
14483
  * <Checkbox value="angular">Angular</Checkbox>
13294
14484
  * </Checkboxes>
13295
14485
  */
13296
- 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 }) => {
13297
14487
  const insideField = useInsideField();
13298
14488
  const insideControl = useInsideControl();
13299
14489
  const ownField = rendersOwnField('Checkboxes', {
@@ -13304,10 +14494,14 @@ const CheckboxesComponent = ({ label, labelSize, labelProps, horizontal, message
13304
14494
  horizontal,
13305
14495
  fieldClassName,
13306
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;
13307
14501
  const { ariaLabelledBy, fieldLabelProps } = useAutoLabelledBy({
13308
14502
  label,
13309
14503
  labelProps,
13310
- rendersLabel: ownField,
14504
+ rendersLabel: ownField && !fieldsetField,
13311
14505
  callerProps: props,
13312
14506
  });
13313
14507
  const { bulmaHelperClasses, rest } = useBulmaClasses({
@@ -13332,11 +14526,17 @@ const CheckboxesComponent = ({ label, labelSize, labelProps, horizontal, message
13332
14526
  name,
13333
14527
  ...(groupActive ? { value: currentValue, onChange: handleChange } : {}),
13334
14528
  }), [name, groupActive, currentValue, handleChange]);
13335
- 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 }));
13336
14533
  let content = checkboxesElement;
13337
14534
  if (rendersOwnControl('Checkboxes', { insideControl })) {
13338
14535
  content = jsxRuntime.jsx(Control, { children: content });
13339
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
+ }
13340
14540
  if (ownField) {
13341
14541
  return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
13342
14542
  }
@@ -13513,36 +14713,112 @@ const Radio = React.forwardRef(({ color, size, className, children, textColor, d
13513
14713
  // Local props always win over the group (explicit > implicit).
13514
14714
  const group = useRadiosGroup();
13515
14715
  const effectiveName = name ?? group?.name;
13516
- // Group-managed checked state — only when the group is in
13517
- // controlled/uncontrolled mode (group.value is defined) AND this Radio
13518
- // 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;
13519
14787
  const groupManaged = group?.value !== undefined && value !== undefined;
13520
14788
  const effectiveChecked = checked !== undefined
13521
14789
  ? checked
13522
14790
  : groupManaged
13523
14791
  ? group.value === value
13524
14792
  : undefined;
13525
- // When the group manages checked, suppress local defaultChecked to avoid
13526
- // the controlled/uncontrolled React warning.
13527
14793
  const effectiveDefaultChecked = groupManaged ? undefined : defaultChecked;
13528
14794
  const handleChange = React.useCallback((e) => {
13529
- // Local onChange always fires.
13530
14795
  onChange?.(e);
13531
- // Dispatch to the group additionally — keeps group state in sync.
13532
14796
  if (group?.onChange && value !== undefined) {
13533
14797
  group.onChange(String(value));
13534
14798
  }
13535
14799
  }, [onChange, group, value]);
13536
- const mainClass = usePrefixedClassNames('styled-radio', 'radio', {
13537
- [`is-${color}`]: color && radioColors.includes(color),
13538
- [`is-${size}`]: size && radioSizes.includes(size),
13539
- });
13540
- const radioClass = classNames(mainClass, bulmaHelperClasses, className);
13541
- const checkClass = usePrefixedClassNames('check');
13542
- const controlLabelClass = usePrefixedClassNames('control-label');
13543
- 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] })] })] }));
13544
14820
  });
13545
- Radio.displayName = 'Radio';
14821
+ RadioCard.displayName = 'RadioCard';
13546
14822
 
13547
14823
  /**
13548
14824
  * The `Radios` component wraps multiple `Radio` components in a Bulma-styled group.
@@ -13568,7 +14844,7 @@ Radio.displayName = 'Radio';
13568
14844
  * <Radio value="green">Green</Radio>
13569
14845
  * </Radios>
13570
14846
  */
13571
- 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 }) => {
13572
14848
  const insideField = useInsideField();
13573
14849
  const insideControl = useInsideControl();
13574
14850
  const ownField = rendersOwnField('Radios', {
@@ -13579,10 +14855,14 @@ const RadiosComponent = ({ label, labelSize, labelProps, horizontal, message, me
13579
14855
  horizontal,
13580
14856
  fieldClassName,
13581
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;
13582
14862
  const { ariaLabelledBy, fieldLabelProps } = useAutoLabelledBy({
13583
14863
  label,
13584
14864
  labelProps,
13585
- rendersLabel: ownField,
14865
+ rendersLabel: ownField && !fieldsetField,
13586
14866
  callerProps: props,
13587
14867
  });
13588
14868
  const { bulmaHelperClasses, rest } = useBulmaClasses({
@@ -13610,11 +14890,17 @@ const RadiosComponent = ({ label, labelSize, labelProps, horizontal, message, me
13610
14890
  name,
13611
14891
  ...(groupActive ? { value: currentValue, onChange: handleChange } : {}),
13612
14892
  }), [name, groupActive, currentValue, handleChange]);
13613
- 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 }));
13614
14897
  let content = radiosElement;
13615
14898
  if (rendersOwnControl('Radios', { insideControl })) {
13616
14899
  content = jsxRuntime.jsx(Control, { children: content });
13617
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
+ }
13618
14904
  if (ownField) {
13619
14905
  return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
13620
14906
  }
@@ -14451,1190 +15737,758 @@ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName,
14451
15737
  const keep = (text) => (toAsciiDigits(text).match(new RegExp(allowed, 'g')) ?? [])
14452
15738
  .join('')
14453
15739
  .slice(0, length);
14454
- const commit = (next) => {
14455
- if (next === code)
14456
- return;
14457
- if (controlledValue === undefined)
14458
- setInternalValue(next);
14459
- onChange?.(next);
14460
- if (next.length === length)
14461
- onComplete?.(next);
14462
- };
14463
- const inputRef = React.useRef(null);
14464
- const setRefs = React.useCallback((node) => {
14465
- inputRef.current = node;
14466
- if (typeof ref === 'function')
14467
- return ref(node);
14468
- if (ref)
14469
- ref.current = node;
14470
- }, [ref]);
14471
- // The input's selection while it has focus, which the boxes draw.
14472
- const [selection, setSelection] = React.useState(null);
14473
- const follow = React.useCallback(() => {
14474
- const input = inputRef.current;
14475
- const next = getActiveElementInTree(input) === input
14476
- ? [input.selectionStart, input.selectionEnd]
14477
- : null;
14478
- setSelection(prev => prev?.[0] === next?.[0] && prev?.[1] === next?.[1] ? prev : next);
14479
- }, []);
14480
- // A new code moves the caret, and so can a value set from outside.
14481
- useClientLayoutEffect(follow, [code, follow]);
14482
- // React's `onSelect` never fires inside a shadow root, so the boxes
14483
- // follow the browser's own `selectionchange`. A caret that moves fires it
14484
- // at the input, and it bubbles to the document, but in a shadow root some
14485
- // browsers fire it at the document alone and others at the input alone,
14486
- // so both are followed.
14487
- React.useEffect(() => {
14488
- const input = inputRef.current;
14489
- const doc = input.ownerDocument;
14490
- input.addEventListener('selectionchange', follow);
14491
- doc.addEventListener('selectionchange', follow);
14492
- return () => {
14493
- input.removeEventListener('selectionchange', follow);
14494
- doc.removeEventListener('selectionchange', follow);
14495
- };
14496
- }, [follow]);
14497
- const handleChange = (e) => {
14498
- const input = e.target;
14499
- const typed = input.value;
14500
- const next = keep(typed);
14501
- if (next !== typed) {
14502
- // Drop what the code doesn't take before React sees the value, so
14503
- // the caret stays where it was rather than jumping to the end.
14504
- const caret = keep(typed.slice(0, input.selectionStart)).length;
14505
- input.value = next;
14506
- input.setSelectionRange(caret, caret);
14507
- }
14508
- commit(next);
14509
- };
14510
- // The browser cuts a paste to `maxLength` before anything else sees it,
14511
- // so a code pasted as "123 456" would lose its last digit. What the code
14512
- // takes is kept first, and it replaces the whole code, since what you
14513
- // paste into a code field is the code.
14514
- const handlePaste = (e) => {
14515
- onPaste?.(e);
14516
- if (e.defaultPrevented)
14517
- return;
14518
- e.preventDefault();
14519
- const pasted = keep(e.clipboardData.getData('text'));
14520
- if (pasted && !e.currentTarget.readOnly)
14521
- commit(pasted);
14522
- };
14523
- const rootClass = classNames(usePrefixedClassNames('pin-input'), bulmaHelperClasses, className);
14524
- const fieldClass = usePrefixedClassNames('pin-input-field');
14525
- const helpClass = usePrefixedClassNames('help', {
14526
- [`is-${messageColor}`]: !!messageColor,
14527
- });
14528
- // The boxes the selection covers: the one at a caret, or every selected
14529
- // one. A caret after the last character marks the last box.
14530
- const [start, end] = selection ?? [-1, -1];
14531
- const first = Math.min(start, length - 1);
14532
- const last = Math.max(first + 1, Math.min(end, length));
14533
- const boxes = Array.from({ length }, (_, i) => {
14534
- const char = code[i];
14535
- const active = i >= first && i < last;
14536
- return (jsxRuntime.jsx("div", { "aria-hidden": "true", className: prefixedClassNames(classPrefix, 'input', 'pin-input-box', {
14537
- [`is-${color}`]: !!color,
14538
- [`is-${size}`]: !!size,
14539
- 'is-rounded': isRounded,
14540
- 'is-focused': active,
14541
- 'has-caret': active && start === end && !char,
14542
- 'is-disabled': disabled,
14543
- }), children: char && (isMasked ? MASK : char) }, i));
14544
- });
14545
- // Left to right on a right-to-left page too: a code is written that way,
14546
- // so its first character stays in the first box on the left, and the
14547
- // arrow keys move the caret the way the boxes run.
14548
- 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,
14549
- // A partial code fails constraint validation. `minLength` would
14550
- // say so only after a keystroke, never after a paste or a filtered
14551
- // character, which set the value from script.
14552
- pattern: `${allowed}{${length}}`, ...rest, value: code, maxLength: length, disabled: disabled, onChange: handleChange, onPaste: handlePaste, onFocus: e => {
14553
- follow();
14554
- onFocus?.(e);
14555
- }, onBlur: e => {
14556
- setSelection(null);
14557
- onBlur?.(e);
14558
- },
14559
- // After the spread: an undefined `id` key in rest would wipe it.
14560
- id: controlId }), boxes] }));
14561
- if (ownControl) {
14562
- content = jsxRuntime.jsx(Control, { children: content });
14563
- }
14564
- const messageEl = message ? jsxRuntime.jsx("p", { className: helpClass, children: message }) : null;
14565
- if (ownField) {
14566
- return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
14567
- }
14568
- return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [content, messageEl] }));
14569
- });
14570
- PinInput.displayName = 'PinInput';
14571
-
14572
- /**
14573
- * Default star icon component.
14574
- */
14575
- 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" }) }));
14576
- /** Snap a value to the nearest precision step. */
14577
- function snapToPrecision(val, precision) {
14578
- return Math.round(val / precision) * precision;
14579
- }
14580
- /** Get fill percentage for a given icon index based on current value. */
14581
- function getFillPercent(iconIndex, value) {
14582
- if (value >= iconIndex + 1)
14583
- return 100;
14584
- if (value <= iconIndex)
14585
- return 0;
14586
- return Math.round((value - iconIndex) * 100);
14587
- }
14588
- /**
14589
- * The `Rate` component provides a star/icon-based rating system.
14590
- *
14591
- * @function
14592
- * @param {RateProps} props - Props for the Rate component.
14593
- * @param {React.Ref<HTMLDivElement>} ref - Forwarded ref to the container element.
14594
- * @returns {JSX.Element} The rendered rating component.
14595
- *
14596
- * @example
14597
- * // Basic 5-star rating
14598
- * <Rate defaultValue={3} />
14599
- *
14600
- * @example
14601
- * // With color and precision
14602
- * <Rate defaultValue={3.5} color="warning" precision={0.5} showScore />
14603
- *
14604
- * @example
14605
- * // With Font Awesome icons
14606
- * <Rate iconName="star" iconLibrary="fa" iconVariant="solid" />
14607
- */
14608
- 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) => {
14609
- const insideField = useInsideField();
14610
- const insideControl = useInsideControl();
14611
- const ownField = rendersOwnField('Rate', {
14612
- insideField,
14613
- insideControl,
14614
- label,
14615
- message,
14616
- horizontal,
14617
- fieldClassName,
14618
- });
14619
- const { ariaLabelledBy, fieldLabelProps } = useAutoLabelledBy({
14620
- label,
14621
- labelProps,
14622
- rendersLabel: ownField,
14623
- callerProps: props,
14624
- });
14625
- const { bulmaHelperClasses, rest } = useBulmaClasses(props);
14626
- const starBaseId = React.useId();
14627
- const [internalValue, setInternalValue] = React.useState(defaultValue);
14628
- const [hoverValue, setHoverValue] = React.useState(null);
14629
- const defaultIconLibrary = useIconLibrary();
14630
- const resolvedLibrary = iconLibraryProp || defaultIconLibrary || 'fa';
14631
- // Determine if controlled
14632
- const isControlled = controlledValue !== undefined;
14633
- const currentValue = isControlled ? controlledValue : internalValue;
14634
- const displayValue = hoverValue !== null ? hoverValue : currentValue;
14635
- // Update value
14636
- const updateValue = React.useCallback((newValue) => {
14637
- if (disabled)
14638
- return;
14639
- // Allow clicking same value to deselect (set to 0)
14640
- const finalValue = newValue === currentValue ? 0 : newValue;
14641
- if (!isControlled) {
14642
- setInternalValue(finalValue);
14643
- }
14644
- onChange?.(finalValue);
14645
- }, [isControlled, currentValue, disabled, onChange]);
14646
- // Handle click with precision support
14647
- const handleClick = React.useCallback((iconIndex, e) => {
14648
- if (disabled)
14649
- return;
14650
- if (precision < 1) {
14651
- const rect = e.currentTarget.getBoundingClientRect();
14652
- let relativeX = (e.clientX - rect.left) / rect.width;
14653
- if (rtl)
14654
- relativeX = 1 - relativeX;
14655
- const rawValue = iconIndex + relativeX;
14656
- const snapped = snapToPrecision(rawValue, precision);
14657
- const clamped = Math.max(precision, Math.min(snapped, max));
14658
- updateValue(clamped);
14659
- }
14660
- else {
14661
- updateValue(iconIndex + 1);
14662
- }
14663
- }, [disabled, precision, rtl, max, updateValue]);
14664
- // Handle mouse enter on icon
14665
- const handleMouseEnter = React.useCallback((iconIndex) => {
14666
- if (disabled)
14667
- return;
14668
- setHoverValue(iconIndex + 1);
14669
- }, [disabled]);
14670
- // Handle mouse move with precision support
14671
- const handleMouseMove = React.useCallback((iconIndex, e) => {
14672
- if (disabled || precision >= 1)
14673
- return;
14674
- const rect = e.currentTarget.getBoundingClientRect();
14675
- let relativeX = (e.clientX - rect.left) / rect.width;
14676
- if (rtl)
14677
- relativeX = 1 - relativeX;
14678
- const rawValue = iconIndex + relativeX;
14679
- const snapped = snapToPrecision(rawValue, precision);
14680
- const clamped = Math.max(precision, Math.min(snapped, max));
14681
- setHoverValue(clamped);
14682
- }, [disabled, precision, rtl, max]);
14683
- // Handle mouse leave
14684
- const handleMouseLeave = React.useCallback(() => {
14685
- setHoverValue(null);
14686
- }, []);
14687
- // Handle keyboard
14688
- const handleKeyDown = React.useCallback((e) => {
14689
- if (disabled)
15740
+ const commit = (next) => {
15741
+ if (next === code)
14690
15742
  return;
14691
- let newValue = currentValue;
14692
- const step = precision < 1 ? precision : 1;
14693
- if (e.key === 'ArrowRight' || e.key === 'ArrowUp') {
14694
- e.preventDefault();
14695
- newValue = Math.min(currentValue + step, max);
14696
- }
14697
- else if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') {
14698
- e.preventDefault();
14699
- newValue = Math.max(currentValue - step, 0);
14700
- }
14701
- else if (e.key === 'Home') {
14702
- e.preventDefault();
14703
- newValue = 0;
14704
- }
14705
- else if (e.key === 'End') {
14706
- e.preventDefault();
14707
- newValue = max;
14708
- }
14709
- // Round to avoid floating point issues
14710
- newValue = Math.round(newValue * 1000) / 1000;
14711
- if (newValue !== currentValue) {
14712
- if (!isControlled) {
14713
- setInternalValue(newValue);
14714
- }
14715
- onChange?.(newValue);
14716
- }
14717
- }, [currentValue, max, precision, disabled, isControlled, onChange]);
14718
- // Get text for current value
14719
- const getText = () => {
14720
- if (!showText || !texts)
14721
- return null;
14722
- const wholeIndex = Math.ceil(displayValue) - 1;
14723
- const baseText = texts[wholeIndex] || null;
14724
- if (!baseText)
14725
- return null;
14726
- // For fractional hover values with precision < 1, append "+"
14727
- if (precision < 1 && hoverValue !== null && hoverValue % 1 !== 0) {
14728
- return `${baseText}+`;
14729
- }
14730
- return baseText;
14731
- };
14732
- // Format score display
14733
- const getScoreDisplay = () => {
14734
- if (precision < 1 && currentValue % 1 !== 0) {
14735
- return currentValue.toFixed(1);
14736
- }
14737
- return currentValue;
15743
+ if (controlledValue === undefined)
15744
+ setInternalValue(next);
15745
+ onChange?.(next);
15746
+ if (next.length === length)
15747
+ onComplete?.(next);
14738
15748
  };
14739
- // Generate classes
14740
- const classPrefix = useClassPrefix();
14741
- const rateClasses = usePrefixedClassNames('rate', {
14742
- [`is-${size}`]: size,
14743
- 'is-disabled': disabled,
14744
- 'is-spaced': spaced,
14745
- 'is-rtl': rtl,
14746
- [`is-${color}`]: color,
14747
- });
14748
- const combinedClasses = classNames(rateClasses, bulmaHelperClasses, className);
14749
- // Render a single icon based on priority: customIcon > iconName > default SVG
14750
- const renderIcon = (iconIndex, isActive, fillPercent) => {
14751
- const iconProps = {
14752
- index: iconIndex,
14753
- isActive,
14754
- isHovered: hoverValue !== null && iconIndex < hoverValue,
14755
- value: displayValue,
14756
- 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);
14757
15781
  };
14758
- if (customIcon) {
14759
- return customIcon(iconProps);
14760
- }
14761
- if (iconName) {
14762
- return (jsxRuntime.jsx(Icon, { name: iconName, library: resolvedLibrary, variant: iconVariant, features: iconFeatures }));
14763
- }
14764
- return jsxRuntime.jsx(StarIcon, { filled: isActive });
14765
- };
14766
- // Render partial icon with layered approach
14767
- const renderPartialIcon = (iconIndex, fillPercent) => {
14768
- if (customIcon) {
14769
- // For custom icons, pass fillPercent and let consumer handle it
14770
- const iconProps = {
14771
- index: iconIndex,
14772
- isActive: fillPercent > 0,
14773
- isHovered: hoverValue !== null && iconIndex < (hoverValue ?? 0),
14774
- value: displayValue,
14775
- fillPercent,
14776
- };
14777
- return customIcon(iconProps);
14778
- }
14779
- if (iconName) {
14780
- return (jsxRuntime.jsxs("span", { className: prefixedClassNames(classPrefix, 'rate-icon-partial'), style: {
14781
- '--rate-fill-percent': `${fillPercent}%`,
14782
- }, 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);
14783
15793
  }
14784
- // Default SVG with clipPath for partial fill
14785
- const clipId = `rate-clip-${iconIndex}`;
14786
- 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);
14787
15795
  };
14788
- // The star the value falls in is the checked radio, and it carries the
14789
- // value: a radiogroup takes no aria-value* of its own. A fractional value
14790
- // names that star by the value itself ("3.5 stars"). Both go by the value
14791
- // as that name writes it, to hundredths, so a value a hair over a whole
14792
- // number checks that number's star under its own name, and no two stars
14793
- // share a name.
14794
- const shownValue = Number(currentValue.toFixed(2));
14795
- const checkedIndex = shownValue > 0 ? Math.min(Math.ceil(shownValue), max) - 1 : -1;
14796
- // Render icons
14797
- const renderIcons = () => {
14798
- const icons = [];
14799
- for (let i = 0; i < max; i++) {
14800
- const iconIndex = rtl ? max - 1 - i : i;
14801
- const fillPercent = getFillPercent(iconIndex, displayValue);
14802
- const isActive = fillPercent === 100;
14803
- const isHovered = hoverValue !== null && iconIndex < hoverValue;
14804
- const isPartial = fillPercent > 0 && fillPercent < 100;
14805
- const icon = isPartial
14806
- ? renderPartialIcon(iconIndex, fillPercent)
14807
- : renderIcon(iconIndex, isActive, fillPercent);
14808
- icons.push(jsxRuntime.jsx("span", { className: prefixedClassNames(classPrefix, 'rate-item', {
14809
- 'is-active': isActive,
14810
- 'is-hovered': isHovered,
14811
- }), 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
14812
- ? `${shownValue} stars`
14813
- : `${iconIndex + 1} star${iconIndex === 0 ? '' : 's'}`, tabIndex: -1, children: icon }, iconIndex));
14814
- }
14815
- 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);
14816
15808
  };
14817
- const text = getText();
15809
+ const rootClass = classNames(usePrefixedClassNames('pin-input'), bulmaHelperClasses, className);
15810
+ const fieldClass = usePrefixedClassNames('pin-input-field');
14818
15811
  const helpClass = usePrefixedClassNames('help', {
14819
15812
  [`is-${messageColor}`]: !!messageColor,
14820
15813
  });
14821
- const messageEl = message ? jsxRuntime.jsx("p", { className: helpClass, children: message }) : null;
14822
- const rateItemsClass = usePrefixedClassNames('rate-items');
14823
- const rateScoreClass = usePrefixedClassNames('rate-score');
14824
- const rateTextClass = usePrefixedClassNames('rate-text');
14825
- const rateCustomTextClass = usePrefixedClassNames('rate-custom-text');
14826
- 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 }))] }));
14827
- let content = rateElement;
14828
- 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) {
14829
15848
  content = jsxRuntime.jsx(Control, { children: content });
14830
15849
  }
15850
+ const messageEl = message ? jsxRuntime.jsx("p", { className: helpClass, children: message }) : null;
14831
15851
  if (ownField) {
14832
15852
  return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
14833
15853
  }
14834
15854
  return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [content, messageEl] }));
14835
15855
  });
14836
- Rate.displayName = 'Rate';
15856
+ PinInput.displayName = 'PinInput';
14837
15857
 
14838
- // What the status region announces: the `empty` content, as the one item
14839
- // it's handed while that content is on screen because of the user's typing.
14840
- const typedNoMatches$1 = [{ id: 'empty' }];
14841
- const nothingToAnnounce$1 = [];
14842
- // A press on an option would take focus from the field, since an option
14843
- // can't hold it, and nothing would put it back. Cancelling the press keeps
14844
- // it there, as the ARIA Authoring Practices' combobox does, and the click
14845
- // still picks the option.
14846
- const keepFocus = (e) => e.preventDefault();
14847
- // An IME that's composing owns every key: Enter confirms what it composed,
14848
- // and the arrows move through its candidates. Safari can say so with
14849
- // `keyCode` 229 rather than `isComposing`.
14850
- const isComposing$2 = (e) => e.nativeEvent.isComposing || e.nativeEvent.keyCode === 229;
14851
- // Whether an option can be picked. The arrows still reach a disabled one, so
14852
- // a screen reader user finds it.
14853
- const isEnabled = (item) => typeof item === 'string' || !item.disabled;
14854
15858
  /**
14855
- * 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.
14856
15876
  *
14857
15877
  * @function
14858
- * @param {AutocompleteProps} props - Props for the Autocomplete component.
14859
- * @param {React.Ref<HTMLInputElement>} ref - Forwarded ref to the input element.
14860
- * @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.
14861
15881
  *
14862
15882
  * @example
14863
- * // Basic usage with string array
14864
- * <Autocomplete
14865
- * data={['Apple', 'Banana', 'Cherry']}
14866
- * placeholder="Search fruit..."
14867
- * onSelect={(item) => console.log(item)}
14868
- * />
15883
+ * // Basic 5-star rating
15884
+ * <Rate defaultValue={3} />
14869
15885
  *
14870
15886
  * @example
14871
- * // With object data
14872
- * <Autocomplete
14873
- * data={[{ value: '1', label: 'Option 1' }, { value: '2', label: 'Option 2' }]}
14874
- * field="label"
14875
- * onSelect={(item) => console.log(item)}
14876
- * />
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" />
14877
15893
  */
14878
- 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,
14879
- // Deprecated no-ops, taken here so they stay off the wrapper.
14880
- // eslint-disable-next-line @typescript-eslint/no-unused-vars -- deprecated no-op
14881
- selected,
14882
- // eslint-disable-next-line @typescript-eslint/no-unused-vars -- deprecated no-op
14883
- dropdown,
14884
- // Field props
14885
- 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) => {
14886
15895
  const insideField = useInsideField();
14887
- // The suggestion list points `aria-labelledby` at the label that names
14888
- // the input, so it needs that label's id.
14889
- 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({
14890
15906
  label,
14891
- id,
14892
15907
  labelProps,
14893
- rendersLabel: !insideField,
14894
- needsLabelId: true,
15908
+ rendersLabel: ownField,
15909
+ callerProps: props,
14895
15910
  });
14896
15911
  const { bulmaHelperClasses, rest } = useBulmaClasses(props);
14897
- const { classPrefix } = useConfig();
14898
- const containerRef = React.useRef(null);
14899
- const inputRef = React.useRef(null);
14900
- const dropdownRef = React.useRef(null);
14901
- const [internalValue, setInternalValue] = React.useState('');
14902
- const [isActive, setIsActive] = React.useState(false);
14903
- // Whether the user has typed since the list opened, since only typing
14904
- // gets the `empty` content announced. Closing the list resets it (during
14905
- // render, React's pattern for adjusting state when other state changes),
14906
- // so opening the list again without typing stays quiet.
14907
- const [typed, setTyped] = React.useState(false);
14908
- if (typed && !isActive)
14909
- setTyped(false);
14910
- const [highlightedIndex, setHighlightedIndex] = React.useState(-1);
14911
- const listboxId = `${React.useId()}-listbox`;
14912
- // Use controlled or internal value
14913
- const inputValue = controlledValue !== undefined ? controlledValue : internalValue;
14914
- // Get display value from item
14915
- const getDisplayValue = (item) => {
14916
- if (typeof item === 'string')
14917
- return item;
14918
- const fieldValue = item[field];
14919
- if (typeof fieldValue === 'string')
14920
- return fieldValue;
14921
- return item.value || '';
14922
- };
14923
- // Filter data based on input
14924
- const filteredData = data.filter(item => {
14925
- const displayValue = getDisplayValue(item);
14926
- return displayValue.toLowerCase().includes(inputValue.toLowerCase());
14927
- });
14928
- // The dropdown shows the options or, when nothing matches, the `empty`
14929
- // content. The listbox holds only options, so it's on screen only while
14930
- // there are some.
14931
- const menuOpen = isActive && (filteredData.length > 0 || !!empty);
14932
- const listOpen = isActive && filteredData.length > 0;
14933
- // Handle input change
14934
- const handleInputChange = React.useCallback((e) => {
14935
- const newValue = e.target.value;
14936
- if (controlledValue === undefined) {
14937
- setInternalValue(newValue);
14938
- }
14939
- onInput?.(newValue);
14940
- setTyped(true);
14941
- if (!isActive && newValue) {
14942
- setIsActive(true);
14943
- }
14944
- }, [controlledValue, isActive, onInput]);
14945
- // Handle item selection
14946
- const handleSelect = React.useCallback((item) => {
14947
- if (typeof item !== 'string' && item.disabled)
14948
- return;
14949
- const displayValue = getDisplayValue(item);
14950
- if (controlledValue === undefined) {
14951
- setInternalValue(displayValue);
14952
- }
14953
- onInput?.(displayValue);
14954
- onSelect?.(item);
14955
- if (!keepOpen) {
14956
- setIsActive(false);
14957
- }
14958
- setHighlightedIndex(-1);
14959
- }, [controlledValue, field, keepOpen, onInput, onSelect]);
14960
- // Handle clear
14961
- const handleClear = React.useCallback(() => {
14962
- if (controlledValue === undefined) {
14963
- setInternalValue('');
14964
- }
14965
- onInput?.('');
14966
- onSelect?.(null);
14967
- inputRef.current?.focus();
14968
- }, [controlledValue, onInput, onSelect]);
14969
- // Handle focus
14970
- const handleFocus = React.useCallback(() => {
14971
- if (openOnFocus && !disabled) {
14972
- setIsActive(true);
14973
- }
14974
- }, [openOnFocus, disabled]);
14975
- // Read when a click lands outside, so the listener stays put while the
14976
- // list is open. What it selects changes as the user types and arrows
14977
- // through suggestions, the filtered list is new on every render, and an
14978
- // inline `onInput` or `onSelect` changes on every render of the parent.
14979
- // Typing leaves the highlight where it was, so a list that narrowed can
14980
- // leave it past the last suggestion, with nothing there to select.
14981
- const selectHighlightedRef = useLatestRef(() => {
14982
- if (selectOnClickOutside &&
14983
- highlightedIndex >= 0 &&
14984
- highlightedIndex < filteredData.length) {
14985
- handleSelect(filteredData[highlightedIndex]);
14986
- }
14987
- });
14988
- // Handle blur (click outside)
14989
- React.useEffect(() => {
14990
- if (!isActive)
14991
- return undefined;
14992
- const handleClickOutside = (e) => {
14993
- if (!containerRef.current)
14994
- return;
14995
- // Use composedPath() to correctly detect clicks inside Shadow DOM
14996
- const path = e.composedPath();
14997
- const isInside = path.includes(containerRef.current);
14998
- if (!isInside) {
14999
- selectHighlightedRef.current();
15000
- setIsActive(false);
15001
- }
15002
- };
15003
- document.addEventListener('mousedown', handleClickOutside);
15004
- return () => document.removeEventListener('mousedown', handleClickOutside);
15005
- }, [isActive, selectHighlightedRef]);
15006
- // Read when the dropdown opens or closes, so an inline `onActiveChange`,
15007
- // new on every render of the parent, isn't told again about a state that
15008
- // didn't change.
15009
- const onActiveChangeRef = useLatestRef(onActiveChange);
15010
- // Notify active state change
15011
- React.useEffect(() => {
15012
- onActiveChangeRef.current?.(isActive);
15013
- }, [isActive, onActiveChangeRef]);
15014
- // Keep first highlighted
15015
- React.useEffect(() => {
15016
- if (keepFirst && isActive && filteredData.length > 0) {
15017
- setHighlightedIndex(0);
15018
- }
15019
- }, [keepFirst, isActive, filteredData.length, inputValue]);
15020
- // Handle keyboard navigation
15021
- const handleKeyDown = React.useCallback((e) => {
15022
- 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)
15023
15924
  return;
15024
- switch (e.key) {
15025
- case 'ArrowDown':
15026
- e.preventDefault();
15027
- if (!isActive) {
15028
- // A closed list opens on its first enabled option, as the ARIA
15029
- // Authoring Practices' combobox does. Alt+Down Arrow opens it
15030
- // without a highlight.
15031
- setIsActive(true);
15032
- setHighlightedIndex(e.altKey ? -1 : filteredData.map(isEnabled).indexOf(true));
15033
- }
15034
- else {
15035
- setHighlightedIndex(prev => prev < filteredData.length - 1 ? prev + 1 : prev);
15036
- }
15037
- break;
15038
- case 'ArrowUp':
15039
- e.preventDefault();
15040
- if (!isActive) {
15041
- // And Up Arrow opens it on its last enabled option.
15042
- setIsActive(true);
15043
- setHighlightedIndex(filteredData.map(isEnabled).lastIndexOf(true));
15044
- }
15045
- else {
15046
- setHighlightedIndex(prev => (prev > 0 ? prev - 1 : prev));
15047
- }
15048
- break;
15049
- case 'Enter':
15050
- // With the list open on a highlighted option it can pick, Enter
15051
- // is that option's. Otherwise it closes the list and goes on, so
15052
- // a form around the field submits, as it would from any text
15053
- // field.
15054
- if (isActive &&
15055
- highlightedIndex >= 0 &&
15056
- filteredData[highlightedIndex] &&
15057
- isEnabled(filteredData[highlightedIndex])) {
15058
- e.preventDefault();
15059
- handleSelect(filteredData[highlightedIndex]);
15060
- }
15061
- else {
15062
- setIsActive(false);
15063
- }
15064
- break;
15065
- case 'Escape':
15066
- // Only closing the dropdown claims the key. With it closed, the
15067
- // key goes on to a Modal around the field.
15068
- if (menuOpen)
15069
- e.preventDefault();
15070
- setIsActive(false);
15071
- setHighlightedIndex(-1);
15072
- break;
15073
- case 'Tab':
15074
- if (isActive &&
15075
- highlightedIndex >= 0 &&
15076
- filteredData[highlightedIndex]) {
15077
- handleSelect(filteredData[highlightedIndex]);
15078
- }
15079
- setIsActive(false);
15080
- break;
15925
+ // Allow clicking same value to deselect (set to 0)
15926
+ const finalValue = newValue === currentValue ? 0 : newValue;
15927
+ if (!isControlled) {
15928
+ setInternalValue(finalValue);
15081
15929
  }
15082
- }, [
15083
- disabled,
15084
- isActive,
15085
- menuOpen,
15086
- highlightedIndex,
15087
- filteredData,
15088
- handleSelect,
15089
- ]);
15090
- // Handle dropdown scroll for infinite scroll
15091
- const handleDropdownScroll = React.useCallback(() => {
15092
- 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)
15093
15935
  return;
15094
- const { scrollTop, scrollHeight, clientHeight } = dropdownRef.current;
15095
- if (scrollHeight - scrollTop - clientHeight <= infiniteScrollDistance) {
15096
- 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);
15097
15945
  }
15098
- }, [checkInfiniteScroll, infiniteScrollDistance, onInfiniteScroll]);
15099
- // Scroll highlighted item into view
15100
- React.useEffect(() => {
15101
- if (highlightedIndex >= 0 && dropdownRef.current) {
15102
- const highlightedEl = dropdownRef.current.querySelector(`[data-index="${highlightedIndex}"]`);
15103
- if (highlightedEl &&
15104
- typeof highlightedEl.scrollIntoView === 'function') {
15105
- 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);
15106
16000
  }
16001
+ onChange?.(newValue);
15107
16002
  }
15108
- }, [highlightedIndex]);
15109
- // Combined ref
15110
- const combinedRef = React.useCallback((node) => {
15111
- inputRef.current =
15112
- node;
15113
- if (typeof ref === 'function') {
15114
- 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}+`;
15115
16015
  }
15116
- else if (ref) {
15117
- ref.current =
15118
- node;
16016
+ return baseText;
16017
+ };
16018
+ // Format score display
16019
+ const getScoreDisplay = () => {
16020
+ if (precision < 1 && currentValue % 1 !== 0) {
16021
+ return currentValue.toFixed(1);
15119
16022
  }
15120
- }, [ref]);
16023
+ return currentValue;
16024
+ };
15121
16025
  // Generate classes
15122
- const autocompleteClasses = usePrefixedClassNames('autocomplete', {
15123
- 'is-active': isActive,
15124
- [`is-${size}`]: !!size,
15125
- });
15126
- const inputClasses = usePrefixedClassNames('input', {
15127
- [`is-${color}`]: !!color,
15128
- [`is-${size}`]: !!size,
15129
- 'is-loading': loading,
15130
- });
15131
- const controlClasses = usePrefixedClassNames('control', 'is-expanded', 'has-icons-right');
15132
- const dropdownMenuClasses = usePrefixedClassNames('dropdown-menu', {
15133
- 'is-active': menuOpen,
15134
- });
15135
- const dropdownContentClass = usePrefixedClassNames('dropdown-content');
15136
- const dropdownHeaderClass = usePrefixedClassNames('dropdown-header');
15137
- const dropdownFooterClass = usePrefixedClassNames('dropdown-footer');
15138
- const emptyItemClasses = usePrefixedClassNames('dropdown-item', 'has-text-grey');
15139
- const iconRightClickableClass = usePrefixedClassNames('icon', 'is-right', 'is-clickable');
15140
- const iconRightClass = usePrefixedClassNames('icon', 'is-right');
15141
- const loaderClass = usePrefixedClassNames('loader', 'is-loading');
15142
- const combinedClasses = classNames(autocompleteClasses, bulmaHelperClasses, className);
15143
- const helpClass = usePrefixedClassNames('help', {
15144
- [`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,
15145
16033
  });
15146
- const messageEl = message ? jsxRuntime.jsx("p", { className: helpClass, children: message }) : null;
15147
- // The clear button gives way to a spinner at its spot: the `loading` one,
15148
- // or that of a loading Control this sits in.
15149
- const controlLoading = useControlLoading();
15150
- const showClear = clearable && !!inputValue && !disabled && !loading && !controlLoading;
15151
- // The combobox controls its listbox and points at the highlighted option.
15152
- // The listbox takes its name from the label that names the combobox, the
15153
- // one this renders or a surrounding Field's, so the two say the same
15154
- // thing. When no label names the combobox, or the one that does has no
15155
- // id to point at, the list keeps a fallback name. While the dropdown
15156
- // shows only the `empty` content there's no listbox, so the combobox is
15157
- // collapsed and points at nothing.
15158
- const activeOptionId = listOpen &&
15159
- highlightedIndex >= 0 &&
15160
- highlightedIndex < filteredData.length
15161
- ? `${listboxId}-${highlightedIndex}`
15162
- : undefined;
15163
- // A polite status region, in the page from the start so it's there
15164
- // before anything is written into it, announces the `empty` content
15165
- // when the user's typing leaves nothing that matches what's in the
15166
- // field. It follows what's on screen, so results that come in after the
15167
- // typing count too. It's said once each time that happens, not again on
15168
- // each keystroke while nothing matches. An empty field isn't a search,
15169
- // so it stays quiet then, even with no data. While `loading`, or a
15170
- // loading Control around it, says results are on their way, it waits,
15171
- // and it's said once loading ends with nothing matching.
15172
- const emptyItems = typed &&
15173
- menuOpen &&
15174
- !listOpen &&
15175
- !!inputValue &&
15176
- !loading &&
15177
- !controlLoading
15178
- ? typedNoMatches$1
15179
- : nothingToAnnounce$1;
15180
- // The region reads the text on screen, whatever `empty` renders.
15181
- const emptyRef = React.useRef(null);
15182
- const describeEmpty = React.useCallback(() => {
15183
- const node = emptyRef.current;
15184
- /* istanbul ignore if: it's only announced while it's on screen */
15185
- if (!node)
15186
- return null;
15187
- return spokenText(node);
15188
- }, []);
15189
- 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) => {
15190
- const isDisabled = typeof item !== 'string' && item.disabled;
15191
- const isHighlighted = index === highlightedIndex;
15192
- const itemClasses = prefixedClassNames(classPrefix, 'dropdown-item', {
15193
- 'is-active': isHighlighted,
15194
- 'is-disabled': isDisabled,
15195
- });
15196
- 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
15197
- ? itemTemplate(item)
15198
- : getDisplayValue(item) }, index));
15199
- }) })) : (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 })] }));
15200
- if (!insideField) {
15201
- 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 });
15202
16116
  }
15203
- 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] }));
15204
16121
  });
15205
- Autocomplete.displayName = 'Autocomplete';
16122
+ Rate.displayName = 'Rate';
15206
16123
 
15207
- // INTERNAL: deliberately not exported from src/index.ts.
15208
- /** An IME is composing, so the key belongs to it (`keyCode` 229 is Safari's word for it). */
15209
- const isComposing$1 = (event) => event.nativeEvent.isComposing || event.nativeEvent.keyCode === 229;
15210
- 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;
15211
16140
  /**
15212
- * The keys, the open state, the highlight and the ARIA of an editable
15213
- * combobox whose list the app renders and filters (#1083). DOM focus stays
15214
- * in the field the whole time: the highlight is `aria-activedescendant`, as
15215
- * the ARIA Authoring Practices ask of a combobox, where `useMenuKeys` moves
15216
- * real focus, as a menu should.
16141
+ * The `Autocomplete` component provides an input field with dropdown suggestions that filter based on user input.
15217
16142
  *
15218
- * The options are read from the listbox in document order whenever they're
15219
- * needed, so options in groups and options that come and go are found, and
15220
- * the highlight is remembered by value, so it survives a re-render. The text
15221
- * in the field and any value are the app's: a pick only calls
15222
- * `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.
15223
16147
  *
15224
- * @function useComboboxEngine
15225
- * @param options - The open state, the pick callback and the shape.
15226
- * @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
+ * />
15227
16163
  */
15228
- function useComboboxEngine({ open: controlledOpen, defaultOpen, onOpenChange, onSelect, onSettle, keepFirst, multiple, inline, deferred, }) {
15229
- const baseId = React.useId();
15230
- const listboxId = `${baseId}-listbox`;
15231
- const itemPrefix = `${baseId}-item-`;
15232
- // The value goes into the id encoded, so a value with a space in it still
15233
- // makes one id, not two.
15234
- const itemId = React.useCallback((value) => itemPrefix + encodeURIComponent(value), [itemPrefix]);
15235
- 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);
15236
16185
  const inputRef = React.useRef(null);
15237
- const listboxRef = React.useRef(null);
15238
- const popupRef = React.useRef(null);
15239
- const insidePressRef = React.useRef(null);
15240
- const optionsRef = React.useRef(new Map());
15241
- const landingRef = React.useRef(null);
15242
- const wasOpenRef = React.useRef(false);
15243
- const shownValuesRef = React.useRef([]);
15244
- // Whether the next highlight change is one to scroll to: a key's or a
15245
- // landing's. The pointer's isn't, since the pointer is already on that
15246
- // option, and scrolling would move the list out from under it.
15247
- const scrollToHighlightRef = React.useRef(false);
15248
- const isControlled = controlledOpen !== undefined;
15249
- const [internalOpen, setInternalOpen] = React.useState(defaultOpen);
15250
- const open = inline || (controlledOpen ?? internalOpen);
15251
- 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.
15252
16193
  const [typed, setTyped] = React.useState(false);
15253
- // Whether a listbox with an option in it is on the page, which only the
15254
- // page can say, so it's read once the open list has rendered.
15255
- const [listShown, setListShown] = React.useState(null);
15256
- // Options register from their own effects, which needn't render the
15257
- // component that holds this hook, so they ask for a render to be read.
15258
- const [, refresh] = React.useReducer((n) => n + 1, 0);
15259
- // Closing forgets the highlight, the typing and what was on the page,
15260
- // during render, React's pattern for adjusting state when other state
15261
- // changes.
15262
- if (!open) {
15263
- if (highlighted !== null)
15264
- setHighlighted(null);
15265
- if (typed)
15266
- setTyped(false);
15267
- if (listShown !== null)
15268
- setListShown(null);
15269
- }
15270
- const expanded = open && (listShown ?? !deferred);
15271
- /** The options on the page, in document order, groups included. */
15272
- const shownOptions = React.useCallback(() => {
15273
- const listbox = listboxRef.current;
15274
- if (!listbox)
15275
- return [];
15276
- const shown = [];
15277
- listbox
15278
- .querySelectorAll('[role="option"]')
15279
- .forEach(element => {
15280
- // Only the Combobox's own options: an element given the role by
15281
- // hand isn't one.
15282
- const state = optionsRef.current.get(element);
15283
- if (state)
15284
- shown.push({ element, ...state });
15285
- });
15286
- return shown;
15287
- }, []);
15288
- const requestOpen = React.useCallback((next, landing = 'open') => {
15289
- if (inline || next === open)
15290
- return;
15291
- landingRef.current = next ? landing : null;
15292
- onOpenChange?.(next);
15293
- if (!isControlled)
15294
- setInternalOpen(next);
15295
- }, [inline, open, onOpenChange, isControlled]);
15296
- const close = React.useCallback(() => requestOpen(false), [requestOpen]);
15297
- // After every render, read the list: whether it has options, and where
15298
- // the highlight goes now. A highlighted option that has left takes the
15299
- // highlight with it, or hands it to the first enabled one under
15300
- // `keepFirst`, which also moves it there whenever the options change.
15301
- // eslint-disable-next-line react-hooks/exhaustive-deps -- after every render on purpose
15302
- useClientLayoutEffect(() => {
15303
- const opened = open && !wasOpenRef.current;
15304
- wasOpenRef.current = open;
15305
- if (!open) {
15306
- landingRef.current = null;
15307
- 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)
15308
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('');
15309
16250
  }
15310
- const options = shownOptions();
15311
- const values = options.map(option => option.value);
15312
- const changed = !sameValues(values, shownValuesRef.current);
15313
- shownValuesRef.current = values;
15314
- setListShown(values.length > 0);
15315
- const landing = landingRef.current ?? (opened ? 'open' : null);
15316
- landingRef.current = null;
15317
- const enabled = options.filter(option => !option.disabled);
15318
- const checked = enabled.find(option => option.checked)?.value;
15319
- const first = enabled.length > 0 ? enabled[0].value : null;
15320
- const last = enabled.length > 0 ? enabled[enabled.length - 1].value : null;
15321
- let next = highlighted;
15322
- if (landing === 'first')
15323
- next = checked ?? first;
15324
- else if (landing === 'last')
15325
- next = checked ?? last;
15326
- else if (landing === 'open')
15327
- next = checked ?? (keepFirst ? first : null);
15328
- else if (landing === 'typed')
15329
- next = keepFirst ? first : null;
15330
- else if (landing === 'none')
15331
- next = null;
15332
- else if (keepFirst && changed)
15333
- next = first;
15334
- // Without `keepFirst`, which moved it above, a highlighted option
15335
- // that left takes the highlight with it.
15336
- else if (next !== null && !values.includes(next))
15337
- next = null;
15338
- // Only a change is set. StrictMode runs this twice on mount, and the
15339
- // second run, with the landing used up, works out the rendered highlight
15340
- // again, so it leaves the first run's landing in place.
15341
- if (next !== highlighted) {
15342
- scrollToHighlightRef.current = true;
15343
- 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]);
15344
16272
  }
15345
16273
  });
15346
- // A browser scrolls to what has focus, not to an active descendant, so
15347
- // the highlighted option is brought into view here, when a key or a
15348
- // landing moved it there.
15349
- useClientLayoutEffect(() => {
15350
- if (highlighted === null || !scrollToHighlightRef.current)
15351
- return;
15352
- scrollToHighlightRef.current = false;
15353
- const option = shownOptions().find(shown => shown.value === highlighted);
15354
- option?.element.scrollIntoView?.({ block: 'nearest' });
15355
- }, [highlighted, shownOptions]);
15356
- // A press outside the field and the list closes it. The path is checked
15357
- // rather than the target, so it works in a shadow root.
16274
+ // Handle blur (click outside)
15358
16275
  React.useEffect(() => {
15359
- const root = rootRef.current;
15360
- if (!open || inline || !root)
16276
+ if (!isActive)
15361
16277
  return undefined;
15362
- const onPress = (event) => {
15363
- if (event === insidePressRef.current ||
15364
- isEventInside(event, rootRef.current) ||
15365
- isEventInside(event, popupRef.current)) {
16278
+ const handleClickOutside = (e) => {
16279
+ if (!containerRef.current)
15366
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);
15367
16287
  }
15368
- close();
15369
16288
  };
15370
- // Its document, an iframe's when the tree renders into one.
15371
- const doc = root.ownerDocument;
15372
- doc.addEventListener('pointerdown', onPress);
15373
- return () => doc.removeEventListener('pointerdown', onPress);
15374
- }, [open, inline, close]);
15375
- const submit = React.useCallback((value) => {
15376
- onSelect?.(value);
15377
- if (!multiple)
15378
- close();
15379
- }, [onSelect, multiple, close]);
15380
- const onKeyDown = (event) => {
15381
- // The caller's handler ran first and can keep a key, and an IME that's
15382
- // composing owns every key.
15383
- if (event.defaultPrevented || isComposing$1(event))
15384
- return;
15385
- const { key, altKey } = event;
15386
- if (!open) {
15387
- if (key === 'ArrowDown' || (key === 'ArrowUp' && !altKey)) {
15388
- event.preventDefault();
15389
- requestOpen(true, altKey ? 'none' : key === 'ArrowDown' ? 'first' : 'last');
15390
- }
15391
- 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);
15392
16304
  }
15393
- 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) {
15394
16311
  case 'ArrowDown':
15395
- case 'ArrowUp': {
15396
- if (altKey) {
15397
- if (key === 'ArrowUp' && !inline) {
15398
- event.preventDefault();
15399
- close();
15400
- }
15401
- 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));
15402
16319
  }
15403
- event.preventDefault();
15404
- // Disabled options too, so a screen reader user finds them.
15405
- const options = shownOptions();
15406
- if (options.length === 0)
15407
- return;
15408
- const at = options.findIndex(option => option.value === highlighted);
15409
- const step = key === 'ArrowDown' ? 1 : -1;
15410
- const index = at === -1
15411
- ? step === 1
15412
- ? 0
15413
- : options.length - 1
15414
- : (at + step + options.length) % options.length;
15415
- scrollToHighlightRef.current = true;
15416
- setHighlighted(options[index].value);
15417
- return;
15418
- }
15419
- case 'Enter': {
15420
- const option = shownOptions().find(shown => shown.value === highlighted);
15421
- if (option) {
15422
- event.preventDefault();
15423
- if (!option.disabled)
15424
- submit(option.value);
15425
- return;
16320
+ else {
16321
+ setHighlightedIndex(prev => prev < filteredData.length - 1 ? prev + 1 : prev);
15426
16322
  }
15427
- // Nothing to pick: the key goes on, so a form submits.
15428
- onSettle?.();
15429
- close();
15430
- return;
15431
- }
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;
15432
16351
  case 'Escape':
15433
- if (inline)
15434
- return;
15435
- // It stops here only when there was something to close, so a
15436
- // Popover or Modal around it keeps its own Escape otherwise. Nothing
15437
- // stops it going on: a handler further out reads `defaultPrevented`.
15438
- if (popupRef.current)
15439
- event.preventDefault();
15440
- close();
15441
- 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;
15442
16359
  case 'Tab':
15443
- close();
15444
- return;
15445
- case 'Home':
15446
- case 'End':
15447
- case 'ArrowLeft':
15448
- case 'ArrowRight':
15449
- // The field's own keys, which take the field back from the list.
15450
- setHighlighted(null);
15451
- return;
15452
- }
15453
- };
15454
- const onChange = () => {
15455
- setTyped(true);
15456
- if (open) {
15457
- landingRef.current = 'typed';
15458
- setHighlighted(null);
15459
- refresh();
15460
- }
15461
- else {
15462
- requestOpen(true, 'typed');
16360
+ if (isActive &&
16361
+ highlightedIndex >= 0 &&
16362
+ filteredData[highlightedIndex]) {
16363
+ handleSelect(filteredData[highlightedIndex]);
16364
+ }
16365
+ setIsActive(false);
16366
+ break;
15463
16367
  }
15464
- };
15465
- const onClick = () => {
15466
- requestOpen(true, 'open');
15467
- };
15468
- const onBlur = (event) => {
15469
- const next = event.relatedTarget;
15470
- if (next &&
15471
- (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)
15472
16379
  return;
16380
+ const { scrollTop, scrollHeight, clientHeight } = dropdownRef.current;
16381
+ if (scrollHeight - scrollTop - clientHeight <= infiniteScrollDistance) {
16382
+ onInfiniteScroll?.();
15473
16383
  }
15474
- close();
15475
- };
15476
- const hover = (value) => {
15477
- scrollToHighlightRef.current = false;
15478
- setHighlighted(value);
15479
- };
15480
- const pick = (value) => {
15481
- submit(value);
15482
- // A press in the list keeps focus in the field, but a browser that
15483
- // ignores that, or a tap, can leave it elsewhere.
15484
- const input = inputRef.current;
15485
- if (input && input.isConnected && getActiveElementInTree(input) !== input) {
15486
- 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
+ }
15487
16393
  }
15488
- };
15489
- const keepFocus = (event) => event.preventDefault();
15490
- const markInside = (event) => {
15491
- insidePressRef.current = event.nativeEvent;
15492
- };
15493
- const registerPopup = React.useCallback((element) => {
15494
- popupRef.current = element;
15495
- return () => {
15496
- popupRef.current = null;
15497
- };
15498
- }, []);
15499
- const registerItem = React.useCallback((element, state) => {
15500
- const options = optionsRef.current;
15501
- options.set(element, state);
15502
- refresh();
15503
- return () => {
15504
- options.delete(element);
15505
- refresh();
15506
- };
15507
- }, []);
15508
- return {
15509
- open,
15510
- expanded,
15511
- highlighted,
15512
- typed,
15513
- listboxId,
15514
- itemId,
15515
- rootRef,
15516
- inputRef,
15517
- listboxRef,
15518
- registerPopup,
15519
- registerItem,
15520
- onKeyDown,
15521
- onChange,
15522
- onClick,
15523
- onBlur,
15524
- hover,
15525
- pick,
15526
- keepFocus,
15527
- markInside,
15528
- };
15529
- }
15530
-
15531
- /**
15532
- * Bulma Input component with full Bulma helper class support.
15533
- *
15534
- * @function
15535
- * @param {InputBaseProps} props - Props for the Input component.
15536
- * @returns {JSX.Element} The rendered input element.
15537
- * @see {@link https://bulma.io/documentation/form/input/ | Bulma Input documentation}
15538
- */
15539
- const InputBase = React.forwardRef(({ color, size, isRounded, isStatic, isHovered, isFocused, isLoading, className, disabled, readOnly, ...props }, ref) => {
15540
- const { bulmaHelperClasses, rest } = useBulmaClasses({
15541
- color,
15542
- ...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,
15543
16411
  });
15544
- // A labeled Field's generated id when no user id arrived (rest.id may be
15545
- // an undefined own key), reported to that Field (#1004).
15546
- const id = useFieldLabelTarget(rest.id);
15547
- const mainClass = usePrefixedClassNames('input', {
16412
+ const inputClasses = usePrefixedClassNames('input', {
15548
16413
  [`is-${color}`]: !!color,
15549
16414
  [`is-${size}`]: !!size,
15550
- 'is-rounded': isRounded,
15551
- 'is-static': isStatic,
15552
- 'is-hovered': isHovered,
15553
- 'is-focused': isFocused,
15554
- 'is-loading': isLoading,
15555
- });
15556
- const inputClass = classNames(mainClass, bulmaHelperClasses, className);
15557
- return (jsxRuntime.jsx("input", { ref: ref, className: inputClass, disabled: disabled, readOnly: readOnly, ...rest,
15558
- // After the spread, which may carry an undefined id key.
15559
- id: id }));
15560
- });
15561
- InputBase.displayName = 'InputBase';
15562
-
15563
- /**
15564
- * 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.
15565
- *
15566
- * @function
15567
- * @param {InputProps} props - Props for Input.
15568
- * @returns {JSX.Element} The composed field element.
15569
- *
15570
- * @example
15571
- * <Input label="Username" placeholder="Enter username" iconLeftName="user" />
15572
- *
15573
- * @example
15574
- * <Input
15575
- * label="Email"
15576
- * type="email"
15577
- * message="Please enter a valid email"
15578
- * messageColor="danger"
15579
- * color="danger"
15580
- * />
15581
- */
15582
- const Input = React.forwardRef(({
15583
- // Field props
15584
- label, labelSize, labelProps, horizontal,
15585
- // Control props
15586
- iconLeft, iconRight, iconLeftName, iconRightName, iconLeftSize, iconRightSize, hasIconsLeft, hasIconsRight, isLoading, isExpanded, controlSize,
15587
- // Message props
15588
- message, messageColor,
15589
- // Container class overrides
15590
- fieldClassName, controlClassName,
15591
- // Everything else goes to Input
15592
- ...inputProps }, ref) => {
15593
- const insideField = useInsideField();
15594
- const insideControl = useInsideControl();
15595
- const ownField = rendersOwnField('Input', {
15596
- insideField,
15597
- insideControl,
15598
- label,
15599
- message,
15600
- horizontal,
15601
- fieldClassName,
15602
- });
15603
- const ownControl = rendersOwnControl('Input', {
15604
- insideControl,
15605
- isLoading,
15606
- iconLeft,
15607
- iconLeftName,
15608
- iconLeftSize,
15609
- iconRight,
15610
- iconRightName,
15611
- iconRightSize,
15612
- hasIconsLeft,
15613
- hasIconsRight,
15614
- isExpanded,
15615
- controlSize,
15616
- controlClassName,
16415
+ 'is-loading': loading,
15617
16416
  });
15618
- const { controlId, fieldLabelProps } = useAutoLabelId({
15619
- label,
15620
- id: inputProps.id,
15621
- labelProps,
15622
- rendersLabel: ownField,
16417
+ const controlClasses = usePrefixedClassNames('control', 'is-expanded', 'has-icons-right');
16418
+ const dropdownMenuClasses = usePrefixedClassNames('dropdown-menu', {
16419
+ 'is-active': menuOpen,
15623
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);
15624
16429
  const helpClass = usePrefixedClassNames('help', {
15625
16430
  [`is-${messageColor}`]: !!messageColor,
15626
16431
  });
15627
- let content = jsxRuntime.jsx(InputBase, { ref: ref, ...inputProps, id: controlId });
15628
- if (ownControl) {
15629
- 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 }));
15630
- }
15631
16432
  const messageEl = message ? jsxRuntime.jsx("p", { className: helpClass, children: message }) : null;
15632
- if (ownField) {
15633
- 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] }));
15634
16488
  }
15635
- return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [content, messageEl] }));
16489
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [autocompleteElement, messageEl] }));
15636
16490
  });
15637
- Input.displayName = 'Input';
16491
+ Autocomplete.displayName = 'Autocomplete';
15638
16492
 
15639
16493
  const validComboboxSizes = ['small', 'medium', 'large'];
15640
16494
  // What the status region announces: the `Combobox.Empty` content, as the one
@@ -23462,6 +24316,7 @@ exports.Carousel = Carousel;
23462
24316
  exports.CarouselItem = CarouselItem;
23463
24317
  exports.Cell = Cell;
23464
24318
  exports.Checkbox = Checkbox;
24319
+ exports.CheckboxCard = CheckboxCard;
23465
24320
  exports.Checkboxes = Checkboxes;
23466
24321
  exports.ClientOnly = ClientOnly;
23467
24322
  exports.Code = Code;
@@ -23476,6 +24331,13 @@ exports.ComboboxInput = ComboboxInput;
23476
24331
  exports.ComboboxItem = ComboboxItem;
23477
24332
  exports.ComboboxList = ComboboxList;
23478
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;
23479
24341
  exports.ConfigProvider = ConfigProvider;
23480
24342
  exports.Container = Container;
23481
24343
  exports.Content = Content;
@@ -23584,6 +24446,7 @@ exports.Portal = Portal;
23584
24446
  exports.Pre = Pre;
23585
24447
  exports.Progress = Progress;
23586
24448
  exports.Radio = Radio;
24449
+ exports.RadioCard = RadioCard;
23587
24450
  exports.Radios = Radios;
23588
24451
  exports.Rate = Rate;
23589
24452
  exports.Reveal = Reveal;