@allxsmith/bestax-bulma 5.30.0 → 5.31.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 }) })] }));
@@ -5321,177 +5448,9 @@ function useHoverOpen(options) {
5321
5448
  }
5322
5449
 
5323
5450
  /**
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
- /**
5492
- * Whether `el` is editable while its parent is not, which makes it an editing
5493
- * host: the one tab stop an editable region gets. `contenteditable="false"`
5494
- * turns editing off, and a missing or unknown value inherits the parent's.
5451
+ * Whether `el` is editable while its parent is not, which makes it an editing
5452
+ * host: the one tab stop an editable region gets. `contenteditable="false"`
5453
+ * turns editing off, and a missing or unknown value inherits the parent's.
5495
5454
  */
5496
5455
  function isEditingHost(el) {
5497
5456
  const editable = (node) => {
@@ -5950,7 +5909,223 @@ function useFocusTrap(containerRef, options = {}) {
5950
5909
  setRefChanges(n => n + 1);
5951
5910
  }
5952
5911
  });
5953
- }
5912
+ }
5913
+
5914
+ /** An id list with `id` added at the end, unless it's in there already. */
5915
+ const withId = (ids, id) => [...new Set([...(ids ?? '').split(/\s+/), id])].filter(Boolean).join(' ');
5916
+ /**
5917
+ * The first element in the trigger that Tab lands on, judged by what it is
5918
+ * rather than by its styles, which can hide it for now. The trigger is all
5919
+ * of the wrapper that comes before the tooltip.
5920
+ */
5921
+ const triggerTabStop = (wrapper, tooltip) => {
5922
+ const elements = Array.from(wrapper.querySelectorAll('*'));
5923
+ return elements
5924
+ .slice(0, elements.indexOf(tooltip))
5925
+ .find(el => isTabbableKind(el) && !el.matches(':disabled'));
5926
+ };
5927
+ /**
5928
+ * The `Tooltip` component displays helpful information when users hover over or focus on an element.
5929
+ *
5930
+ * Hovering the trigger or giving it keyboard focus shows it. Touch doesn't,
5931
+ * unless `openOnTap` is on, and neither does focus a click or a tap brings,
5932
+ * so tapping the trigger just does what the trigger does. The pointer can
5933
+ * move onto a shown tooltip. Escape hides it wherever focus is, without
5934
+ * moving the pointer or focus, and it stays hidden until one of them leaves
5935
+ * the trigger and comes back, or, with `openOnTap`, until the next tap on
5936
+ * the trigger. The key always goes on too, to whatever has focus or a
5937
+ * `Modal` or `Popover` around it, since a tooltip only adds to what's
5938
+ * there. The tooltip is its trigger's description, so a screen reader reads
5939
+ * it as the trigger takes focus, whether it's showing yet or not.
5940
+ *
5941
+ * @function
5942
+ * @param {TooltipProps} props - Props for the Tooltip component.
5943
+ * @returns {JSX.Element} The rendered tooltip wrapper.
5944
+ *
5945
+ * @example
5946
+ * // Basic tooltip
5947
+ * <Tooltip label="This is helpful info">
5948
+ * <button>Hover me</button>
5949
+ * </Tooltip>
5950
+ *
5951
+ * @example
5952
+ * // Positioned tooltip with color, on an icon-only button with a name of its own
5953
+ * <Tooltip label="Moves it to the trash" position="right" color="danger">
5954
+ * <Button aria-label="Delete">
5955
+ * <Icon name="fas fa-trash" />
5956
+ * </Button>
5957
+ * </Tooltip>
5958
+ *
5959
+ * @example
5960
+ * // Multiline tooltip
5961
+ * <Tooltip label="This is a longer tooltip that wraps to multiple lines" multiline>
5962
+ * <button>More info</button>
5963
+ * </Tooltip>
5964
+ */
5965
+ 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 }) => {
5966
+ const { bulmaHelperClasses, rest } = useBulmaClasses(props);
5967
+ const { onPointerEnter, onPointerOver, onPointerLeave, onPointerDown, onPointerUp, onFocus, onBlur, ...attrs } = rest;
5968
+ const [isVisible, setIsVisible] = React.useState(false);
5969
+ const [resolvedPosition, setResolvedPosition] = React.useState(position === 'auto' ? 'top' : position);
5970
+ const [contentStyle, setContentStyle] = React.useState({});
5971
+ const wrapperRef = React.useRef(null);
5972
+ const contentRef = React.useRef(null);
5973
+ const tooltipId = `${React.useId()}-tooltip`;
5974
+ // Only a single element can be given the link, and a fragment takes no
5975
+ // props of its own.
5976
+ const linkable = React.isValidElement(children) &&
5977
+ children.type !== React.Fragment;
5978
+ // A screen reader reads the tooltip only when Tab lands on an element that
5979
+ // carries the link. Only the page shows whether it does, since a component
5980
+ // may not pass the attribute on, or what takes focus may sit inside the
5981
+ // element that took it. After every commit, since the children can change,
5982
+ // and only in development.
5983
+ React.useEffect(() => {
5984
+ if (!isDev())
5985
+ return;
5986
+ const stop = triggerTabStop(wrapperRef.current, contentRef.current);
5987
+ if (stop?.getAttribute('aria-describedby')?.split(/\s+/).includes(tooltipId)) {
5988
+ return;
5989
+ }
5990
+ warnOnce('Tooltip:unlinked-trigger', "[bestax-bulma] <Tooltip> can't describe its trigger, so a screen " +
5991
+ "reader won't read the tooltip: Tab reaches nothing in the trigger, " +
5992
+ "or what it reaches doesn't carry the aria-describedby that points " +
5993
+ 'at the tooltip. Pass one element that takes focus and passes ' +
5994
+ 'aria-describedby on to its DOM node, such as a button, a link or a ' +
5995
+ 'Span with tabIndex={0}.');
5996
+ });
5997
+ const hover = useHoverOpen({
5998
+ // The pointer and focus as they are. `active` shows it on top of that, so
5999
+ // dropping `active` leaves it shown while the pointer or focus holds it.
6000
+ // An ask to close can come while it's already hidden, which is no change.
6001
+ open: isVisible,
6002
+ onOpenChange: setIsVisible,
6003
+ openDelay: delay,
6004
+ closeDelay,
6005
+ // The wrapper stands in for the trigger. It holds only the children and
6006
+ // the tooltip, and nothing in the tooltip takes focus, so focus anywhere
6007
+ // in it is focus on the trigger.
6008
+ triggerRef: wrapperRef,
6009
+ floatingRef: contentRef,
6010
+ // Escape hides it and carries on, so a control in the trigger, or a
6011
+ // Modal around it, still gets the key.
6012
+ consumeEscape: false,
6013
+ openOnTap,
6014
+ });
6015
+ // Auto-placement and overflow correction
6016
+ React.useLayoutEffect(() => {
6017
+ const isAuto = position === 'auto';
6018
+ if (!isAuto) {
6019
+ // eslint-disable-next-line react-hooks/set-state-in-effect -- layout measurement effect resolves placement
6020
+ setResolvedPosition(position);
6021
+ }
6022
+ const showing = isVisible || alwaysActive;
6023
+ if (!showing) {
6024
+ setContentStyle({});
6025
+ return;
6026
+ }
6027
+ const wrapper = wrapperRef.current;
6028
+ const content = contentRef.current;
6029
+ if (!wrapper || !content)
6030
+ return;
6031
+ const wrapperRect = wrapper.getBoundingClientRect();
6032
+ const margin = 40;
6033
+ const edgePadding = 8; // minimum distance from viewport edge
6034
+ // Step 1: determine position (auto mode only)
6035
+ if (isAuto) {
6036
+ const spaceTop = wrapperRect.top;
6037
+ const spaceBottom = window.innerHeight - wrapperRect.bottom;
6038
+ if (spaceTop >= margin) {
6039
+ setResolvedPosition('top');
6040
+ }
6041
+ else if (spaceBottom >= margin) {
6042
+ setResolvedPosition('bottom');
6043
+ }
6044
+ else if (window.innerWidth - wrapperRect.right >= wrapperRect.left) {
6045
+ setResolvedPosition('right');
6046
+ }
6047
+ else {
6048
+ setResolvedPosition('left');
6049
+ }
6050
+ }
6051
+ // Step 2: measure tooltip content and correct overflow.
6052
+ // Use requestAnimationFrame so the resolved position class is applied first.
6053
+ requestAnimationFrame(() => {
6054
+ const contentEl = contentRef.current;
6055
+ if (!contentEl)
6056
+ return;
6057
+ const contentRect = contentEl.getBoundingClientRect();
6058
+ const style = {};
6059
+ // For top/bottom positions, tooltip is centered horizontally via CSS
6060
+ // (left: 50%; transform: translateX(-50%)).
6061
+ // Check if it overflows left or right and apply a corrective shift.
6062
+ const pos = isAuto ? undefined : position; // read from state won't be updated yet in auto
6063
+ const effectivePos = pos || 'top'; // fallback, actual is set by state
6064
+ const isVertical = effectivePos === 'top' ||
6065
+ effectivePos === 'bottom' ||
6066
+ resolvedPosition === 'top' ||
6067
+ resolvedPosition === 'bottom';
6068
+ if (isVertical) {
6069
+ if (contentRect.left < edgePadding) {
6070
+ // Overflows left — shift right
6071
+ const shift = edgePadding - contentRect.left;
6072
+ style.left = `calc(50% + ${shift}px)`;
6073
+ style['--tooltip-arrow-offset'] =
6074
+ `${-shift}px`;
6075
+ }
6076
+ else if (contentRect.right > window.innerWidth - edgePadding) {
6077
+ // Overflows right — shift left
6078
+ const shift = contentRect.right - (window.innerWidth - edgePadding);
6079
+ style.left = `calc(50% - ${shift}px)`;
6080
+ style['--tooltip-arrow-offset'] =
6081
+ `${shift}px`;
6082
+ }
6083
+ }
6084
+ else {
6085
+ // For left/right positions, tooltip is centered vertically via CSS
6086
+ // (top: 50%; transform: translateY(-50%)).
6087
+ if (contentRect.top < edgePadding) {
6088
+ const shift = edgePadding - contentRect.top;
6089
+ style.top = `calc(50% + ${shift}px)`;
6090
+ style['--tooltip-arrow-offset'] =
6091
+ `${-shift}px`;
6092
+ }
6093
+ else if (contentRect.bottom > window.innerHeight - edgePadding) {
6094
+ const shift = contentRect.bottom - (window.innerHeight - edgePadding);
6095
+ style.top = `calc(50% - ${shift}px)`;
6096
+ style['--tooltip-arrow-offset'] =
6097
+ `${shift}px`;
6098
+ }
6099
+ }
6100
+ setContentStyle(style);
6101
+ });
6102
+ }, [position, resolvedPosition, isVisible, alwaysActive, label, content]);
6103
+ // Generate Bulma classes
6104
+ const tooltipClasses = usePrefixedClassNames('tooltip', {
6105
+ 'is-active': alwaysActive || isVisible,
6106
+ // Open by the pointer, focus or a tap, rather than shown by `active`
6107
+ // alone: only then does the tooltip, and the gap to it, take the pointer.
6108
+ 'is-open': isVisible,
6109
+ [`is-${resolvedPosition}`]: resolvedPosition,
6110
+ [`is-${color}`]: color,
6111
+ [`is-${size}`]: size,
6112
+ 'is-multiline': multiline,
6113
+ 'is-animated': animated,
6114
+ 'is-square': square,
6115
+ 'is-dashed': dashed,
6116
+ });
6117
+ const combinedClasses = classNames(tooltipClasses, bulmaHelperClasses, className);
6118
+ const combinedTooltipClasses = usePrefixedClassNames('tooltip-content', tooltipClassName);
6119
+ 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
6120
+ ? React.cloneElement(children, {
6121
+ 'aria-describedby': withId(children.props['aria-describedby'], tooltipId),
6122
+ })
6123
+ : children, jsxRuntime.jsx("span", { ref: contentRef, id: tooltipId, className: combinedTooltipClasses,
6124
+ // Hidden by `visibility: hidden` in the stylesheet, which keeps it out
6125
+ // of the accessibility tree and still lets it describe the trigger
6126
+ // (accname step 2A), so it takes no aria-hidden.
6127
+ role: "tooltip", style: Object.keys(contentStyle).length > 0 ? contentStyle : undefined, children: content || label })] }));
6128
+ };
5954
6129
 
