@allxsmith/bestax-bulma 5.30.0 → 5.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -2352,6 +2352,182 @@ function getDeepestActiveElement(doc = document) {
2352
2352
  return active;
2353
2353
  }
2354
2354
 
2355
+ // INTERNAL: deliberately not exported from src/index.ts.
2356
+ /**
2357
+ * Every menu-item role, so an item a caller gives `menuitemcheckbox` or
2358
+ * `menuitemradio` (a filter or sort menu) stays reachable.
2359
+ */
2360
+ const MENU_ITEM_SELECTOR = '[role="menuitem"], [role="menuitemcheckbox"], [role="menuitemradio"]';
2361
+ /**
2362
+ * Whether a menu item can be reached and activated: it isn't disabled, as a
2363
+ * native control or through `aria-disabled="true"`.
2364
+ */
2365
+ function isEnabledMenuItem(item) {
2366
+ return (!item.hasAttribute('disabled') &&
2367
+ item.getAttribute('aria-disabled') !== 'true');
2368
+ }
2369
+ /** Whether `node` is one of `menu`'s items, enabled or not. */
2370
+ const isItemOf = (menu, node) => Boolean(node && menu.contains(node) && node.matches(MENU_ITEM_SELECTOR));
2371
+ /** The items in `menu` the keys reach, in document order. */
2372
+ function enabledMenuItems(menu) {
2373
+ return Array.from(menu.querySelectorAll(MENU_ITEM_SELECTOR)).filter(isEnabledMenuItem);
2374
+ }
2375
+ /** How long typed characters keep adding up to one search, in milliseconds. */
2376
+ const TYPEAHEAD_TIMEOUT = 500;
2377
+ /**
2378
+ * Whether the browser activates `item` on this key by itself: a `<button>`
2379
+ * answers Enter and Space, and a link with an `href` answers Enter.
2380
+ */
2381
+ function activatesItself(item, key) {
2382
+ if (item.tagName === 'BUTTON')
2383
+ return true;
2384
+ return key === 'Enter' && item.tagName === 'A' && item.hasAttribute('href');
2385
+ }
2386
+ /**
2387
+ * The text typing finds an item by: what it says, without what's hidden from
2388
+ * assistive technology, such as an icon whose glyph is a ligature's name.
2389
+ */
2390
+ function label(item) {
2391
+ let text = '';
2392
+ const read = (node) => {
2393
+ node.childNodes.forEach(child => {
2394
+ if (child.nodeType === 3)
2395
+ text += child.textContent;
2396
+ else if (child.nodeType === 1 &&
2397
+ child.getAttribute('aria-hidden') !== 'true') {
2398
+ read(child);
2399
+ }
2400
+ });
2401
+ };
2402
+ read(item);
2403
+ return text.trim().toLowerCase();
2404
+ }
2405
+ /**
2406
+ * The item typing `search` moves to, or none. A single character looks from
2407
+ * the item after the focused one, so typing the same letter again cycles
2408
+ * through the items that start with it, and so does a run of one letter
2409
+ * ("dd"). A longer search looks from the focused item, so typing more of
2410
+ * its own text keeps it.
2411
+ */
2412
+ function typeaheadMatch(items, search, current) {
2413
+ const repeated = Array.from(search).every(char => char === search[0]);
2414
+ const wanted = repeated ? search[0] : search;
2415
+ const start = current < 0 ? 0 : current + (wanted.length === 1 ? 1 : 0);
2416
+ for (let i = 0; i < items.length; i += 1) {
2417
+ const item = items[(start + i) % items.length];
2418
+ if (label(item).startsWith(wanted))
2419
+ return item;
2420
+ }
2421
+ return undefined;
2422
+ }
2423
+ /**
2424
+ * Moves through and activates the items of a `role="menu"` element from the
2425
+ * keyboard. Put the `onKeyDown` it returns on that element. It reads the
2426
+ * items from the element each time, so items that come and go are found,
2427
+ * and it renders nothing.
2428
+ *
2429
+ * It acts only while focus is on the menu element itself or on one of its
2430
+ * items, so anything else focusable inside the menu, such as a filter
2431
+ * field, keeps its own keys.
2432
+ *
2433
+ * - Down and Up move to the next and previous enabled item, wrapping, and
2434
+ * Home and End go to the first and last. From no item, Down goes to the
2435
+ * first and Up to the last.
2436
+ * - Enter and Space activate the focused item. A `<button>`, and a link with
2437
+ * an `href` for Enter, get the browser's own click, and anything else gets
2438
+ * one from here, so its `onClick` runs either way. A key held down
2439
+ * activates it once. A key handler on the item that called
2440
+ * `preventDefault()` has claimed the key, and nothing else runs it.
2441
+ * - A printable character moves to the next enabled item whose text starts
2442
+ * with what has been typed. Characters typed within `TYPEAHEAD_TIMEOUT` of
2443
+ * each other add up to one search, until `reset` starts a new one.
2444
+ *
2445
+ * Items are `MENU_ITEM_SELECTOR` matches, and disabled ones are skipped by
2446
+ * all of the above. Escape and Tab are left to the menu's owner, since what
2447
+ * closing means is up to it, and so is any other key.
2448
+ *
2449
+ * @function useMenuKeys
2450
+ * @returns The `keydown` handler for the menu element, and a way to forget
2451
+ * what has been typed.
2452
+ */
2453
+ function useMenuKeys() {
2454
+ const typedRef = React.useRef({ text: '' });
2455
+ React.useEffect(() => {
2456
+ const typed = typedRef.current;
2457
+ return () => clearTimeout(typed.timer);
2458
+ }, []);
2459
+ return React.useMemo(() => {
2460
+ const reset = () => {
2461
+ const typed = typedRef.current;
2462
+ clearTimeout(typed.timer);
2463
+ typed.text = '';
2464
+ };
2465
+ const onKeyDown = (event) => {
2466
+ const menu = event.currentTarget;
2467
+ const focused = getActiveElementInTree(menu);
2468
+ if (focused !== menu && !isItemOf(menu, focused))
2469
+ return;
2470
+ const items = enabledMenuItems(menu);
2471
+ if (!items.length)
2472
+ return;
2473
+ const current = items.indexOf(focused);
2474
+ switch (event.key) {
2475
+ case 'ArrowDown':
2476
+ event.preventDefault();
2477
+ items[current >= 0 ? (current + 1) % items.length : 0].focus();
2478
+ return;
2479
+ case 'ArrowUp':
2480
+ event.preventDefault();
2481
+ items[current >= 0
2482
+ ? (current - 1 + items.length) % items.length
2483
+ : items.length - 1].focus();
2484
+ return;
2485
+ case 'Home':
2486
+ event.preventDefault();
2487
+ items[0].focus();
2488
+ return;
2489
+ case 'End':
2490
+ event.preventDefault();
2491
+ items[items.length - 1].focus();
2492
+ return;
2493
+ case 'Enter':
2494
+ case ' ': {
2495
+ if (current < 0 || event.defaultPrevented)
2496
+ return;
2497
+ const item = items[current];
2498
+ if (activatesItself(item, event.key)) {
2499
+ // The browser clicks on Enter's keydown, and a held key sends one
2500
+ // per auto-repeat. Space clicks on keyup, once however long it's
2501
+ // held.
2502
+ if (event.repeat && event.key === 'Enter')
2503
+ event.preventDefault();
2504
+ return;
2505
+ }
2506
+ // Space would scroll the page, and a held key repeats.
2507
+ event.preventDefault();
2508
+ if (!event.repeat)
2509
+ item.click();
2510
+ return;
2511
+ }
2512
+ }
2513
+ if (event.key.length !== 1 ||
2514
+ event.ctrlKey ||
2515
+ event.metaKey ||
2516
+ event.altKey ||
2517
+ event.defaultPrevented) {
2518
+ return;
2519
+ }
2520
+ event.preventDefault();
2521
+ const typed = typedRef.current;
2522
+ typed.text += event.key.toLowerCase();
2523
+ clearTimeout(typed.timer);
2524
+ typed.timer = setTimeout(reset, TYPEAHEAD_TIMEOUT);
2525
+ typeaheadMatch(items, typed.text, current)?.focus();
2526
+ };
2527
+ return { onKeyDown, reset };
2528
+ }, []);
2529
+ }
2530
+
2355
2531
  /**
2356
2532
  * Checks if code is running in a browser environment.
2357
2533
  * @deprecated Dropdown's internal SSR guard, exported by accident; it will
@@ -2365,6 +2541,19 @@ const isBrowser$1 = (win, doc) => typeof win !== 'undefined' && typeof doc !== '
2365
2541
  /**
2366
2542
  * The `Dropdown` component provides Bulma's versatile dropdown menu for your Bulma React UI.
2367
2543
  *
2544
+ * Down, Enter and Space on the trigger open the menu on its first enabled
2545
+ * item, and Up opens it on its last. On the trigger of a menu that's already
2546
+ * open, Down and Up move focus to its first and last item. In the menu, the
2547
+ * arrow keys, Home and End move between the items, and Enter and Space run
2548
+ * the focused one the way a click does. Typing a letter moves to the next
2549
+ * item whose text starts with it, skipping disabled ones, and letters typed
2550
+ * in quick succession add up to one search. Typing searches once focus is in
2551
+ * the menu, so a letter typed on the trigger does nothing. Escape closes the
2552
+ * menu and focuses the trigger, and Tab closes it and lets focus move on.
2553
+ * Anything focusable in the menu that isn't an item, such as a field, keeps
2554
+ * its own keys apart from Escape and Tab. For a panel built around fields,
2555
+ * use `Popover`.
2556
+ *
2368
2557
  * @function
2369
2558
  * @param {DropdownProps} props - Props for the Dropdown component.
2370
2559
  * @param {React.Ref<HTMLDivElement>} ref - Forwarded ref to the root dropdown element.
@@ -2375,6 +2564,8 @@ const DropdownComponent = React.forwardRef(function DropdownComponent({ label, c
2375
2564
  const [active, setActive] = React.useState(!!activeProp);
2376
2565
  const dropdownRef = React.useRef(null);
2377
2566
  const triggerRef = React.useRef(null);
2567
+ const menuRef = React.useRef(null);
2568
+ const menuKeys = useMenuKeys();
2378
2569
  const pendingFocusRef = React.useRef(null);
2379
2570
  // Cleanup returned by a consumer's callback ref on attach, held until detach.
2380
2571
  const consumerCleanupRef = React.useRef(null);
@@ -2462,29 +2653,23 @@ const DropdownComponent = React.forwardRef(function DropdownComponent({ label, c
2462
2653
  }
2463
2654
  }
2464
2655
  };
2465
- const getMenuItems = () => {
2466
- /* istanbul ignore next: dropdownRef.current is never null once mounted */
2467
- if (!dropdownRef.current)
2468
- return [];
2469
- return Array.from(
2470
- // Every menu-item role, so an item a caller gives `menuitemcheckbox`
2471
- // or `menuitemradio` (a filter or sort menu) stays reachable.
2472
- dropdownRef.current.querySelectorAll('[role="menuitem"], [role="menuitemcheckbox"], [role="menuitemradio"]')).filter(el => !el.hasAttribute('disabled') &&
2473
- el.getAttribute('aria-disabled') !== 'true');
2656
+ const focusEdgeItem = (edge) => {
2657
+ const items = enabledMenuItems(menuRef.current);
2658
+ items[edge === 'first' ? 0 : items.length - 1]?.focus();
2474
2659
  };
