@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/esm/components/Dropdown.js +37 -85
- package/dist/esm/components/Dropdown.js.map +1 -1
- package/dist/esm/components/SegmentedControl.js +230 -0
- package/dist/esm/components/SegmentedControl.js.map +1 -0
- package/dist/esm/components/Tooltip.js +57 -7
- package/dist/esm/components/Tooltip.js.map +1 -1
- package/dist/esm/elements/Buttons.js +2 -0
- package/dist/esm/elements/Buttons.js.map +1 -1
- package/dist/esm/elements/ToggleButton.js +15 -6
- package/dist/esm/elements/ToggleButton.js.map +1 -1
- package/dist/index.cjs +882 -654
- package/dist/index.cjs.map +1 -1
- package/dist/index.esm.js +1 -0
- package/dist/index.esm.js.map +1 -1
- package/dist/types/components/SegmentedControl.d.ts +116 -0
- package/dist/types/components/Tooltip.d.ts +20 -7
- package/dist/types/elements/Buttons.d.ts +1 -0
- package/dist/types/elements/ToggleButton.d.ts +15 -4
- package/dist/types/index.d.ts +1 -0
- package/dist/types-cjs/components/SegmentedControl.d.ts +116 -0
- package/dist/types-cjs/components/Tooltip.d.ts +20 -7
- package/dist/types-cjs/elements/Buttons.d.ts +1 -0
- package/dist/types-cjs/elements/ToggleButton.d.ts +15 -4
- package/dist/types-cjs/index.d.ts +1 -0
- package/package.json +1 -1
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
|
|
2466
|
-
|
|
2467
|
-
|
|
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
|
-
//
|
|
2476
|
-
//
|
|
2477
|
-
// `
|
|
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
|
|
2665
|
+
if (!active)
|
|
2480
2666
|
return;
|
|
2481
|
-
|
|
2482
|
-
if (
|
|
2483
|
-
|
|
2484
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2549
|
-
//
|
|
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
|
-
|
|
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 `
|
|
4650
|
+
* The `Button` component provides a flexible and highly customizable button for your Bulma React UI.
|
|
4523
4651
|
*
|
|
4524
4652
|
* @function
|
|
4525
|
-
* @param {
|
|
4526
|
-
* @
|
|
4527
|
-
*
|
|
4528
|
-
* @
|
|
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
|
|
4551
|
-
const {
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
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
|
-
|
|
4561
|
-
|
|
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
|
|
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
|
|
8654
|
-
//
|
|
8655
|
-
//
|
|
8656
|
-
|
|
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;
|