5955
6130
  // INTERNAL: deliberately not exported from src/index.ts.
5956
6131
  /**
@@ -6708,182 +6883,6 @@ function useContextMenuTrigger({ onOpen, disabled, }) {
6708
6883
  };
6709
6884
  }
6710
6885
 
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
6886
  /**
6888
6887
  * Where the menu goes along one axis, from the point at `at`: after it when
6889
6888
  * there's room, before it when there's room there instead, and otherwise
@@ -8573,38 +8572,6 @@ const LinkButton = React.forwardRef(function LinkButton({ variant = 'text', colo
8573
8572
  });
8574
8573
  LinkButton.displayName = 'LinkButton';
8575
8574
 
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
8575
  /**
8609
8576
  * The `ToggleButton` component renders a button that stays pressed until it's pressed again, and says whether it's on through `aria-pressed`.
8610
8577
  *
@@ -8630,7 +8597,7 @@ const Buttons = withSubComponents(ButtonsComponent, {
8630
8597
  * </ToggleButton>
8631
8598
  * </Buttons>
8632
8599
  */
8633
- const ToggleButton = React.forwardRef(function ToggleButton({ pressed: controlledPressed, defaultPressed = false, onPressedChange, color = 'link', className, onClick, ...props }, ref) {
8600
+ 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
8601
  const isControlled = controlledPressed !== undefined;
8635
8602
  const [internalPressed, setInternalPressed] = React.useState(defaultPressed);
8636
8603
  const pressed = controlledPressed ?? internalPressed;
@@ -8641,7 +8608,13 @@ const ToggleButton = React.forwardRef(function ToggleButton({ pressed: controlle
8641
8608
  // The caller's onClick runs first, so a `preventDefault()` there vetoes
8642
8609
  // the toggle, as it does in Radix, MUI and Zag. Enter and Space reach here
8643
8610
  // through the button's own click, and a disabled button gets no click.
8611
+ //
8612
+ // A loading one gets the click but drops it. Bulma's spinner only stops a
8613
+ // pointer, so without this a key flipped a toggle the mouse couldn't.
8614
+ // `disabled` would stop both, but it takes focus away mid-save.
8644
8615
  const handleClick = (event) => {
8616
+ if (isLoading)
8617
+ return;
8645
8618
  onClick?.(event);
8646
8619
  if (event.defaultPrevented)
8647
8620
  return;
@@ -8650,13 +8623,48 @@ const ToggleButton = React.forwardRef(function ToggleButton({ pressed: controlle
8650
8623
  if (!isControlled)
8651
8624
  setInternalPressed(next);
8652
8625
  };
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 }));
8626
+ // The props are spread first, so what a toggle depends on wins over
8627
+ // anything a JavaScript caller or a spread object passes along: it stays
8628
+ // a button, never submits, reports the state it's in, and says it's busy
8629
+ // and won't respond while it's loading. A caller's own `aria-busy` or
8630
+ // `aria-disabled` stays when it isn't loading.
8631
+ 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
8632
  });
8658
8633
  ToggleButton.displayName = 'ToggleButton';
8659
8634
 
8635
+ const validButtonsSizes = ['small', 'medium', 'large'];
8636
+ /**
8637
+ * The `Buttons` component lets you group multiple `Button` elements together with Bulma's spacing, alignment, and add-on features.
8638
+ *
8639
+ * @function
8640
+ * @param {ButtonsProps} props - Props for the Buttons component.
8641
+ * @returns {JSX.Element} The rendered group of buttons.
8642
+ * @see {@link https://bulma.io/documentation/elements/button/#group | Bulma Button Group documentation}
8643
+ */
8644
+ const ButtonsComponent = ({ className, textColor, color, bgColor, isCentered, isRight, hasAddons, size, children, ...props }) => {
8645
+ const buttonsClasses = usePrefixedClassNames('buttons', {
8646
+ 'is-centered': isCentered,
8647
+ 'is-right': isRight,
8648
+ 'has-addons': hasAddons,
8649
+ [`are-${size}`]: size && validButtonsSizes.includes(size),
8650
+ });
8651
+ /**
8652
+ * Generates Bulma helper classes and separates out remaining props.
8653
+ */
8654
+ const { bulmaHelperClasses, rest } = useBulmaClasses({
8655
+ color: textColor ?? color,
8656
+ backgroundColor: bgColor,
8657
+ ...props,
8658
+ });
8659
+ const combinedClasses = classNames(buttonsClasses, className, bulmaHelperClasses);
8660
+ return (jsxRuntime.jsx("div", { className: combinedClasses, ...rest, children: children }));
8661
+ };
8662
+ const Buttons = withSubComponents(ButtonsComponent, {
8663
+ Button,
8664
+ LinkButton,
8665
+ ToggleButton,
8666
+ }, 'Buttons');
8667
+
8660
8668
  /**
8661
8669
  * The `Code` component renders a styled `<code>` element with Bulma helper class integration.
8662
8670
  *