2475
- // Focus the pending menu item once the menu becomes visible; the item is
2476
- // unfocusable while `display: none` applies, so this must wait for the
2477
- // `is-active` class to actually land in the DOM.
2660
+ // Once the menu shows, forget a search typed just before it last closed,
2661
+ // and focus the pending item. The item is unfocusable while
2662
+ // `display: none` applies, so this must wait for the `is-active` class to
2663
+ // actually land in the DOM.
2478
2664
  React.useEffect(() => {
2479
- if (!active || !pendingFocusRef.current)
2665
+ if (!active)
2480
2666
  return;
2481
- const items = getMenuItems();
2482
- if (items.length) {
2483
- const index = pendingFocusRef.current === 'first' ? 0 : items.length - 1;
2484
- items[index].focus();
2485
- }
2667
+ menuKeys.reset();
2668
+ if (!pendingFocusRef.current)
2669
+ return;
2670
+ focusEdgeItem(pendingFocusRef.current);
2486
2671
  pendingFocusRef.current = null;
2487
- }, [active]);
2672
+ }, [active, menuKeys]);
2488
2673
  const handleTriggerKeyDown = (e) => {
2489
2674
  if (disabled)
2490
2675
  return;
@@ -2497,9 +2682,7 @@ const DropdownComponent = React.forwardRef(function DropdownComponent({ label, c
2497
2682
  onActiveChange?.(true);
2498
2683
  }
2499
2684
  else {
2500
- const items = getMenuItems();
2501
- if (items.length)
2502
- items[0].focus();
2685
+ focusEdgeItem('first');
2503
2686
  pendingFocusRef.current = null;
2504
2687
  }
2505
2688
  break;
@@ -2511,9 +2694,7 @@ const DropdownComponent = React.forwardRef(function DropdownComponent({ label, c
2511
2694
  onActiveChange?.(true);
2512
2695
  }
2513
2696
  else {
2514
- const items = getMenuItems();
2515
- if (items.length)
2516
- items[items.length - 1].focus();
2697
+ focusEdgeItem('last');
2517
2698
  pendingFocusRef.current = null;
2518
2699
  }
2519
2700
  break;
@@ -2545,8 +2726,10 @@ const DropdownComponent = React.forwardRef(function DropdownComponent({ label, c
2545
2726
  }
2546
2727
  };
2547
2728
  const handleMenuKeyDown = (e) => {
2548
- // Escape and Tab close the menu regardless of whether it has any focusable
2549
- // items — an empty or all-disabled menu must still honor the close contract.
2729
+ // Closing is Dropdown's: Escape and Tab close the menu from anywhere in
2730
+ // it, a field included, and an empty or all-disabled menu too. Moving
2731
+ // between the items, running them and typeahead are the hook's, which
2732
+ // acts only while focus is on the menu or one of its items.
2550
2733
  if (e.key === 'Escape') {
2551
2734
  e.preventDefault();
2552
2735
  setActive(false);
@@ -2559,66 +2742,10 @@ const DropdownComponent = React.forwardRef(function DropdownComponent({ label, c
2559
2742
  onActiveChange?.(false);
2560
2743
  return;
2561
2744
  }
2562
- const items = getMenuItems();
2563
- if (!items.length)
2564
- return;
2565
- const currentIndex = items.indexOf(getActiveElementInTree(e.currentTarget));
2566
- switch (e.key) {
2567
- case 'ArrowDown': {
2568
- e.preventDefault();
2569
- const next = currentIndex >= 0 ? (currentIndex + 1) % items.length : 0;
2570
- items[next].focus();
2571
- break;
2572
- }
2573
- case 'ArrowUp': {
2574
- e.preventDefault();
2575
- const prev = currentIndex >= 0
2576
- ? (currentIndex - 1 + items.length) % items.length
2577
- : items.length - 1;
2578
- items[prev].focus();
2579
- break;
2580
- }
2581
- case 'Home':
2582
- e.preventDefault();
2583
- items[0].focus();
2584
- break;
2585
- case 'End':
2586
- e.preventDefault();
2587
- items[items.length - 1].focus();
2588
- break;
2589
- case 'Enter':
2590
- case ' ': {
2591
- // Activate the focused item by clicking it, so its `onClick` runs and
2592
- // closing follows `closeOnClick` as it does for the mouse. A caller's
2593
- // own key handler that prevented the default has claimed the key, and
2594
- // is left to it rather than followed by a second activation.
2595
- if (currentIndex < 0 || e.defaultPrevented)
2596
- break;
2597
- const item = items[currentIndex];
2598
- // Leave the browser's own activation alone, or the item runs twice: a
2599
- // `<button>` answers both keys, and a link with an `href` answers
2600
- // Enter. A link does not answer Space, so Space on a link is handled
2601
- // here too, which also keeps the page from scrolling.
2602
- if (item.tagName === 'BUTTON')
2603
- break;
2604
- if (e.key === 'Enter' &&
2605
- item.tagName === 'A' &&
2606
- item.hasAttribute('href')) {
2607
- break;
2608
- }
2609
- e.preventDefault();
2610
- // A held key sends a keydown per auto-repeat. Clicking on each would
2611
- // toggle a checkbox item over and over while `closeOnClick` is off,
2612
- // so only the first press activates. The default is still prevented
2613
- // on the repeats above, so a held Space does not scroll the page.
2614
- if (!e.repeat)
2615
- item.click();
2616
- break;
2617
- }
2618
- }
2745
+ menuKeys.onKeyDown(e);
2619
2746
  };
2620
2747
  const dropdownClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
2621
- return (jsxRuntime.jsxs("div", { className: dropdownClasses, ref: setRefs, id: id, "data-testid": "dropdown-root", ...rest, children: [jsxRuntime.jsx("div", { className: usePrefixedClassNames('dropdown-trigger'), children: jsxRuntime.jsxs("button", { ref: triggerRef, className: buttonClass, "aria-haspopup": "true", "aria-controls": id ? `${id}-menu` : undefined, "aria-expanded": active, onClick: handleToggle, onKeyDown: handleTriggerKeyDown, disabled: disabled, type: "button", children: [jsxRuntime.jsx("span", { children: label }), jsxRuntime.jsx("span", { className: usePrefixedClassNames('icon', 'is-small'), "aria-hidden": "true", children: jsxRuntime.jsx("i", { className: "fas fa-angle-down" }) })] }) }), jsxRuntime.jsx("div", { className: classNames(usePrefixedClassNames('dropdown-menu'), menuClassName), id: id ? `${id}-menu` : undefined, role: "menu", "data-testid": "dropdown-menu", onKeyDown: handleMenuKeyDown,
2748
+ return (jsxRuntime.jsxs("div", { className: dropdownClasses, ref: setRefs, id: id, "data-testid": "dropdown-root", ...rest, children: [jsxRuntime.jsx("div", { className: usePrefixedClassNames('dropdown-trigger'), children: jsxRuntime.jsxs("button", { ref: triggerRef, className: buttonClass, "aria-haspopup": "true", "aria-controls": id ? `${id}-menu` : undefined, "aria-expanded": active, onClick: handleToggle, onKeyDown: handleTriggerKeyDown, disabled: disabled, type: "button", children: [jsxRuntime.jsx("span", { children: label }), jsxRuntime.jsx("span", { className: usePrefixedClassNames('icon', 'is-small'), "aria-hidden": "true", children: jsxRuntime.jsx("i", { className: "fas fa-angle-down" }) })] }) }), jsxRuntime.jsx("div", { className: classNames(usePrefixedClassNames('dropdown-menu'), menuClassName), id: id ? `${id}-menu` : undefined, ref: menuRef, role: "menu", "data-testid": "dropdown-menu", onKeyDown: handleMenuKeyDown,
2622
2749
  // The menu, not its content, takes focus from a click between items,
2623
2750
  // so the menu owns nothing focusable but its items.
2624
2751
  tabIndex: -1, children: jsxRuntime.jsx("div", { className: usePrefixedClassNames('dropdown-content'), onClick: handleMenuClick, children: children }) })] }));
@@ -4518,47 +4645,402 @@ const Tabs = withSubComponents(TabsComponent, {
4518
4645
  Content: TabsContent,
4519
4646
  }, 'Tabs');
4520
4647
 
4648
+ const validButtonColors = [...validColors, 'text', 'ghost'];
4521
4649
  /**
4522
- * The `Loading` component provides a loading overlay with a spinner animation.
4650
+ * The `Button` component provides a flexible and highly customizable button for your Bulma React UI.
4523
4651
  *
4524
4652
  * @function
4525
- * @param {LoadingProps} props - Props for the Loading component.
4526
- * @returns {JSX.Element | null} The rendered loading overlay or null if not active.
4527
- *
4528
- * @example
4529
- * // Full page loading
4530
- * <Loading active isFullPage />
4531
- *
4532
- * @example
4533
- * // Container loading with message
4534
- * <div style={{ position: 'relative', height: '200px' }}>
4535
- * <Loading active>Loading data...</Loading>
4536
- * </div>
4537
- *
4538
- * @example
4539
- * // With color variant
4540
- * <Loading active color="primary" />
4541
- *
4542
- * @example
4543
- * // With cancel functionality
4544
- * <Loading
4545
- * active={isLoading}
4546
- * canCancel
4547
- * onCancel={() => setIsLoading(false)}
4548
- * />
4653
+ * @param {ButtonProps} props - Props for the Button component.
4654
+ * @param {React.Ref} ref - Forwarded ref to the element `as` renders.
4655
+ * @returns {JSX.Element} The rendered button, anchor, or custom element.
4656
+ * @see {@link https://bulma.io/documentation/elements/button/ | Bulma Button documentation}
4549
4657
  */
4550
- const Loading = ({ active = false, isFullPage = false, size, color, canCancel = false, onCancel, children, className, overlayClassName, iconClassName, indicator, overlay, ...props }) => {
4551
- const { bulmaHelperClasses, rest } = useBulmaClasses(props);
4552
- // Generate Bulma classes with prefix
4553
- const loadingClasses = usePrefixedClassNames('loading', {
4554
- 'is-active': active,
4555
- 'is-full-page': isFullPage,
4556
- 'is-cancelable': canCancel,
4557
- [`is-${overlay}`]: !!overlay,
4558
- [`is-${color}`]: !!color,
4658
+ const Button = React.forwardRef(function Button(props, ref) {
4659
+ const { color, size, isLight, isRounded, isLoading, isStatic, isFullwidth, isFullWidth, isOutlined, isInverted, isFocused, isActive, isHovered, isDisabled, className, children, textColor, bgColor, as: Component = 'button', href, onClick, target, rel, ...bulmaProps } = props;
4660
+ const { bulmaHelperClasses, rest } = useBulmaClasses({
4661
+ color: textColor,
4662
+ backgroundColor: bgColor,
4663
+ ...bulmaProps,
4559
4664
  });
4560
- const iconClasses = usePrefixedClassNames('loading-icon', {
4561
- [`is-${size}`]: size,
4665
+ // Generate Bulma classes with prefix
4666
+ const bulmaClasses = usePrefixedClassNames('button', {
4667
+ [`is-${color}`]: color &&
4668
+ validButtonColors.includes(color),
4669
+ [`is-${size}`]: size,
4670
+ 'is-outlined': isOutlined,
4671
+ 'is-light': isLight,
4672
+ 'is-loading': isLoading,
4673
+ 'is-static': isStatic,
4674
+ 'is-disabled': isDisabled,
4675
+ 'is-rounded': isRounded,
4676
+ 'is-hovered': isHovered,
4677
+ 'is-focused': isFocused,
4678
+ 'is-active': isActive,
4679
+ 'is-inverted': isInverted,
4680
+ 'is-fullwidth': isFullwidth ?? isFullWidth,
4681
+ });
4682
+ // Combine prefixed Bulma classes with unprefixed user className and prefixed helper classes
4683
+ const buttonClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
4684
+ if (Component !== 'button') {
4685
+ // Strip the form-control attributes a link-like element cannot carry, so a
4686
+ // JavaScript consumer or a spread object doesn't put them on the DOM. The
4687
+ // types no longer allow them through; this is the runtime backstop.
4688
+ //
4689
+ // Only for INTRINSIC tags. A custom component (a router Link, ...) owns its
4690
+ // prop contract, and `ComponentPropsWithoutRef<T>` promises the caller that
4691
+ // its props reach it. Stripping names by their spelling alone broke that
4692
+ // promise silently: `<Button as={Custom} name="x" />` type-checked while
4693
+ // `Custom` never received the `name` it requires.
4694
+ //
4695
+ // `autoFocus` and `type` are deliberately NOT stripped: both are valid on
4696
+ // targets this list was filtering them from. `autoFocus` is a global
4697
+ // attribute (React decides per element whether to honour it, which is not
4698
+ // ours to pre-empt), and `type` on an anchor is the MIME hint that
4699
+ // `ButtonProps<'a'>` now types it as.
4700
+ //
4701
+ // The types already answer this for a TypeScript caller: `ButtonProps<T>`
4702
+ // derives from `ComponentPropsWithoutRef<T>`, so only what the target
4703
+ // accepts can be passed. What is left is a runtime backstop for JavaScript
4704
+ // consumers and spread objects.
4705
+ //
4706
+ // Keep it to the two cases where a stray attribute does something. The
4707
+ // submit-overrides belong to a submit control and never make sense on the
4708
+ // anchor path. `disabled` is the one that is VISIBLE: Bulma styles
4709
+ // `.button[disabled]` (background, border, shadow, opacity), so letting it
4710
+ // through to a `<span>` greys the element out — main stripped it, and not
4711
+ // stripping it would be a silent visual change on a types-only release.
4712
+ //
4713
+ // Everything else (`name`, `value`, `form`) is inert where it does not
4714
+ // belong, and enumerating which tags own it is what kept going wrong:
4715
+ // first all nine names on every non-button tag (so `<Button as="input"
4716
+ // name="query" value="Search">` submitted nothing), then a form-control
4717
+ // allowlist that still lost `formAction` on an input and `value` on an
4718
+ // option. Those now ride on the types instead.
4719
+ //
4720
+ // A custom component is exempt entirely — it owns its prop contract, and
4721
+ // `ComponentPropsWithoutRef<T>` promises the caller its props arrive.
4722
+ //
4723
+ // The elements that own `disabled`, per the HTML spec. A closed set, unlike
4724
+ // the open-ended question of which tag owns which attribute.
4725
+ const SUBMIT_OVERRIDES = [
4726
+ 'formAction',
4727
+ 'formEncType',
4728
+ 'formMethod',
4729
+ 'formNoValidate',
4730
+ 'formTarget',
4731
+ ];
4732
+ const OWNS_DISABLED = [
4733
+ 'button',
4734
+ 'fieldset',
4735
+ 'input',
4736
+ 'optgroup',
4737
+ 'option',
4738
+ 'select',
4739
+ 'textarea',
4740
+ ];
4741
+ // Two independent filters. They are not exclusive — an `<a>` owns neither
4742
+ // the submit-overrides nor `disabled`, so it takes both — and deriving them
4743
+ // from one destructure, or from one if/else, is what broke each of them in
4744
+ // turn: first the overrides came off every intrinsic (an `as="input"` lost
4745
+ // `formAction`), then scoping them to the anchor let `disabled` back onto
4746
+ // it.
4747
+ const forwardedRest = { ...rest };
4748
+ if (typeof Component === 'string' && !isCustomElement(Component)) {
4749
+ // Meaningless anywhere but a submit control, and an anchor is not one.
4750
+ if (Component === 'a') {
4751
+ for (const key of SUBMIT_OVERRIDES)
4752
+ delete forwardedRest[key];
4753
+ }
4754
+ // Visible via Bulma's `.button[disabled]`, so withheld from any element
4755
+ // that does not own it.
4756
+ if (!OWNS_DISABLED.includes(Component))
4757
+ delete forwardedRest.disabled;
4758
+ }
4759
+ return (jsxRuntime.jsx(Component, { ref: ref, className: buttonClasses, href: href, target: target, rel: rel, "aria-disabled": isDisabled, tabIndex: isDisabled ? -1 : undefined, onClick: isDisabled
4760
+ ? // Guarded, because this handler is installed into whatever `as`
4761
+ // renders and a custom component's `onClick` need not be a DOM
4762
+ // event handler at all. One declaring `onClick: (v: string) =>
4763
+ // void` calls this with a string, and an unguarded
4764
+ // `e.preventDefault()` throws `is not a function`. Blocking the
4765
+ // real event is the point; a foreign payload is simply not ours
4766
+ // to act on.
4767
+ (e) => {
4768
+ if (typeof e?.preventDefault === 'function')
4769
+ e.preventDefault();
4770
+ }
4771
+ : onClick, ...forwardedRest, children: children }));
4772
+ }
4773
+ return (jsxRuntime.jsx("button", { ref: ref, className: buttonClasses, disabled: isDisabled, onClick: onClick, ...rest, children: children }));
4774
+ });
4775
+ Button.displayName = 'Button';
4776
+
4777
+ // INTERNAL: deliberately not exported from src/index.ts.
4778
+ /**
4779
+ * `useLayoutEffect` in the browser, `useEffect` on the server. A server
4780
+ * render runs neither, and React 18 warns about a layout effect there, so the
4781
+ * server is handed the plain one.
4782
+ */
4783
+ const useClientLayoutEffect = typeof document === 'undefined' ? React.useEffect : React.useLayoutEffect;
4784
+
4785
+ // Not exported, so it stays out of the public API that `index.ts` re-exports.
4786
+ const SegmentedControlContext = React.createContext(null);
4787
+ /** Sorts mounted segments into the order they sit in the page. */
4788
+ const inDocumentOrder = (a, b) => a.node.compareDocumentPosition(b.node) & Node.DOCUMENT_POSITION_FOLLOWING
4789
+ ? -1
4790
+ : 1;
4791
+ /** How far each arrow key moves along the row. */
4792
+ const STEPS = {
4793
+ ArrowRight: 1,
4794
+ ArrowDown: 1,
4795
+ ArrowLeft: -1,
4796
+ ArrowUp: -1,
4797
+ };
4798
+ /**
4799
+ * The segment a key moves to, found among the segments of the same group in
4800
+ * DOM order, skipping disabled ones and wrapping at either end. `undefined`
4801
+ * for a key the group leaves alone, when no enabled segment is left, or when
4802
+ * the segment isn't inside its row in the page, as a portaled one isn't.
4803
+ */
4804
+ function segmentFor(current, key) {
4805
+ const group = current.closest('[role="radiogroup"]');
4806
+ if (!group)
4807
+ return undefined;
4808
+ const segments = Array.from(group.querySelectorAll('[role="radio"]'));
4809
+ const enabled = segments.filter(segment => !segment.disabled);
4810
+ if (key === 'Home')
4811
+ return enabled[0];
4812
+ if (key === 'End')
4813
+ return enabled[enabled.length - 1];
4814
+ const step = STEPS[key];
4815
+ if (!step)
4816
+ return undefined;
4817
+ const at = segments.indexOf(current);
4818
+ for (let i = 1; i <= segments.length; i += 1) {
4819
+ const candidate = segments[(at + step * i + segments.length) % segments.length];
4820
+ if (!candidate.disabled)
4821
+ return candidate;
4822
+ }
4823
+ return undefined;
4824
+ }
4825
+ /**
4826
+ * The `SegmentedControl` component renders a row of buttons where only one can be on, and tells a screen reader it's a radio group.
4827
+ *
4828
+ * It's Bulma's `.buttons.has-addons` row, with the checked segment filled and
4829
+ * marked `is-selected`, the way Bulma's button docs show it. To a screen
4830
+ * reader it's `role="radiogroup"`, with `role="radio"` and `aria-checked` on
4831
+ * each segment, which is how the APG toolbar example marks its alignment
4832
+ * buttons. The keys follow the APG radio group: the row is one tab stop, the
4833
+ * arrow keys move to the next or previous enabled segment and check it,
4834
+ * wrapping at either end, Home and End go to the first and last, and Space
4835
+ * checks the focused segment. Once a segment is checked, one always is.
4836
+ *
4837
+ * It carries no form value. A choice that's sent with a form is `Radios`.
4838
+ *
4839
+ * @function
4840
+ * @param {SegmentedControlProps} props - Props for the SegmentedControl component.
4841
+ * @param {React.Ref<HTMLDivElement>} ref - Forwarded ref to the row's `<div>`.
4842
+ * @returns {JSX.Element} The rendered segmented control.
4843
+ * @see {@link https://www.w3.org/WAI/ARIA/apg/patterns/radio/ | APG radio group pattern}
4844
+ * @see {@link https://bulma.io/documentation/elements/button/#list-of-buttons | Bulma list of buttons}
4845
+ *
4846
+ * @example
4847
+ * <SegmentedControl aria-label="Text alignment" defaultValue="left">
4848
+ * <SegmentedControl.Segment value="left">Left</SegmentedControl.Segment>
4849
+ * <SegmentedControl.Segment value="center">Center</SegmentedControl.Segment>
4850
+ * <SegmentedControl.Segment value="right">Right</SegmentedControl.Segment>
4851
+ * </SegmentedControl>
4852
+ */
4853
+ const SegmentedControlRoot = React.forwardRef(function SegmentedControl({ value, defaultValue, onChange, color = 'link', size, hasAddons = true, isCentered, isRight, className, children, ...props }, ref) {
4854
+ const { bulmaHelperClasses, rest } = useBulmaClasses({ ...props });
4855
+ // Controlled vs uncontrolled, as in Tabs and Collapses.
4856
+ const isControlled = value !== undefined;
4857
+ const [internalValue, setInternalValue] = React.useState(defaultValue);
4858
+ const checked = isControlled ? (value ?? undefined) : internalValue;
4859
+ const select = (next) => {
4860
+ if (next === checked)
4861
+ return;
4862
+ if (!isControlled)
4863
+ setInternalValue(next);
4864
+ onChange?.(next);
4865
+ };
4866
+ // The segments mount and register from an effect, so the group knows
4867
+ // which can hold the tab stop wherever in the tree they sit.
4868
+ const [segments, setSegments] = React.useState([]);
4869
+ const register = React.useCallback((record) => {
4870
+ setSegments(prev => [...prev, record]);
4871
+ return () => setSegments(prev => prev.filter(r => r !== record));
4872
+ }, []);
4873
+ const [focused, setFocused] = React.useState();
4874
+ // The first enabled segment in the page, sorted when it's read rather
4875
+ // than when a segment registers, so reordered segments are seen too. It's
4876
+ // read after the commit: a render runs before React moves the nodes, so
4877
+ // sorting there would find the old order.
4878
+ const [firstEnabled, setFirstEnabled] = React.useState();
4879
+ useClientLayoutEffect(() => {
4880
+ const inPage = segments.filter(s => !s.disabled).sort(inDocumentOrder);
4881
+ setFirstEnabled(inPage[0]?.value);
4882
+ }, [segments, children]);
4883
+ // One tab stop for the row (roving tabindex). While focus is in the row
4884
+ // the stop follows it, so Tab leaves from wherever focus is even when a
4885
+ // controlled group hasn't taken the new value. Otherwise it's the checked
4886
+ // segment, or the first enabled one when none is checked or the checked
4887
+ // one is disabled. Before the segments register (the server render and
4888
+ // the first client render) the checked one holds it, so a row with
4889
+ // nothing checked has no stop until the client has rendered.
4890
+ const canHoldStop = (candidate) => segments.some(s => !s.disabled && s.value === candidate);
4891
+ let tabStop;
4892
+ if (canHoldStop(focused))
4893
+ tabStop = focused;
4894
+ else if (segments.length === 0 || canHoldStop(checked))
4895
+ tabStop = checked;
4896
+ else
4897
+ tabStop = firstEnabled;
4898
+ const named = Boolean(rest['aria-label'] || rest['aria-labelledby']);
4899
+ React.useEffect(() => {
4900
+ if (!named) {
4901
+ warnOnce('SegmentedControl:unnamed', '[bestax-bulma] <SegmentedControl> has no accessible name. Pass ' +
4902
+ 'aria-label or aria-labelledby, so a screen reader can say what ' +
4903
+ 'the choice is.');
4904
+ }
4905
+ }, [named]);
4906
+ const rowClasses = usePrefixedClassNames('buttons', {
4907
+ 'has-addons': hasAddons,
4908
+ 'is-centered': isCentered,
4909
+ 'is-right': isRight,
4910
+ [`are-${size}`]: size,
4911
+ });
4912
+ // The props are spread first, so the role a screen reader relies on wins
4913
+ // over anything a JavaScript caller or a spread object passes along.
4914
+ return (jsxRuntime.jsx(SegmentedControlContext.Provider, { value: { checked, tabStop, color, select, setFocused, register }, children: jsxRuntime.jsx("div", { ...rest, ref: ref, role: "radiogroup", className: classNames(rowClasses, bulmaHelperClasses, className), children: children }) }));
4915
+ });
4916
+ /**
4917
+ * One option in the row: a `<button role="radio">` that's checked while the group's value is its `value`, filled with the group's `color` and marked `is-selected`.
4918
+ *
4919
+ * @function
4920
+ * @param {SegmentedControlSegmentProps} props - Props for the segment.
4921
+ * @param {React.Ref<HTMLButtonElement>} ref - Forwarded ref to the `<button>`.
4922
+ * @returns {JSX.Element} The rendered segment.
4923
+ */
4924
+ const Segment = React.forwardRef(function Segment({ value, isDisabled, disabled, className, onClick, onKeyDown, onFocus, onBlur, ...props }, ref) {
4925
+ const ctx = React.useContext(SegmentedControlContext);
4926
+ const checked = ctx !== null && ctx.checked === value;
4927
+ const off = Boolean(isDisabled || disabled);
4928
+ // Prefixed like Button's own classes, which a hand-written
4929
+ // `className="is-selected"` could not be under ConfigProvider classPrefix.
4930
+ // Bulma lifts `is-selected` above its neighbours in `.buttons.has-addons`.
4931
+ const selectedClass = usePrefixedClassNames({ 'is-selected': checked });
4932
+ const nodeRef = React.useRef(null);
4933
+ const setRefs = React.useCallback((node) => {
4934
+ nodeRef.current = node;
4935
+ if (typeof ref === 'function')
4936
+ ref(node);
4937
+ else if (ref)
4938
+ ref.current = node;
4939
+ }, [ref]);
4940
+ const register = ctx?.register;
4941
+ React.useEffect(() => {
4942
+ if (!register) {
4943
+ warnOnce('SegmentedControl.Segment:outside', '[bestax-bulma] <SegmentedControl.Segment> is outside a ' +
4944
+ '<SegmentedControl>, so nothing can check it. Put it inside one.');
4945
+ return undefined;
4946
+ }
4947
+ return register({
4948
+ value,
4949
+ disabled: off,
4950
+ node: nodeRef.current,
4951
+ });
4952
+ }, [register, value, off]);
4953
+ // The caller's onClick runs first, so a `preventDefault()` there keeps
4954
+ // the segment from being checked. A disabled button gets no click.
4955
+ const handleClick = (event) => {
4956
+ onClick?.(event);
4957
+ if (event.defaultPrevented)
4958
+ return;
4959
+ ctx?.select(value);
4960
+ };
4961
+ const handleKeyDown = (event) => {
4962
+ onKeyDown?.(event);
4963
+ if (!ctx ||
4964
+ event.defaultPrevented ||
4965
+ event.altKey ||
4966
+ event.ctrlKey ||
4967
+ event.metaKey) {
4968
+ return;
4969
+ }
4970
+ const target = segmentFor(event.currentTarget, event.key);
4971
+ if (!target)
4972
+ return;
4973
+ event.preventDefault();
4974
+ // The row has taken this key, so nothing above it acts on it too: a
4975
+ // Carousel moves its slide on the arrows from a document listener that
4976
+ // doesn't read `defaultPrevented`. Keys the row leaves alone still bubble.
4977
+ event.stopPropagation();
4978
+ target.focus();
4979
+ // The check follows focus, APG's rule for a radio group outside a
4980
+ // toolbar. It goes through a click on the segment, as a native radio's
4981
+ // does, so its onClick sees it. Inside a toolbar the arrows would move
4982
+ // focus alone; that is the line a toolbar would change.
4983
+ target.click();
4984
+ };
4985
+ const handleFocus = (event) => {
4986
+ onFocus?.(event);
4987
+ ctx?.setFocused(value);
4988
+ };
4989
+ // The segment focus moves to, if any, records itself straight after.
4990
+ const handleBlur = (event) => {
4991
+ onBlur?.(event);
4992
+ ctx?.setFocused(undefined);
4993
+ };
4994
+ // The props are spread first, so what a radio depends on wins over
4995
+ // anything a JavaScript caller or a spread object passes along: it stays
4996
+ // a button, never submits, reports whether it's checked, and the group
4997
+ // decides whether Tab reaches it.
4998
+ return (jsxRuntime.jsx(Button, { ...props, ref: setRefs, as: "button", type: "button", role: "radio", "aria-checked": checked, tabIndex: ctx ? (ctx.tabStop === value ? 0 : -1) : undefined, isDisabled: off, color: checked ? ctx.color : undefined, className: classNames(selectedClass, className), onClick: handleClick, onKeyDown: handleKeyDown, onFocus: handleFocus, onBlur: handleBlur }));
4999
+ });
5000
+ Segment.displayName = 'SegmentedControl.Segment';
5001
+ const SegmentedControl = withSubComponents(SegmentedControlRoot, { Segment }, 'SegmentedControl');
5002
+
5003
+ /**
5004
+ * The `Loading` component provides a loading overlay with a spinner animation.
5005
+ *
5006
+ * @function
5007
+ * @param {LoadingProps} props - Props for the Loading component.
5008
+ * @returns {JSX.Element | null} The rendered loading overlay or null if not active.
5009
+ *
5010
+ * @example
5011
+ * // Full page loading
5012
+ * <Loading active isFullPage />
5013
+ *
5014
+ * @example
5015
+ * // Container loading with message
5016
+ * <div style={{ position: 'relative', height: '200px' }}>
5017
+ * <Loading active>Loading data...</Loading>
5018
+ * </div>
5019
+ *
5020
+ * @example
5021
+ * // With color variant
5022
+ * <Loading active color="primary" />
5023
+ *
5024
+ * @example
5025
+ * // With cancel functionality
5026
+ * <Loading
5027
+ * active={isLoading}
5028
+ * canCancel
5029
+ * onCancel={() => setIsLoading(false)}
5030
+ * />
5031
+ */
5032
+ const Loading = ({ active = false, isFullPage = false, size, color, canCancel = false, onCancel, children, className, overlayClassName, iconClassName, indicator, overlay, ...props }) => {
5033
+ const { bulmaHelperClasses, rest } = useBulmaClasses(props);
5034
+ // Generate Bulma classes with prefix
5035
+ const loadingClasses = usePrefixedClassNames('loading', {
5036
+ 'is-active': active,
5037
+ 'is-full-page': isFullPage,
5038
+ 'is-cancelable': canCancel,
5039
+ [`is-${overlay}`]: !!overlay,
5040
+ [`is-${color}`]: !!color,
5041
+ });
5042
+ const iconClasses = usePrefixedClassNames('loading-icon', {
5043
+ [`is-${size}`]: size,
4562
5044
  });
4563
5045
  // Combine classes
4564
5046
  const combinedClasses = classNames(loadingClasses, bulmaHelperClasses, className);
@@ -4895,14 +5377,6 @@ const CollapsesComponent = ({ multiple = false, value, defaultValue, onChange, s
4895
5377
  };
4896
5378
  const Collapses = withSubComponents(CollapsesComponent, { Collapse }, 'Collapses');
4897
5379
 
4898
- // INTERNAL: deliberately not exported from src/index.ts.
4899
- /**
4900
- * `useLayoutEffect` in the browser, `useEffect` on the server. A server
4901
- * render runs neither, and React 18 warns about a layout effect there, so the
4902
- * server is handed the plain one.
4903
- */
4904
- const useClientLayoutEffect = typeof document === 'undefined' ? React.useEffect : React.useLayoutEffect;
4905
-
4906
5380
  // INTERNAL: deliberately not exported from src/index.ts.
4907
5381
  /**
4908
5382
  * Runs the caller's own handler on the element that holds the trigger, then
@@ -5320,174 +5794,6 @@ function useHoverOpen(options) {
5320
5794
  }, [settle, ask]);
5321
5795
  }
5322
5796
 
5323
- /**
5324
- * The `Tooltip` component displays helpful information when users hover over or focus on an element.
5325
- *
5326
- * Hovering the trigger or giving it keyboard focus shows it. Touch doesn't,
5327
- * unless `openOnTap` is on, and neither does focus a click or a tap brings,
5328
- * so tapping the trigger just does what the trigger does. The pointer can
5329
- * move onto a shown tooltip. Escape hides it wherever focus is, without
5330
- * moving the pointer or focus, and it stays hidden until one of them leaves
5331
- * the trigger and comes back, or, with `openOnTap`, until the next tap on
5332
- * the trigger. The key always goes on too, to whatever has focus or a
5333
- * `Modal` or `Popover` around it, since a tooltip only adds to what's
5334
- * there.
5335
- *
5336
- * @function
5337
- * @param {TooltipProps} props - Props for the Tooltip component.
5338
- * @returns {JSX.Element} The rendered tooltip wrapper.
5339
- *
5340
- * @example
5341
- * // Basic tooltip
5342
- * <Tooltip label="This is helpful info">
5343
- * <button>Hover me</button>
5344
- * </Tooltip>
5345
- *
5346
- * @example
5347
- * // Positioned tooltip with color
5348
- * <Tooltip label="Delete item" position="right" color="danger">
5349
- * <Icon icon="fas fa-trash" />
5350
- * </Tooltip>
5351
- *
5352
- * @example
5353
- * // Multiline tooltip
5354
- * <Tooltip label="This is a longer tooltip that wraps to multiple lines" multiline>
5355
- * <span>?</span>
5356
- * </Tooltip>
5357
- */
5358
- const Tooltip = ({ label, content, position = 'top', color, size, active: alwaysActive, multiline = false, animated = true, square = false, dashed = false, delay = 0, closeDelay = 0, openOnTap = false, children, className, tooltipClassName, ...props }) => {
5359
- const { bulmaHelperClasses, rest } = useBulmaClasses(props);
5360
- const { onPointerEnter, onPointerOver, onPointerLeave, onPointerDown, onPointerUp, onFocus, onBlur, ...attrs } = rest;
5361
- const [isVisible, setIsVisible] = React.useState(false);
5362
- const [resolvedPosition, setResolvedPosition] = React.useState(position === 'auto' ? 'top' : position);
5363
- const [contentStyle, setContentStyle] = React.useState({});
5364
- const wrapperRef = React.useRef(null);
5365
- const contentRef = React.useRef(null);
5366
- const hover = useHoverOpen({
5367
- // The pointer and focus as they are. `active` shows it on top of that, so
5368
- // dropping `active` leaves it shown while the pointer or focus holds it.
5369
- // An ask to close can come while it's already hidden, which is no change.
5370
- open: isVisible,
5371
- onOpenChange: setIsVisible,
5372
- openDelay: delay,
5373
- closeDelay,
5374
- // The wrapper stands in for the trigger. It holds only the children and
5375
- // the tooltip, and nothing in the tooltip takes focus, so focus anywhere
5376
- // in it is focus on the trigger.
5377
- triggerRef: wrapperRef,
5378
- floatingRef: contentRef,
5379
- // Escape hides it and carries on, so a control in the trigger, or a
5380
- // Modal around it, still gets the key.
5381
- consumeEscape: false,
5382
- openOnTap,
5383
- });
5384
- // Auto-placement and overflow correction
5385
- React.useLayoutEffect(() => {
5386
- const isAuto = position === 'auto';
5387
- if (!isAuto) {
5388
- // eslint-disable-next-line react-hooks/set-state-in-effect -- layout measurement effect resolves placement
5389
- setResolvedPosition(position);
5390
- }
5391
- const showing = isVisible || alwaysActive;
5392
- if (!showing) {
5393
- setContentStyle({});
5394
- return;
5395
- }
5396
- const wrapper = wrapperRef.current;
5397
- const content = contentRef.current;
5398
- if (!wrapper || !content)
5399
- return;
5400
- const wrapperRect = wrapper.getBoundingClientRect();
5401
- const margin = 40;
5402
- const edgePadding = 8; // minimum distance from viewport edge
5403
- // Step 1: determine position (auto mode only)
5404
- if (isAuto) {
5405
- const spaceTop = wrapperRect.top;
5406
- const spaceBottom = window.innerHeight - wrapperRect.bottom;
5407
- if (spaceTop >= margin) {
5408
- setResolvedPosition('top');
5409
- }
5410
- else if (spaceBottom >= margin) {
5411
- setResolvedPosition('bottom');
5412
- }
5413
- else if (window.innerWidth - wrapperRect.right >= wrapperRect.left) {
5414
- setResolvedPosition('right');
5415
- }
5416
- else {
5417
- setResolvedPosition('left');
5418
- }
5419
- }
5420
- // Step 2: measure tooltip content and correct overflow.
5421
- // Use requestAnimationFrame so the resolved position class is applied first.
5422
- requestAnimationFrame(() => {
5423
- const contentEl = contentRef.current;
5424
- if (!contentEl)
5425
- return;
5426
- const contentRect = contentEl.getBoundingClientRect();
5427
- const style = {};
5428
- // For top/bottom positions, tooltip is centered horizontally via CSS
5429
- // (left: 50%; transform: translateX(-50%)).
5430
- // Check if it overflows left or right and apply a corrective shift.
5431
- const pos = isAuto ? undefined : position; // read from state won't be updated yet in auto
5432
- const effectivePos = pos || 'top'; // fallback, actual is set by state
5433
- const isVertical = effectivePos === 'top' ||
5434
- effectivePos === 'bottom' ||
5435
- resolvedPosition === 'top' ||
5436
- resolvedPosition === 'bottom';
5437
- if (isVertical) {
5438
- if (contentRect.left < edgePadding) {
5439
- // Overflows left — shift right
5440
- const shift = edgePadding - contentRect.left;
5441
- style.left = `calc(50% + ${shift}px)`;
5442
- style['--tooltip-arrow-offset'] =
5443
- `${-shift}px`;
5444
- }
5445
- else if (contentRect.right > window.innerWidth - edgePadding) {
5446
- // Overflows right — shift left
5447
- const shift = contentRect.right - (window.innerWidth - edgePadding);
5448
- style.left = `calc(50% - ${shift}px)`;
5449
- style['--tooltip-arrow-offset'] =
5450
- `${shift}px`;
5451
- }
5452
- }
5453
- else {
5454
- // For left/right positions, tooltip is centered vertically via CSS
5455
- // (top: 50%; transform: translateY(-50%)).
5456
- if (contentRect.top < edgePadding) {
5457
- const shift = edgePadding - contentRect.top;
5458
- style.top = `calc(50% + ${shift}px)`;
5459
- style['--tooltip-arrow-offset'] =
5460
- `${-shift}px`;
5461
- }
5462
- else if (contentRect.bottom > window.innerHeight - edgePadding) {
5463
- const shift = contentRect.bottom - (window.innerHeight - edgePadding);
5464
- style.top = `calc(50% - ${shift}px)`;
5465
- style['--tooltip-arrow-offset'] =
5466
- `${shift}px`;
5467
- }
5468
- }
5469
- setContentStyle(style);
5470
- });
5471
- }, [position, resolvedPosition, isVisible, alwaysActive, label, content]);
5472
- // Generate Bulma classes
5473
- const tooltipClasses = usePrefixedClassNames('tooltip', {
5474
- 'is-active': alwaysActive || isVisible,
5475
- // Open by the pointer, focus or a tap, rather than shown by `active`
5476
- // alone: only then does the tooltip, and the gap to it, take the pointer.
5477
- 'is-open': isVisible,
5478
- [`is-${resolvedPosition}`]: resolvedPosition,
5479
- [`is-${color}`]: color,
5480
- [`is-${size}`]: size,
5481
- 'is-multiline': multiline,
5482
- 'is-animated': animated,
5483
- 'is-square': square,
5484
- 'is-dashed': dashed,
5485
- });
5486
- const combinedClasses = classNames(tooltipClasses, bulmaHelperClasses, className);
5487
- const combinedTooltipClasses = usePrefixedClassNames('tooltip-content', tooltipClassName);
5488
- return (jsxRuntime.jsxs("span", { ref: wrapperRef, className: combinedClasses, onPointerEnter: chain$1(onPointerEnter, hover.onPointerEnter), onPointerOver: chain$1(onPointerOver, hover.onPointerOver), onPointerLeave: chain$1(onPointerLeave, hover.onPointerLeave), onFocus: chain$1(onFocus, hover.onFocus), onBlur: chain$1(onBlur, hover.onBlur), onPointerDown: chain$1(onPointerDown, hover.onPointerDown), onPointerUp: chain$1(onPointerUp, hover.onPointerUp), ...(label ? { 'data-tooltip': label } : {}), ...attrs, children: [children, jsxRuntime.jsx("span", { ref: contentRef, className: combinedTooltipClasses, role: "tooltip", "aria-hidden": !isVisible && !alwaysActive, style: Object.keys(contentStyle).length > 0 ? contentStyle : undefined, children: content || label })] }));
5489
- };
5490
-
5491
5797
  /**
5492
5798
  * Whether `el` is editable while its parent is not, which makes it an editing
5493
5799
  * host: the one tab stop an editable region gets. `contenteditable="false"`
@@ -5952,6 +6258,222 @@ function useFocusTrap(containerRef, options = {}) {
5952
6258
  });
5953
6259
  }
5954
6260
 
6261
+ /** An id list with `id` added at the end, unless it's in there already. */
6262
+ const withId = (ids, id) => [...new Set([...(ids ?? '').split(/\s+/), id])].filter(Boolean).join(' ');
6263
+ /**
6264
+ * The first element in the trigger that Tab lands on, judged by what it is
6265
+ * rather than by its styles, which can hide it for now. The trigger is all
6266
+ * of the wrapper that comes before the tooltip.
6267
+ */
6268
+ const triggerTabStop = (wrapper, tooltip) => {
6269
+ const elements = Array.from(wrapper.querySelectorAll('*'));
6270
+ return elements
6271
+ .slice(0, elements.indexOf(tooltip))
6272
+ .find(el => isTabbableKind(el) && !el.matches(':disabled'));
6273
+ };
6274
+ /**
6275
+ * The `Tooltip` component displays helpful information when users hover over or focus on an element.
6276
+ *
6277
+ * Hovering the trigger or giving it keyboard focus shows it. Touch doesn't,
6278
+ * unless `openOnTap` is on, and neither does focus a click or a tap brings,
6279
+ * so tapping the trigger just does what the trigger does. The pointer can
6280
+ * move onto a shown tooltip. Escape hides it wherever focus is, without
6281
+ * moving the pointer or focus, and it stays hidden until one of them leaves
6282
+ * the trigger and comes back, or, with `openOnTap`, until the next tap on
6283
+ * the trigger. The key always goes on too, to whatever has focus or a
6284
+ * `Modal` or `Popover` around it, since a tooltip only adds to what's
6285
+ * there. The tooltip is its trigger's description, so a screen reader reads
6286
+ * it as the trigger takes focus, whether it's showing yet or not.
6287
+ *
6288
+ * @function
6289
+ * @param {TooltipProps} props - Props for the Tooltip component.
6290
+ * @returns {JSX.Element} The rendered tooltip wrapper.
6291
+ *
6292
+ * @example
6293
+ * // Basic tooltip
6294
+ * <Tooltip label="This is helpful info">
6295
+ * <button>Hover me</button>
6296
+ * </Tooltip>
6297
+ *
6298
+ * @example
6299
+ * // Positioned tooltip with color, on an icon-only button with a name of its own
6300
+ * <Tooltip label="Moves it to the trash" position="right" color="danger">
6301
+ * <Button aria-label="Delete">
6302
+ * <Icon name="fas fa-trash" />
6303
+ * </Button>
6304
+ * </Tooltip>
6305
+ *
6306
+ * @example
6307
+ * // Multiline tooltip
6308
+ * <Tooltip label="This is a longer tooltip that wraps to multiple lines" multiline>
6309
+ * <button>More info</button>
6310
+ * </Tooltip>
6311
+ */
6312
+ const Tooltip = ({ label, content, position = 'top', color, size, active: alwaysActive, multiline = false, animated = true, square = false, dashed = false, delay = 0, closeDelay = 0, openOnTap = false, children, className, tooltipClassName, ...props }) => {
6313
+ const { bulmaHelperClasses, rest } = useBulmaClasses(props);
6314
+ const { onPointerEnter, onPointerOver, onPointerLeave, onPointerDown, onPointerUp, onFocus, onBlur, ...attrs } = rest;
6315
+ const [isVisible, setIsVisible] = React.useState(false);
6316
+ const [resolvedPosition, setResolvedPosition] = React.useState(position === 'auto' ? 'top' : position);
6317
+ const [contentStyle, setContentStyle] = React.useState({});
6318
+ const wrapperRef = React.useRef(null);
6319
+ const contentRef = React.useRef(null);
6320
+ const tooltipId = `${React.useId()}-tooltip`;
6321
+ // Only a single element can be given the link, and a fragment takes no
6322
+ // props of its own.
6323
+ const linkable = React.isValidElement(children) &&
6324
+ children.type !== React.Fragment;
6325
+ // A screen reader reads the tooltip only when Tab lands on an element that
6326
+ // carries the link. Only the page shows whether it does, since a component
6327
+ // may not pass the attribute on, or what takes focus may sit inside the
6328
+ // element that took it. After every commit, since the children can change,
6329
+ // and only in development.
6330
+ React.useEffect(() => {
6331
+ if (!isDev())
6332
+ return;
6333
+ const stop = triggerTabStop(wrapperRef.current, contentRef.current);
6334
+ if (stop?.getAttribute('aria-describedby')?.split(/\s+/).includes(tooltipId)) {
6335
+ return;
6336
+ }
6337
+ warnOnce('Tooltip:unlinked-trigger', "[bestax-bulma] <Tooltip> can't describe its trigger, so a screen " +
6338
+ "reader won't read the tooltip: Tab reaches nothing in the trigger, " +
6339
+ "or what it reaches doesn't carry the aria-describedby that points " +
6340
+ 'at the tooltip. Pass one element that takes focus and passes ' +
6341
+ 'aria-describedby on to its DOM node, such as a button, a link or a ' +
6342
+ 'Span with tabIndex={0}.');
6343
+ });
6344
+ const hover = useHoverOpen({
6345
+ // The pointer and focus as they are. `active` shows it on top of that, so
6346
+ // dropping `active` leaves it shown while the pointer or focus holds it.
6347
+ // An ask to close can come while it's already hidden, which is no change.
6348
+ open: isVisible,
6349
+ onOpenChange: setIsVisible,
6350
+ openDelay: delay,
6351
+ closeDelay,
6352
+ // The wrapper stands in for the trigger. It holds only the children and
6353
+ // the tooltip, and nothing in the tooltip takes focus, so focus anywhere
6354
+ // in it is focus on the trigger.
6355
+ triggerRef: wrapperRef,
6356
+ floatingRef: contentRef,
6357
+ // Escape hides it and carries on, so a control in the trigger, or a
6358
+ // Modal around it, still gets the key.
6359
+ consumeEscape: false,
6360
+ openOnTap,
6361
+ });
6362
+ // Auto-placement and overflow correction
6363
+ React.useLayoutEffect(() => {
6364
+ const isAuto = position === 'auto';
6365
+ if (!isAuto) {
6366
+ // eslint-disable-next-line react-hooks/set-state-in-effect -- layout measurement effect resolves placement
6367
+ setResolvedPosition(position);
6368
+ }
6369
+ const showing = isVisible || alwaysActive;
6370
+ if (!showing) {
6371
+ setContentStyle({});
6372
+ return;
6373
+ }
6374
+ const wrapper = wrapperRef.current;
6375
+ const content = contentRef.current;
6376
+ if (!wrapper || !content)
6377
+ return;
6378
+ const wrapperRect = wrapper.getBoundingClientRect();
6379
+ const margin = 40;
6380
+ const edgePadding = 8; // minimum distance from viewport edge
6381
+ // Step 1: determine position (auto mode only)
6382
+ if (isAuto) {
6383
+ const spaceTop = wrapperRect.top;
6384
+ const spaceBottom = window.innerHeight - wrapperRect.bottom;
6385
+ if (spaceTop >= margin) {
6386
+ setResolvedPosition('top');
6387
+ }
6388
+ else if (spaceBottom >= margin) {
6389
+ setResolvedPosition('bottom');
6390
+ }
6391
+ else if (window.innerWidth - wrapperRect.right >= wrapperRect.left) {
6392
+ setResolvedPosition('right');
6393
+ }
6394
+ else {
6395
+ setResolvedPosition('left');
6396
+ }
6397
+ }
6398
+ // Step 2: measure tooltip content and correct overflow.
6399
+ // Use requestAnimationFrame so the resolved position class is applied first.
6400
+ requestAnimationFrame(() => {
6401
+ const contentEl = contentRef.current;
6402
+ if (!contentEl)
6403
+ return;
6404
+ const contentRect = contentEl.getBoundingClientRect();
6405
+ const style = {};
6406
+ // For top/bottom positions, tooltip is centered horizontally via CSS
6407
+ // (left: 50%; transform: translateX(-50%)).
6408
+ // Check if it overflows left or right and apply a corrective shift.
6409
+ const pos = isAuto ? undefined : position; // read from state won't be updated yet in auto
6410
+ const effectivePos = pos || 'top'; // fallback, actual is set by state
6411
+ const isVertical = effectivePos === 'top' ||
6412
+ effectivePos === 'bottom' ||
6413
+ resolvedPosition === 'top' ||
6414
+ resolvedPosition === 'bottom';
6415
+ if (isVertical) {
6416
+ if (contentRect.left < edgePadding) {
6417
+ // Overflows left — shift right
6418
+ const shift = edgePadding - contentRect.left;
6419
+ style.left = `calc(50% + ${shift}px)`;
6420
+ style['--tooltip-arrow-offset'] =
6421
+ `${-shift}px`;
6422
+ }
6423
+ else if (contentRect.right > window.innerWidth - edgePadding) {
6424
+ // Overflows right — shift left
6425
+ const shift = contentRect.right - (window.innerWidth - edgePadding);
6426
+ style.left = `calc(50% - ${shift}px)`;
6427
+ style['--tooltip-arrow-offset'] =
6428
+ `${shift}px`;
6429
+ }
6430
+ }
6431
+ else {
6432
+ // For left/right positions, tooltip is centered vertically via CSS
6433
+ // (top: 50%; transform: translateY(-50%)).
6434
+ if (contentRect.top < edgePadding) {
6435
+ const shift = edgePadding - contentRect.top;
6436
+ style.top = `calc(50% + ${shift}px)`;
6437
+ style['--tooltip-arrow-offset'] =
6438
+ `${-shift}px`;
6439
+ }
6440
+ else if (contentRect.bottom > window.innerHeight - edgePadding) {
6441
+ const shift = contentRect.bottom - (window.innerHeight - edgePadding);
6442
+ style.top = `calc(50% - ${shift}px)`;
6443
+ style['--tooltip-arrow-offset'] =
6444
+ `${shift}px`;
6445
+ }
6446
+ }
6447
+ setContentStyle(style);
6448
+ });
6449
+ }, [position, resolvedPosition, isVisible, alwaysActive, label, content]);
6450
+ // Generate Bulma classes
6451
+ const tooltipClasses = usePrefixedClassNames('tooltip', {
6452
+ 'is-active': alwaysActive || isVisible,
6453
+ // Open by the pointer, focus or a tap, rather than shown by `active`
6454
+ // alone: only then does the tooltip, and the gap to it, take the pointer.
6455
+ 'is-open': isVisible,
6456
+ [`is-${resolvedPosition}`]: resolvedPosition,
6457
+ [`is-${color}`]: color,
6458
+ [`is-${size}`]: size,
6459
+ 'is-multiline': multiline,
6460
+ 'is-animated': animated,
6461
+ 'is-square': square,
6462
+ 'is-dashed': dashed,
6463
+ });
6464
+ const combinedClasses = classNames(tooltipClasses, bulmaHelperClasses, className);
6465
+ const combinedTooltipClasses = usePrefixedClassNames('tooltip-content', tooltipClassName);
6466
+ return (jsxRuntime.jsxs("span", { ref: wrapperRef, className: combinedClasses, onPointerEnter: chain$1(onPointerEnter, hover.onPointerEnter), onPointerOver: chain$1(onPointerOver, hover.onPointerOver), onPointerLeave: chain$1(onPointerLeave, hover.onPointerLeave), onFocus: chain$1(onFocus, hover.onFocus), onBlur: chain$1(onBlur, hover.onBlur), onPointerDown: chain$1(onPointerDown, hover.onPointerDown), onPointerUp: chain$1(onPointerUp, hover.onPointerUp), ...(label ? { 'data-tooltip': label } : {}), ...attrs, children: [linkable
6467
+ ? React.cloneElement(children, {
6468
+ 'aria-describedby': withId(children.props['aria-describedby'], tooltipId),
6469
+ })
6470
+ : children, jsxRuntime.jsx("span", { ref: contentRef, id: tooltipId, className: combinedTooltipClasses,
6471
+ // Hidden by `visibility: hidden` in the stylesheet, which keeps it out
6472
+ // of the accessibility tree and still lets it describe the trigger
6473
+ // (accname step 2A), so it takes no aria-hidden.
6474
+ role: "tooltip", style: Object.keys(contentStyle).length > 0 ? contentStyle : undefined, children: content || label })] }));
6475
+ };
6476
+
5955
6477
  // INTERNAL: deliberately not exported from src/index.ts.
5956
6478
  /**
5957
6479
  * A `styledGap` for a panel whose stylesheet puts the gap in a margin on the
@@ -6017,149 +6539,20 @@ function useAnchoredPosition(anchorRef, panelRef, { active, position, fixed, off
6017
6539
  ? rect.bottom + offset
6018
6540
  : rect.top - height - offset,
6019
6541
  left: corner.endsWith('left') ? rect.left : rect.right - width,
6020
- }
6021
- : { position: corner };
6022
- setResolved(prev => (samePosition(prev, next) ? prev : next));
6023
- };
6024
- update();
6025
- view.addEventListener('resize', update, { passive: true });
6026
- view.addEventListener('scroll', update, { passive: true, capture: true });
6027
- return () => {
6028
- view.removeEventListener('resize', update);
6029
- view.removeEventListener('scroll', update, { capture: true });
6030
- };
6031
- }, [active, anchorRef, panelRef, position, fixed, offset, styledGap]);
6032
- return resolved;
6033
- }
6034
-
6035
- const validButtonColors = [...validColors, 'text', 'ghost'];
6036
- /**
6037
- * The `Button` component provides a flexible and highly customizable button for your Bulma React UI.
6038
- *
6039
- * @function
6040
- * @param {ButtonProps} props - Props for the Button component.
6041
- * @param {React.Ref} ref - Forwarded ref to the element `as` renders.
6042
- * @returns {JSX.Element} The rendered button, anchor, or custom element.
6043
- * @see {@link https://bulma.io/documentation/elements/button/ | Bulma Button documentation}
6044
- */
6045
- const Button = React.forwardRef(function Button(props, ref) {
6046
- const { color, size, isLight, isRounded, isLoading, isStatic, isFullwidth, isFullWidth, isOutlined, isInverted, isFocused, isActive, isHovered, isDisabled, className, children, textColor, bgColor, as: Component = 'button', href, onClick, target, rel, ...bulmaProps } = props;
6047
- const { bulmaHelperClasses, rest } = useBulmaClasses({
6048
- color: textColor,
6049
- backgroundColor: bgColor,
6050
- ...bulmaProps,
6051
- });
6052
- // Generate Bulma classes with prefix
6053
- const bulmaClasses = usePrefixedClassNames('button', {
6054
- [`is-${color}`]: color &&
6055
- validButtonColors.includes(color),
6056
- [`is-${size}`]: size,
6057
- 'is-outlined': isOutlined,
6058
- 'is-light': isLight,
6059
- 'is-loading': isLoading,
6060
- 'is-static': isStatic,
6061
- 'is-disabled': isDisabled,
6062
- 'is-rounded': isRounded,
6063
- 'is-hovered': isHovered,
6064
- 'is-focused': isFocused,
6065
- 'is-active': isActive,
6066
- 'is-inverted': isInverted,
6067
- 'is-fullwidth': isFullwidth ?? isFullWidth,
6068
- });
6069
- // Combine prefixed Bulma classes with unprefixed user className and prefixed helper classes
6070
- const buttonClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
6071
- if (Component !== 'button') {
6072
- // Strip the form-control attributes a link-like element cannot carry, so a
6073
- // JavaScript consumer or a spread object doesn't put them on the DOM. The
6074
- // types no longer allow them through; this is the runtime backstop.
6075
- //
6076
- // Only for INTRINSIC tags. A custom component (a router Link, ...) owns its
6077
- // prop contract, and `ComponentPropsWithoutRef<T>` promises the caller that
6078
- // its props reach it. Stripping names by their spelling alone broke that
6079
- // promise silently: `<Button as={Custom} name="x" />` type-checked while
6080
- // `Custom` never received the `name` it requires.
6081
- //
6082
- // `autoFocus` and `type` are deliberately NOT stripped: both are valid on
6083
- // targets this list was filtering them from. `autoFocus` is a global
6084
- // attribute (React decides per element whether to honour it, which is not
6085
- // ours to pre-empt), and `type` on an anchor is the MIME hint that
6086
- // `ButtonProps<'a'>` now types it as.
6087
- //
6088
- // The types already answer this for a TypeScript caller: `ButtonProps<T>`
6089
- // derives from `ComponentPropsWithoutRef<T>`, so only what the target
6090
- // accepts can be passed. What is left is a runtime backstop for JavaScript
6091
- // consumers and spread objects.
6092
- //
6093
- // Keep it to the two cases where a stray attribute does something. The
6094
- // submit-overrides belong to a submit control and never make sense on the
6095
- // anchor path. `disabled` is the one that is VISIBLE: Bulma styles
6096
- // `.button[disabled]` (background, border, shadow, opacity), so letting it
6097
- // through to a `<span>` greys the element out — main stripped it, and not
6098
- // stripping it would be a silent visual change on a types-only release.
6099
- //
6100
- // Everything else (`name`, `value`, `form`) is inert where it does not
6101
- // belong, and enumerating which tags own it is what kept going wrong:
6102
- // first all nine names on every non-button tag (so `<Button as="input"
6103
- // name="query" value="Search">` submitted nothing), then a form-control
6104
- // allowlist that still lost `formAction` on an input and `value` on an
6105
- // option. Those now ride on the types instead.
6106
- //
6107
- // A custom component is exempt entirely — it owns its prop contract, and
6108
- // `ComponentPropsWithoutRef<T>` promises the caller its props arrive.
6109
- //
6110
- // The elements that own `disabled`, per the HTML spec. A closed set, unlike
6111
- // the open-ended question of which tag owns which attribute.
6112
- const SUBMIT_OVERRIDES = [
6113
- 'formAction',
6114
- 'formEncType',
6115
- 'formMethod',
6116
- 'formNoValidate',
6117
- 'formTarget',
6118
- ];
6119
- const OWNS_DISABLED = [
6120
- 'button',
6121
- 'fieldset',
6122
- 'input',
6123
- 'optgroup',
6124
- 'option',
6125
- 'select',
6126
- 'textarea',
6127
- ];
6128
- // Two independent filters. They are not exclusive — an `<a>` owns neither
6129
- // the submit-overrides nor `disabled`, so it takes both — and deriving them
6130
- // from one destructure, or from one if/else, is what broke each of them in
6131
- // turn: first the overrides came off every intrinsic (an `as="input"` lost
6132
- // `formAction`), then scoping them to the anchor let `disabled` back onto
6133
- // it.
6134
- const forwardedRest = { ...rest };
6135
- if (typeof Component === 'string' && !isCustomElement(Component)) {
6136
- // Meaningless anywhere but a submit control, and an anchor is not one.
6137
- if (Component === 'a') {
6138
- for (const key of SUBMIT_OVERRIDES)
6139
- delete forwardedRest[key];
6140
- }
6141
- // Visible via Bulma's `.button[disabled]`, so withheld from any element
6142
- // that does not own it.
6143
- if (!OWNS_DISABLED.includes(Component))
6144
- delete forwardedRest.disabled;
6145
- }
6146
- return (jsxRuntime.jsx(Component, { ref: ref, className: buttonClasses, href: href, target: target, rel: rel, "aria-disabled": isDisabled, tabIndex: isDisabled ? -1 : undefined, onClick: isDisabled
6147
- ? // Guarded, because this handler is installed into whatever `as`
6148
- // renders and a custom component's `onClick` need not be a DOM
6149
- // event handler at all. One declaring `onClick: (v: string) =>
6150
- // void` calls this with a string, and an unguarded
6151
- // `e.preventDefault()` throws `is not a function`. Blocking the
6152
- // real event is the point; a foreign payload is simply not ours
6153
- // to act on.
6154
- (e) => {
6155
- if (typeof e?.preventDefault === 'function')
6156
- e.preventDefault();
6157
- }
6158
- : onClick, ...forwardedRest, children: children }));
6159
- }
6160
- return (jsxRuntime.jsx("button", { ref: ref, className: buttonClasses, disabled: isDisabled, onClick: onClick, ...rest, children: children }));
6161
- });
6162
- Button.displayName = 'Button';
6542
+ }
6543
+ : { position: corner };
6544
+ setResolved(prev => (samePosition(prev, next) ? prev : next));
6545
+ };
6546
+ update();
6547
+ view.addEventListener('resize', update, { passive: true });
6548
+ view.addEventListener('scroll', update, { passive: true, capture: true });
6549
+ return () => {
6550
+ view.removeEventListener('resize', update);
6551
+ view.removeEventListener('scroll', update, { capture: true });
6552
+ };
6553
+ }, [active, anchorRef, panelRef, position, fixed, offset, styledGap]);
6554
+ return resolved;
6555
+ }
6163
6556
 
6164
6557
  const PopoverContext = React.createContext(null);
6165
6558
  /** Intrinsic elements that are controls already, and keep their own role. */
@@ -6708,182 +7101,6 @@ function useContextMenuTrigger({ onOpen, disabled, }) {
6708
7101
  };
6709
7102
  }
6710
7103
 
6711
- // INTERNAL: deliberately not exported from src/index.ts.
6712
- /**
6713
- * Every menu-item role, so an item a caller gives `menuitemcheckbox` or
6714
- * `menuitemradio` (a filter or sort menu) stays reachable.
6715
- */
6716
- const MENU_ITEM_SELECTOR = '[role="menuitem"], [role="menuitemcheckbox"], [role="menuitemradio"]';
6717
- /**
6718
- * Whether a menu item can be reached and activated: it isn't disabled, as a
6719
- * native control or through `aria-disabled="true"`.
6720
- */
6721
- function isEnabledMenuItem(item) {
6722
- return (!item.hasAttribute('disabled') &&
6723
- item.getAttribute('aria-disabled') !== 'true');
6724
- }
6725
- /** Whether `node` is one of `menu`'s items, enabled or not. */
6726
- const isItemOf = (menu, node) => Boolean(node && menu.contains(node) && node.matches(MENU_ITEM_SELECTOR));
6727
- /** The items in `menu` the keys reach, in document order. */
6728
- function enabledMenuItems(menu) {
6729
- return Array.from(menu.querySelectorAll(MENU_ITEM_SELECTOR)).filter(isEnabledMenuItem);
6730
- }
6731
- /** How long typed characters keep adding up to one search, in milliseconds. */
6732
- const TYPEAHEAD_TIMEOUT = 500;
6733
- /**
6734
- * Whether the browser activates `item` on this key by itself: a `<button>`
6735
- * answers Enter and Space, and a link with an `href` answers Enter.
6736
- */
6737
- function activatesItself(item, key) {
6738
- if (item.tagName === 'BUTTON')
6739
- return true;
6740
- return key === 'Enter' && item.tagName === 'A' && item.hasAttribute('href');
6741
- }
6742
- /**
6743
- * The text typing finds an item by: what it says, without what's hidden from
6744
- * assistive technology, such as an icon whose glyph is a ligature's name.
6745
- */
6746
- function label(item) {
6747
- let text = '';
6748
- const read = (node) => {
6749
- node.childNodes.forEach(child => {
6750
- if (child.nodeType === 3)
6751
- text += child.textContent;
6752
- else if (child.nodeType === 1 &&
6753
- child.getAttribute('aria-hidden') !== 'true') {
6754
- read(child);
6755
- }
6756
- });
6757
- };
6758
- read(item);
6759
- return text.trim().toLowerCase();
6760
- }
6761
- /**
6762
- * The item typing `search` moves to, or none. A single character looks from
6763
- * the item after the focused one, so typing the same letter again cycles
6764
- * through the items that start with it, and so does a run of one letter
6765
- * ("dd"). A longer search looks from the focused item, so typing more of
6766
- * its own text keeps it.
6767
- */
6768
- function typeaheadMatch(items, search, current) {
6769
- const repeated = Array.from(search).every(char => char === search[0]);
6770
- const wanted = repeated ? search[0] : search;
6771
- const start = current < 0 ? 0 : current + (wanted.length === 1 ? 1 : 0);
6772
- for (let i = 0; i < items.length; i += 1) {
6773
- const item = items[(start + i) % items.length];
6774
- if (label(item).startsWith(wanted))
6775
- return item;
6776
- }
6777
- return undefined;
6778
- }
6779
- /**
6780
- * Moves through and activates the items of a `role="menu"` element from the
6781
- * keyboard. Put the `onKeyDown` it returns on that element. It reads the
6782
- * items from the element each time, so items that come and go are found,
6783
- * and it renders nothing.
6784
- *
6785
- * It acts only while focus is on the menu element itself or on one of its
6786
- * items, so anything else focusable inside the menu, such as a filter
6787
- * field, keeps its own keys.
6788
- *
6789
- * - Down and Up move to the next and previous enabled item, wrapping, and
6790
- * Home and End go to the first and last. From no item, Down goes to the
6791
- * first and Up to the last.
6792
- * - Enter and Space activate the focused item. A `<button>`, and a link with
6793
- * an `href` for Enter, get the browser's own click, and anything else gets
6794
- * one from here, so its `onClick` runs either way. A key held down
6795
- * activates it once. A key handler on the item that called
6796
- * `preventDefault()` has claimed the key, and nothing else runs it.
6797
- * - A printable character moves to the next enabled item whose text starts
6798
- * with what has been typed. Characters typed within `TYPEAHEAD_TIMEOUT` of
6799
- * each other add up to one search, until `reset` starts a new one.
6800
- *
6801
- * Items are `MENU_ITEM_SELECTOR` matches, and disabled ones are skipped by
6802
- * all of the above. Escape and Tab are left to the menu's owner, since what
6803
- * closing means is up to it, and so is any other key.
6804
- *
6805
- * @function useMenuKeys
6806
- * @returns The `keydown` handler for the menu element, and a way to forget
6807
- * what has been typed.
6808
- */
6809
- function useMenuKeys() {
6810
- const typedRef = React.useRef({ text: '' });
6811
- React.useEffect(() => {
6812
- const typed = typedRef.current;
6813
- return () => clearTimeout(typed.timer);
6814
- }, []);
6815
- return React.useMemo(() => {
6816
- const reset = () => {
6817
- const typed = typedRef.current;
6818
- clearTimeout(typed.timer);
6819
- typed.text = '';
6820
- };
6821
- const onKeyDown = (event) => {
6822
- const menu = event.currentTarget;
6823
- const focused = getActiveElementInTree(menu);
6824
- if (focused !== menu && !isItemOf(menu, focused))
6825
- return;
6826
- const items = enabledMenuItems(menu);
6827
- if (!items.length)
6828
- return;
6829
- const current = items.indexOf(focused);
6830
- switch (event.key) {
6831
- case 'ArrowDown':
6832
- event.preventDefault();
6833
- items[current >= 0 ? (current + 1) % items.length : 0].focus();
6834
- return;
6835
- case 'ArrowUp':
6836
- event.preventDefault();
6837
- items[current >= 0
6838
- ? (current - 1 + items.length) % items.length
6839
- : items.length - 1].focus();
6840
- return;
6841
- case 'Home':
6842
- event.preventDefault();
6843
- items[0].focus();
6844
- return;
6845
- case 'End':
6846
- event.preventDefault();
6847
- items[items.length - 1].focus();
6848
- return;
6849
- case 'Enter':
6850
- case ' ': {
6851
- if (current < 0 || event.defaultPrevented)
6852
- return;
6853
- const item = items[current];
6854
- if (activatesItself(item, event.key)) {
6855
- // The browser clicks on Enter's keydown, and a held key sends one
6856
- // per auto-repeat. Space clicks on keyup, once however long it's
6857
- // held.
6858
- if (event.repeat && event.key === 'Enter')
6859
- event.preventDefault();
6860
- return;
6861
- }
6862
- // Space would scroll the page, and a held key repeats.
6863
- event.preventDefault();
6864
- if (!event.repeat)
6865
- item.click();
6866
- return;
6867
- }
6868
- }
6869
- if (event.key.length !== 1 ||
6870
- event.ctrlKey ||
6871
- event.metaKey ||
6872
- event.altKey ||
6873
- event.defaultPrevented) {
6874
- return;
6875
- }
6876
- event.preventDefault();
6877
- const typed = typedRef.current;
6878
- typed.text += event.key.toLowerCase();
6879
- clearTimeout(typed.timer);
6880
- typed.timer = setTimeout(reset, TYPEAHEAD_TIMEOUT);
6881
- typeaheadMatch(items, typed.text, current)?.focus();
6882
- };
6883
- return { onKeyDown, reset };
6884
- }, []);
6885
- }
6886
-
6887
7104
  /**
6888
7105
  * Where the menu goes along one axis, from the point at `at`: after it when
6889
7106
  * there's room, before it when there's room there instead, and otherwise
@@ -8573,38 +8790,6 @@ const LinkButton = React.forwardRef(function LinkButton({ variant = 'text', colo
8573
8790
  });
8574
8791
  LinkButton.displayName = 'LinkButton';
8575
8792
 
8576
- const validButtonsSizes = ['small', 'medium', 'large'];
8577
- /**
8578
- * The `Buttons` component lets you group multiple `Button` elements together with Bulma's spacing, alignment, and add-on features.
8579
- *
8580
- * @function
8581
- * @param {ButtonsProps} props - Props for the Buttons component.
8582
- * @returns {JSX.Element} The rendered group of buttons.
8583
- * @see {@link https://bulma.io/documentation/elements/button/#group | Bulma Button Group documentation}
8584
- */
8585
- const ButtonsComponent = ({ className, textColor, color, bgColor, isCentered, isRight, hasAddons, size, children, ...props }) => {
8586
- const buttonsClasses = usePrefixedClassNames('buttons', {
8587
- 'is-centered': isCentered,
8588
- 'is-right': isRight,
8589
- 'has-addons': hasAddons,
8590
- [`are-${size}`]: size && validButtonsSizes.includes(size),
8591
- });
8592
- /**
8593
- * Generates Bulma helper classes and separates out remaining props.
8594
- */
8595
- const { bulmaHelperClasses, rest } = useBulmaClasses({
8596
- color: textColor ?? color,
8597
- backgroundColor: bgColor,
8598
- ...props,
8599
- });
8600
- const combinedClasses = classNames(buttonsClasses, className, bulmaHelperClasses);
8601
- return (jsxRuntime.jsx("div", { className: combinedClasses, ...rest, children: children }));
8602
- };
8603
- const Buttons = withSubComponents(ButtonsComponent, {
8604
- Button,
8605
- LinkButton,
8606
- }, 'Buttons');
8607
-
8608
8793
  /**
8609
8794
  * The `ToggleButton` component renders a button that stays pressed until it's pressed again, and says whether it's on through `aria-pressed`.
8610
8795
  *
@@ -8612,7 +8797,8 @@ const Buttons = withSubComponents(ButtonsComponent, {
8612
8797
  * carries no form value: a setting saved with a form is a `Checkbox` or a
8613
8798
  * `Switch`. A set of toggles that each turn on and off on their own goes in
8614
8799
  * `Buttons hasAddons` with `role="group"` and an `aria-label`. A set where only
8615
- * one can be on is a radio group rather than a row of these: use `Radios`.
8800
+ * one can be on is a radio group rather than a row of these: use
8801
+ * `SegmentedControl`, or `Radios` for a choice that's sent with a form.
8616
8802
  *
8617
8803
  * @function
8618
8804
  * @param {ToggleButtonProps} props - Props for the ToggleButton component.
@@ -8630,7 +8816,7 @@ const Buttons = withSubComponents(ButtonsComponent, {
8630
8816
  * </ToggleButton>
8631
8817
  * </Buttons>
8632
8818
  */
8633
- const ToggleButton = React.forwardRef(function ToggleButton({ pressed: controlledPressed, defaultPressed = false, onPressedChange, color = 'link', className, onClick, ...props }, ref) {
8819
+ const ToggleButton = React.forwardRef(function ToggleButton({ pressed: controlledPressed, defaultPressed = false, onPressedChange, color = 'link', isLoading, 'aria-busy': ariaBusy, 'aria-disabled': ariaDisabled, className, onClick, ...props }, ref) {
8634
8820
  const isControlled = controlledPressed !== undefined;
8635
8821
  const [internalPressed, setInternalPressed] = React.useState(defaultPressed);
8636
8822
  const pressed = controlledPressed ?? internalPressed;
@@ -8641,7 +8827,13 @@ const ToggleButton = React.forwardRef(function ToggleButton({ pressed: controlle
8641
8827
  // The caller's onClick runs first, so a `preventDefault()` there vetoes
8642
8828
  // the toggle, as it does in Radix, MUI and Zag. Enter and Space reach here
8643
8829
  // through the button's own click, and a disabled button gets no click.
8830
+ //
8831
+ // A loading one gets the click but drops it. Bulma's spinner only stops a
8832
+ // pointer, so without this a key flipped a toggle the mouse couldn't.
8833
+ // `disabled` would stop both, but it takes focus away mid-save.
8644
8834
  const handleClick = (event) => {
8835
+ if (isLoading)
8836
+ return;
8645
8837
  onClick?.(event);
8646
8838
  if (event.defaultPrevented)
8647
8839
  return;
@@ -8650,13 +8842,48 @@ const ToggleButton = React.forwardRef(function ToggleButton({ pressed: controlle
8650
8842
  if (!isControlled)
8651
8843
  setInternalPressed(next);
8652
8844
  };
8653
- // The props are spread first, so the four things a toggle depends on win
8654
- // over anything a JavaScript caller or a spread object passes along: it
8655
- // stays a button, never submits, and reports the state it's in.
8656
- return (jsxRuntime.jsx(Button, { ...props, ref: ref, as: "button", type: "button", "aria-pressed": pressed, color: pressed ? color : undefined, className: classNames(selectedClass, className), onClick: handleClick }));
8845
+ // The props are spread first, so what a toggle depends on wins over
8846
+ // anything a JavaScript caller or a spread object passes along: it stays
8847
+ // a button, never submits, reports the state it's in, and says it's busy
8848
+ // and won't respond while it's loading. A caller's own `aria-busy` or
8849
+ // `aria-disabled` stays when it isn't loading.
8850
+ return (jsxRuntime.jsx(Button, { ...props, ref: ref, as: "button", type: "button", isLoading: isLoading, "aria-busy": isLoading || ariaBusy, "aria-disabled": isLoading || ariaDisabled, "aria-pressed": pressed, color: pressed ? color : undefined, className: classNames(selectedClass, className), onClick: handleClick }));
8657
8851
  });
8658
8852
  ToggleButton.displayName = 'ToggleButton';
8659
8853
 
8854
+ const validButtonsSizes = ['small', 'medium', 'large'];
8855
+ /**
8856
+ * The `Buttons` component lets you group multiple `Button` elements together with Bulma's spacing, alignment, and add-on features.
8857
+ *
8858
+ * @function
8859
+ * @param {ButtonsProps} props - Props for the Buttons component.
8860
+ * @returns {JSX.Element} The rendered group of buttons.
8861
+ * @see {@link https://bulma.io/documentation/elements/button/#group | Bulma Button Group documentation}
8862
+ */
8863
+ const ButtonsComponent = ({ className, textColor, color, bgColor, isCentered, isRight, hasAddons, size, children, ...props }) => {
8864
+ const buttonsClasses = usePrefixedClassNames('buttons', {
8865
+ 'is-centered': isCentered,
8866
+ 'is-right': isRight,
8867
+ 'has-addons': hasAddons,
8868
+ [`are-${size}`]: size && validButtonsSizes.includes(size),
8869
+ });
8870
+ /**
8871
+ * Generates Bulma helper classes and separates out remaining props.
8872
+ */
8873
+ const { bulmaHelperClasses, rest } = useBulmaClasses({
8874
+ color: textColor ?? color,
8875
+ backgroundColor: bgColor,
8876
+ ...props,
8877
+ });
8878
+ const combinedClasses = classNames(buttonsClasses, className, bulmaHelperClasses);
8879
+ return (jsxRuntime.jsx("div", { className: combinedClasses, ...rest, children: children }));
8880
+ };
8881
+ const Buttons = withSubComponents(ButtonsComponent, {
8882
+ Button,
8883
+ LinkButton,
8884
+ ToggleButton,
8885
+ }, 'Buttons');
8886
+
8660
8887
  /**
8661
8888
  * The `Code` component renders a styled `<code>` element with Bulma helper class integration.
8662
8889
  *
@@ -19198,6 +19425,7 @@ exports.Radios = Radios;
19198
19425
  exports.Rate = Rate;
19199
19426
  exports.Reveal = Reveal;
19200
19427
  exports.Section = Section;
19428
+ exports.SegmentedControl = SegmentedControl;
19201
19429
  exports.Select = Select;
19202
19430
  exports.SelectBase = SelectBase;
19203
19431
  exports.Sidebar = Sidebar;