@allxsmith/bestax-bulma 5.30.0 → 5.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/components/Dropdown.js +37 -85
- package/dist/esm/components/Dropdown.js.map +1 -1
- 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 +13 -5
- package/dist/esm/elements/ToggleButton.js.map +1 -1
- package/dist/index.cjs +478 -470
- package/dist/index.cjs.map +1 -1
- 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 +13 -3
- 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 +13 -3
- 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 }) })] }));
|
|
@@ -5321,177 +5448,9 @@ function useHoverOpen(options) {
|
|
|
5321
5448
|
}
|
|
5322
5449
|
|
|
5323
5450
|
/**
|
|
5324
|
-
*
|
|
5325
|
-
*
|
|
5326
|
-
*
|
|
5327
|
-
* unless `openOnTap` is on, and neither does focus a click or a tap brings,
|
|
5328
|
-
* so tapping the trigger just does what the trigger does. The pointer can
|
|
5329
|
-
* move onto a shown tooltip. Escape hides it wherever focus is, without
|
|
5330
|
-
* moving the pointer or focus, and it stays hidden until one of them leaves
|
|
5331
|
-
* the trigger and comes back, or, with `openOnTap`, until the next tap on
|
|
5332
|
-
* the trigger. The key always goes on too, to whatever has focus or a
|
|
5333
|
-
* `Modal` or `Popover` around it, since a tooltip only adds to what's
|
|
5334
|
-
* there.
|
|
5335
|
-
*
|
|
5336
|
-
* @function
|
|
5337
|
-
* @param {TooltipProps} props - Props for the Tooltip component.
|
|
5338
|
-
* @returns {JSX.Element} The rendered tooltip wrapper.
|
|
5339
|
-
*
|
|
5340
|
-
* @example
|
|
5341
|
-
* // Basic tooltip
|
|
5342
|
-
* <Tooltip label="This is helpful info">
|
|
5343
|
-
* <button>Hover me</button>
|
|
5344
|
-
* </Tooltip>
|
|
5345
|
-
*
|
|
5346
|
-
* @example
|
|
5347
|
-
* // Positioned tooltip with color
|
|
5348
|
-
* <Tooltip label="Delete item" position="right" color="danger">
|
|
5349
|
-
* <Icon icon="fas fa-trash" />
|
|
5350
|
-
* </Tooltip>
|
|
5351
|
-
*
|
|
5352
|
-
* @example
|
|
5353
|
-
* // Multiline tooltip
|
|
5354
|
-
* <Tooltip label="This is a longer tooltip that wraps to multiple lines" multiline>
|
|
5355
|
-
* <span>?</span>
|
|
5356
|
-
* </Tooltip>
|
|
5357
|
-
*/
|
|
5358
|
-
const Tooltip = ({ label, content, position = 'top', color, size, active: alwaysActive, multiline = false, animated = true, square = false, dashed = false, delay = 0, closeDelay = 0, openOnTap = false, children, className, tooltipClassName, ...props }) => {
|
|
5359
|
-
const { bulmaHelperClasses, rest } = useBulmaClasses(props);
|
|
5360
|
-
const { onPointerEnter, onPointerOver, onPointerLeave, onPointerDown, onPointerUp, onFocus, onBlur, ...attrs } = rest;
|
|
5361
|
-
const [isVisible, setIsVisible] = React.useState(false);
|
|
5362
|
-
const [resolvedPosition, setResolvedPosition] = React.useState(position === 'auto' ? 'top' : position);
|
|
5363
|
-
const [contentStyle, setContentStyle] = React.useState({});
|
|
5364
|
-
const wrapperRef = React.useRef(null);
|
|
5365
|
-
const contentRef = React.useRef(null);
|
|
5366
|
-
const hover = useHoverOpen({
|
|
5367
|
-
// The pointer and focus as they are. `active` shows it on top of that, so
|
|
5368
|
-
// dropping `active` leaves it shown while the pointer or focus holds it.
|
|
5369
|
-
// An ask to close can come while it's already hidden, which is no change.
|
|
5370
|
-
open: isVisible,
|
|
5371
|
-
onOpenChange: setIsVisible,
|
|
5372
|
-
openDelay: delay,
|
|
5373
|
-
closeDelay,
|
|
5374
|
-
// The wrapper stands in for the trigger. It holds only the children and
|
|
5375
|
-
// the tooltip, and nothing in the tooltip takes focus, so focus anywhere
|
|
5376
|
-
// in it is focus on the trigger.
|
|
5377
|
-
triggerRef: wrapperRef,
|
|
5378
|
-
floatingRef: contentRef,
|
|
5379
|
-
// Escape hides it and carries on, so a control in the trigger, or a
|
|
5380
|
-
// Modal around it, still gets the key.
|
|
5381
|
-
consumeEscape: false,
|
|
5382
|
-
openOnTap,
|
|
5383
|
-
});
|
|
5384
|
-
// Auto-placement and overflow correction
|
|
5385
|
-
React.useLayoutEffect(() => {
|
|
5386
|
-
const isAuto = position === 'auto';
|
|
5387
|
-
if (!isAuto) {
|
|
5388
|
-
// eslint-disable-next-line react-hooks/set-state-in-effect -- layout measurement effect resolves placement
|
|
5389
|
-
setResolvedPosition(position);
|
|
5390
|
-
}
|
|
5391
|
-
const showing = isVisible || alwaysActive;
|
|
5392
|
-
if (!showing) {
|
|
5393
|
-
setContentStyle({});
|
|
5394
|
-
return;
|
|
5395
|
-
}
|
|
5396
|
-
const wrapper = wrapperRef.current;
|
|
5397
|
-
const content = contentRef.current;
|
|
5398
|
-
if (!wrapper || !content)
|
|
5399
|
-
return;
|
|
5400
|
-
const wrapperRect = wrapper.getBoundingClientRect();
|
|
5401
|
-
const margin = 40;
|
|
5402
|
-
const edgePadding = 8; // minimum distance from viewport edge
|
|
5403
|
-
// Step 1: determine position (auto mode only)
|
|
5404
|
-
if (isAuto) {
|
|
5405
|
-
const spaceTop = wrapperRect.top;
|
|
5406
|
-
const spaceBottom = window.innerHeight - wrapperRect.bottom;
|
|
5407
|
-
if (spaceTop >= margin) {
|
|
5408
|
-
setResolvedPosition('top');
|
|
5409
|
-
}
|
|
5410
|
-
else if (spaceBottom >= margin) {
|
|
5411
|
-
setResolvedPosition('bottom');
|
|
5412
|
-
}
|
|
5413
|
-
else if (window.innerWidth - wrapperRect.right >= wrapperRect.left) {
|
|
5414
|
-
setResolvedPosition('right');
|
|
5415
|
-
}
|
|
5416
|
-
else {
|
|
5417
|
-
setResolvedPosition('left');
|
|
5418
|
-
}
|
|
5419
|
-
}
|
|
5420
|
-
// Step 2: measure tooltip content and correct overflow.
|
|
5421
|
-
// Use requestAnimationFrame so the resolved position class is applied first.
|
|
5422
|
-
requestAnimationFrame(() => {
|
|
5423
|
-
const contentEl = contentRef.current;
|
|
5424
|
-
if (!contentEl)
|
|
5425
|
-
return;
|
|
5426
|
-
const contentRect = contentEl.getBoundingClientRect();
|
|
5427
|
-
const style = {};
|
|
5428
|
-
// For top/bottom positions, tooltip is centered horizontally via CSS
|
|
5429
|
-
// (left: 50%; transform: translateX(-50%)).
|
|
5430
|
-
// Check if it overflows left or right and apply a corrective shift.
|
|
5431
|
-
const pos = isAuto ? undefined : position; // read from state won't be updated yet in auto
|
|
5432
|
-
const effectivePos = pos || 'top'; // fallback, actual is set by state
|
|
5433
|
-
const isVertical = effectivePos === 'top' ||
|
|
5434
|
-
effectivePos === 'bottom' ||
|
|
5435
|
-
resolvedPosition === 'top' ||
|
|
5436
|
-
resolvedPosition === 'bottom';
|
|
5437
|
-
if (isVertical) {
|
|
5438
|
-
if (contentRect.left < edgePadding) {
|
|
5439
|
-
// Overflows left — shift right
|
|
5440
|
-
const shift = edgePadding - contentRect.left;
|
|
5441
|
-
style.left = `calc(50% + ${shift}px)`;
|
|
5442
|
-
style['--tooltip-arrow-offset'] =
|
|
5443
|
-
`${-shift}px`;
|
|
5444
|
-
}
|
|
5445
|
-
else if (contentRect.right > window.innerWidth - edgePadding) {
|
|
5446
|
-
// Overflows right — shift left
|
|
5447
|
-
const shift = contentRect.right - (window.innerWidth - edgePadding);
|
|
5448
|
-
style.left = `calc(50% - ${shift}px)`;
|
|
5449
|
-
style['--tooltip-arrow-offset'] =
|
|
5450
|
-
`${shift}px`;
|
|
5451
|
-
}
|
|
5452
|
-
}
|
|
5453
|
-
else {
|
|
5454
|
-
// For left/right positions, tooltip is centered vertically via CSS
|
|
5455
|
-
// (top: 50%; transform: translateY(-50%)).
|
|
5456
|
-
if (contentRect.top < edgePadding) {
|
|
5457
|
-
const shift = edgePadding - contentRect.top;
|
|
5458
|
-
style.top = `calc(50% + ${shift}px)`;
|
|
5459
|
-
style['--tooltip-arrow-offset'] =
|
|
5460
|
-
`${-shift}px`;
|
|
5461
|
-
}
|
|
5462
|
-
else if (contentRect.bottom > window.innerHeight - edgePadding) {
|
|
5463
|
-
const shift = contentRect.bottom - (window.innerHeight - edgePadding);
|
|
5464
|
-
style.top = `calc(50% - ${shift}px)`;
|
|
5465
|
-
style['--tooltip-arrow-offset'] =
|
|
5466
|
-
`${shift}px`;
|
|
5467
|
-
}
|
|
5468
|
-
}
|
|
5469
|
-
setContentStyle(style);
|
|
5470
|
-
});
|
|
5471
|
-
}, [position, resolvedPosition, isVisible, alwaysActive, label, content]);
|
|
5472
|
-
// Generate Bulma classes
|
|
5473
|
-
const tooltipClasses = usePrefixedClassNames('tooltip', {
|
|
5474
|
-
'is-active': alwaysActive || isVisible,
|
|
5475
|
-
// Open by the pointer, focus or a tap, rather than shown by `active`
|
|
5476
|
-
// alone: only then does the tooltip, and the gap to it, take the pointer.
|
|
5477
|
-
'is-open': isVisible,
|
|
5478
|
-
[`is-${resolvedPosition}`]: resolvedPosition,
|
|
5479
|
-
[`is-${color}`]: color,
|
|
5480
|
-
[`is-${size}`]: size,
|
|
5481
|
-
'is-multiline': multiline,
|
|
5482
|
-
'is-animated': animated,
|
|
5483
|
-
'is-square': square,
|
|
5484
|
-
'is-dashed': dashed,
|
|
5485
|
-
});
|
|
5486
|
-
const combinedClasses = classNames(tooltipClasses, bulmaHelperClasses, className);
|
|
5487
|
-
const combinedTooltipClasses = usePrefixedClassNames('tooltip-content', tooltipClassName);
|
|
5488
|
-
return (jsxRuntime.jsxs("span", { ref: wrapperRef, className: combinedClasses, onPointerEnter: chain$1(onPointerEnter, hover.onPointerEnter), onPointerOver: chain$1(onPointerOver, hover.onPointerOver), onPointerLeave: chain$1(onPointerLeave, hover.onPointerLeave), onFocus: chain$1(onFocus, hover.onFocus), onBlur: chain$1(onBlur, hover.onBlur), onPointerDown: chain$1(onPointerDown, hover.onPointerDown), onPointerUp: chain$1(onPointerUp, hover.onPointerUp), ...(label ? { 'data-tooltip': label } : {}), ...attrs, children: [children, jsxRuntime.jsx("span", { ref: contentRef, className: combinedTooltipClasses, role: "tooltip", "aria-hidden": !isVisible && !alwaysActive, style: Object.keys(contentStyle).length > 0 ? contentStyle : undefined, children: content || label })] }));
|
|
5489
|
-
};
|
|
5490
|
-
|
|
5491
|
-
/**
|
|
5492
|
-
* Whether `el` is editable while its parent is not, which makes it an editing
|
|
5493
|
-
* host: the one tab stop an editable region gets. `contenteditable="false"`
|
|
5494
|
-
* turns editing off, and a missing or unknown value inherits the parent's.
|
|
5451
|
+
* Whether `el` is editable while its parent is not, which makes it an editing
|
|
5452
|
+
* host: the one tab stop an editable region gets. `contenteditable="false"`
|
|
5453
|
+
* turns editing off, and a missing or unknown value inherits the parent's.
|
|
5495
5454
|
*/
|
|
5496
5455
|
function isEditingHost(el) {
|
|
5497
5456
|
const editable = (node) => {
|
|
@@ -5950,7 +5909,223 @@ function useFocusTrap(containerRef, options = {}) {
|
|
|
5950
5909
|
setRefChanges(n => n + 1);
|
|
5951
5910
|
}
|
|
5952
5911
|
});
|
|
5953
|
-
}
|
|
5912
|
+
}
|
|
5913
|
+
|
|
5914
|
+
/** An id list with `id` added at the end, unless it's in there already. */
|
|
5915
|
+
const withId = (ids, id) => [...new Set([...(ids ?? '').split(/\s+/), id])].filter(Boolean).join(' ');
|
|
5916
|
+
/**
|
|
5917
|
+
* The first element in the trigger that Tab lands on, judged by what it is
|
|
5918
|
+
* rather than by its styles, which can hide it for now. The trigger is all
|
|
5919
|
+
* of the wrapper that comes before the tooltip.
|
|
5920
|
+
*/
|
|
5921
|
+
const triggerTabStop = (wrapper, tooltip) => {
|
|
5922
|
+
const elements = Array.from(wrapper.querySelectorAll('*'));
|
|
5923
|
+
return elements
|
|
5924
|
+
.slice(0, elements.indexOf(tooltip))
|
|
5925
|
+
.find(el => isTabbableKind(el) && !el.matches(':disabled'));
|
|
5926
|
+
};
|
|
5927
|
+
/**
|
|
5928
|
+
* The `Tooltip` component displays helpful information when users hover over or focus on an element.
|
|
5929
|
+
*
|
|
5930
|
+
* Hovering the trigger or giving it keyboard focus shows it. Touch doesn't,
|
|
5931
|
+
* unless `openOnTap` is on, and neither does focus a click or a tap brings,
|
|
5932
|
+
* so tapping the trigger just does what the trigger does. The pointer can
|
|
5933
|
+
* move onto a shown tooltip. Escape hides it wherever focus is, without
|
|
5934
|
+
* moving the pointer or focus, and it stays hidden until one of them leaves
|
|
5935
|
+
* the trigger and comes back, or, with `openOnTap`, until the next tap on
|
|
5936
|
+
* the trigger. The key always goes on too, to whatever has focus or a
|
|
5937
|
+
* `Modal` or `Popover` around it, since a tooltip only adds to what's
|
|
5938
|
+
* there. The tooltip is its trigger's description, so a screen reader reads
|
|
5939
|
+
* it as the trigger takes focus, whether it's showing yet or not.
|
|
5940
|
+
*
|
|
5941
|
+
* @function
|
|
5942
|
+
* @param {TooltipProps} props - Props for the Tooltip component.
|
|
5943
|
+
* @returns {JSX.Element} The rendered tooltip wrapper.
|
|
5944
|
+
*
|
|
5945
|
+
* @example
|
|
5946
|
+
* // Basic tooltip
|
|
5947
|
+
* <Tooltip label="This is helpful info">
|
|
5948
|
+
* <button>Hover me</button>
|
|
5949
|
+
* </Tooltip>
|
|
5950
|
+
*
|
|
5951
|
+
* @example
|
|
5952
|
+
* // Positioned tooltip with color, on an icon-only button with a name of its own
|
|
5953
|
+
* <Tooltip label="Moves it to the trash" position="right" color="danger">
|
|
5954
|
+
* <Button aria-label="Delete">
|
|
5955
|
+
* <Icon name="fas fa-trash" />
|
|
5956
|
+
* </Button>
|
|
5957
|
+
* </Tooltip>
|
|
5958
|
+
*
|
|
5959
|
+
* @example
|
|
5960
|
+
* // Multiline tooltip
|
|
5961
|
+
* <Tooltip label="This is a longer tooltip that wraps to multiple lines" multiline>
|
|
5962
|
+
* <button>More info</button>
|
|
5963
|
+
* </Tooltip>
|
|
5964
|
+
*/
|
|
5965
|
+
const Tooltip = ({ label, content, position = 'top', color, size, active: alwaysActive, multiline = false, animated = true, square = false, dashed = false, delay = 0, closeDelay = 0, openOnTap = false, children, className, tooltipClassName, ...props }) => {
|
|
5966
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses(props);
|
|
5967
|
+
const { onPointerEnter, onPointerOver, onPointerLeave, onPointerDown, onPointerUp, onFocus, onBlur, ...attrs } = rest;
|
|
5968
|
+
const [isVisible, setIsVisible] = React.useState(false);
|
|
5969
|
+
const [resolvedPosition, setResolvedPosition] = React.useState(position === 'auto' ? 'top' : position);
|
|
5970
|
+
const [contentStyle, setContentStyle] = React.useState({});
|
|
5971
|
+
const wrapperRef = React.useRef(null);
|
|
5972
|
+
const contentRef = React.useRef(null);
|
|
5973
|
+
const tooltipId = `${React.useId()}-tooltip`;
|
|
5974
|
+
// Only a single element can be given the link, and a fragment takes no
|
|
5975
|
+
// props of its own.
|
|
5976
|
+
const linkable = React.isValidElement(children) &&
|
|
5977
|
+
children.type !== React.Fragment;
|
|
5978
|
+
// A screen reader reads the tooltip only when Tab lands on an element that
|
|
5979
|
+
// carries the link. Only the page shows whether it does, since a component
|
|
5980
|
+
// may not pass the attribute on, or what takes focus may sit inside the
|
|
5981
|
+
// element that took it. After every commit, since the children can change,
|
|
5982
|
+
// and only in development.
|
|
5983
|
+
React.useEffect(() => {
|
|
5984
|
+
if (!isDev())
|
|
5985
|
+
return;
|
|
5986
|
+
const stop = triggerTabStop(wrapperRef.current, contentRef.current);
|
|
5987
|
+
if (stop?.getAttribute('aria-describedby')?.split(/\s+/).includes(tooltipId)) {
|
|
5988
|
+
return;
|
|
5989
|
+
}
|
|
5990
|
+
warnOnce('Tooltip:unlinked-trigger', "[bestax-bulma] <Tooltip> can't describe its trigger, so a screen " +
|
|
5991
|
+
"reader won't read the tooltip: Tab reaches nothing in the trigger, " +
|
|
5992
|
+
"or what it reaches doesn't carry the aria-describedby that points " +
|
|
5993
|
+
'at the tooltip. Pass one element that takes focus and passes ' +
|
|
5994
|
+
'aria-describedby on to its DOM node, such as a button, a link or a ' +
|
|
5995
|
+
'Span with tabIndex={0}.');
|
|
5996
|
+
});
|
|
5997
|
+
const hover = useHoverOpen({
|
|
5998
|
+
// The pointer and focus as they are. `active` shows it on top of that, so
|
|
5999
|
+
// dropping `active` leaves it shown while the pointer or focus holds it.
|
|
6000
|
+
// An ask to close can come while it's already hidden, which is no change.
|
|
6001
|
+
open: isVisible,
|
|
6002
|
+
onOpenChange: setIsVisible,
|
|
6003
|
+
openDelay: delay,
|
|
6004
|
+
closeDelay,
|
|
6005
|
+
// The wrapper stands in for the trigger. It holds only the children and
|
|
6006
|
+
// the tooltip, and nothing in the tooltip takes focus, so focus anywhere
|
|
6007
|
+
// in it is focus on the trigger.
|
|
6008
|
+
triggerRef: wrapperRef,
|
|
6009
|
+
floatingRef: contentRef,
|
|
6010
|
+
// Escape hides it and carries on, so a control in the trigger, or a
|
|
6011
|
+
// Modal around it, still gets the key.
|
|
6012
|
+
consumeEscape: false,
|
|
6013
|
+
openOnTap,
|
|
6014
|
+
});
|
|
6015
|
+
// Auto-placement and overflow correction
|
|
6016
|
+
React.useLayoutEffect(() => {
|
|
6017
|
+
const isAuto = position === 'auto';
|
|
6018
|
+
if (!isAuto) {
|
|
6019
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect -- layout measurement effect resolves placement
|
|
6020
|
+
setResolvedPosition(position);
|
|
6021
|
+
}
|
|
6022
|
+
const showing = isVisible || alwaysActive;
|
|
6023
|
+
if (!showing) {
|
|
6024
|
+
setContentStyle({});
|
|
6025
|
+
return;
|
|
6026
|
+
}
|
|
6027
|
+
const wrapper = wrapperRef.current;
|
|
6028
|
+
const content = contentRef.current;
|
|
6029
|
+
if (!wrapper || !content)
|
|
6030
|
+
return;
|
|
6031
|
+
const wrapperRect = wrapper.getBoundingClientRect();
|
|
6032
|
+
const margin = 40;
|
|
6033
|
+
const edgePadding = 8; // minimum distance from viewport edge
|
|
6034
|
+
// Step 1: determine position (auto mode only)
|
|
6035
|
+
if (isAuto) {
|
|
6036
|
+
const spaceTop = wrapperRect.top;
|
|
6037
|
+
const spaceBottom = window.innerHeight - wrapperRect.bottom;
|
|
6038
|
+
if (spaceTop >= margin) {
|
|
6039
|
+
setResolvedPosition('top');
|
|
6040
|
+
}
|
|
6041
|
+
else if (spaceBottom >= margin) {
|
|
6042
|
+
setResolvedPosition('bottom');
|
|
6043
|
+
}
|
|
6044
|
+
else if (window.innerWidth - wrapperRect.right >= wrapperRect.left) {
|
|
6045
|
+
setResolvedPosition('right');
|
|
6046
|
+
}
|
|
6047
|
+
else {
|
|
6048
|
+
setResolvedPosition('left');
|
|
6049
|
+
}
|
|
6050
|
+
}
|
|
6051
|
+
// Step 2: measure tooltip content and correct overflow.
|
|
6052
|
+
// Use requestAnimationFrame so the resolved position class is applied first.
|
|
6053
|
+
requestAnimationFrame(() => {
|
|
6054
|
+
const contentEl = contentRef.current;
|
|
6055
|
+
if (!contentEl)
|
|
6056
|
+
return;
|
|
6057
|
+
const contentRect = contentEl.getBoundingClientRect();
|
|
6058
|
+
const style = {};
|
|
6059
|
+
// For top/bottom positions, tooltip is centered horizontally via CSS
|
|
6060
|
+
// (left: 50%; transform: translateX(-50%)).
|
|
6061
|
+
// Check if it overflows left or right and apply a corrective shift.
|
|
6062
|
+
const pos = isAuto ? undefined : position; // read from state won't be updated yet in auto
|
|
6063
|
+
const effectivePos = pos || 'top'; // fallback, actual is set by state
|
|
6064
|
+
const isVertical = effectivePos === 'top' ||
|
|
6065
|
+
effectivePos === 'bottom' ||
|
|
6066
|
+
resolvedPosition === 'top' ||
|
|
6067
|
+
resolvedPosition === 'bottom';
|
|
6068
|
+
if (isVertical) {
|
|
6069
|
+
if (contentRect.left < edgePadding) {
|
|
6070
|
+
// Overflows left — shift right
|
|
6071
|
+
const shift = edgePadding - contentRect.left;
|
|
6072
|
+
style.left = `calc(50% + ${shift}px)`;
|
|
6073
|
+
style['--tooltip-arrow-offset'] =
|
|
6074
|
+
`${-shift}px`;
|
|
6075
|
+
}
|
|
6076
|
+
else if (contentRect.right > window.innerWidth - edgePadding) {
|
|
6077
|
+
// Overflows right — shift left
|
|
6078
|
+
const shift = contentRect.right - (window.innerWidth - edgePadding);
|
|
6079
|
+
style.left = `calc(50% - ${shift}px)`;
|
|
6080
|
+
style['--tooltip-arrow-offset'] =
|
|
6081
|
+
`${shift}px`;
|
|
6082
|
+
}
|
|
6083
|
+
}
|
|
6084
|
+
else {
|
|
6085
|
+
// For left/right positions, tooltip is centered vertically via CSS
|
|
6086
|
+
// (top: 50%; transform: translateY(-50%)).
|
|
6087
|
+
if (contentRect.top < edgePadding) {
|
|
6088
|
+
const shift = edgePadding - contentRect.top;
|
|
6089
|
+
style.top = `calc(50% + ${shift}px)`;
|
|
6090
|
+
style['--tooltip-arrow-offset'] =
|
|
6091
|
+
`${-shift}px`;
|
|
6092
|
+
}
|
|
6093
|
+
else if (contentRect.bottom > window.innerHeight - edgePadding) {
|
|
6094
|
+
const shift = contentRect.bottom - (window.innerHeight - edgePadding);
|
|
6095
|
+
style.top = `calc(50% - ${shift}px)`;
|
|
6096
|
+
style['--tooltip-arrow-offset'] =
|
|
6097
|
+
`${shift}px`;
|
|
6098
|
+
}
|
|
6099
|
+
}
|
|
6100
|
+
setContentStyle(style);
|
|
6101
|
+
});
|
|
6102
|
+
}, [position, resolvedPosition, isVisible, alwaysActive, label, content]);
|
|
6103
|
+
// Generate Bulma classes
|
|
6104
|
+
const tooltipClasses = usePrefixedClassNames('tooltip', {
|
|
6105
|
+
'is-active': alwaysActive || isVisible,
|
|
6106
|
+
// Open by the pointer, focus or a tap, rather than shown by `active`
|
|
6107
|
+
// alone: only then does the tooltip, and the gap to it, take the pointer.
|
|
6108
|
+
'is-open': isVisible,
|
|
6109
|
+
[`is-${resolvedPosition}`]: resolvedPosition,
|
|
6110
|
+
[`is-${color}`]: color,
|
|
6111
|
+
[`is-${size}`]: size,
|
|
6112
|
+
'is-multiline': multiline,
|
|
6113
|
+
'is-animated': animated,
|
|
6114
|
+
'is-square': square,
|
|
6115
|
+
'is-dashed': dashed,
|
|
6116
|
+
});
|
|
6117
|
+
const combinedClasses = classNames(tooltipClasses, bulmaHelperClasses, className);
|
|
6118
|
+
const combinedTooltipClasses = usePrefixedClassNames('tooltip-content', tooltipClassName);
|
|
6119
|
+
return (jsxRuntime.jsxs("span", { ref: wrapperRef, className: combinedClasses, onPointerEnter: chain$1(onPointerEnter, hover.onPointerEnter), onPointerOver: chain$1(onPointerOver, hover.onPointerOver), onPointerLeave: chain$1(onPointerLeave, hover.onPointerLeave), onFocus: chain$1(onFocus, hover.onFocus), onBlur: chain$1(onBlur, hover.onBlur), onPointerDown: chain$1(onPointerDown, hover.onPointerDown), onPointerUp: chain$1(onPointerUp, hover.onPointerUp), ...(label ? { 'data-tooltip': label } : {}), ...attrs, children: [linkable
|
|
6120
|
+
? React.cloneElement(children, {
|
|
6121
|
+
'aria-describedby': withId(children.props['aria-describedby'], tooltipId),
|
|
6122
|
+
})
|
|
6123
|
+
: children, jsxRuntime.jsx("span", { ref: contentRef, id: tooltipId, className: combinedTooltipClasses,
|
|
6124
|
+
// Hidden by `visibility: hidden` in the stylesheet, which keeps it out
|
|
6125
|
+
// of the accessibility tree and still lets it describe the trigger
|
|
6126
|
+
// (accname step 2A), so it takes no aria-hidden.
|
|
6127
|
+
role: "tooltip", style: Object.keys(contentStyle).length > 0 ? contentStyle : undefined, children: content || label })] }));
|
|
6128
|
+
};
|
|
5954
6129
|
|
|
5955
6130
|
// INTERNAL: deliberately not exported from src/index.ts.
|
|
5956
6131
|
/**
|
|
@@ -6708,182 +6883,6 @@ function useContextMenuTrigger({ onOpen, disabled, }) {
|
|
|
6708
6883
|
};
|
|
6709
6884
|
}
|
|
6710
6885
|
|
|
6711
|
-
// INTERNAL: deliberately not exported from src/index.ts.
|
|
6712
|
-
/**
|
|
6713
|
-
* Every menu-item role, so an item a caller gives `menuitemcheckbox` or
|
|
6714
|
-
* `menuitemradio` (a filter or sort menu) stays reachable.
|
|
6715
|
-
*/
|
|
6716
|
-
const MENU_ITEM_SELECTOR = '[role="menuitem"], [role="menuitemcheckbox"], [role="menuitemradio"]';
|
|
6717
|
-
/**
|
|
6718
|
-
* Whether a menu item can be reached and activated: it isn't disabled, as a
|
|
6719
|
-
* native control or through `aria-disabled="true"`.
|
|
6720
|
-
*/
|
|
6721
|
-
function isEnabledMenuItem(item) {
|
|
6722
|
-
return (!item.hasAttribute('disabled') &&
|
|
6723
|
-
item.getAttribute('aria-disabled') !== 'true');
|
|
6724
|
-
}
|
|
6725
|
-
/** Whether `node` is one of `menu`'s items, enabled or not. */
|
|
6726
|
-
const isItemOf = (menu, node) => Boolean(node && menu.contains(node) && node.matches(MENU_ITEM_SELECTOR));
|
|
6727
|
-
/** The items in `menu` the keys reach, in document order. */
|
|
6728
|
-
function enabledMenuItems(menu) {
|
|
6729
|
-
return Array.from(menu.querySelectorAll(MENU_ITEM_SELECTOR)).filter(isEnabledMenuItem);
|
|
6730
|
-
}
|
|
6731
|
-
/** How long typed characters keep adding up to one search, in milliseconds. */
|
|
6732
|
-
const TYPEAHEAD_TIMEOUT = 500;
|
|
6733
|
-
/**
|
|
6734
|
-
* Whether the browser activates `item` on this key by itself: a `<button>`
|
|
6735
|
-
* answers Enter and Space, and a link with an `href` answers Enter.
|
|
6736
|
-
*/
|
|
6737
|
-
function activatesItself(item, key) {
|
|
6738
|
-
if (item.tagName === 'BUTTON')
|
|
6739
|
-
return true;
|
|
6740
|
-
return key === 'Enter' && item.tagName === 'A' && item.hasAttribute('href');
|
|
6741
|
-
}
|
|
6742
|
-
/**
|
|
6743
|
-
* The text typing finds an item by: what it says, without what's hidden from
|
|
6744
|
-
* assistive technology, such as an icon whose glyph is a ligature's name.
|
|
6745
|
-
*/
|
|
6746
|
-
function label(item) {
|
|
6747
|
-
let text = '';
|
|
6748
|
-
const read = (node) => {
|
|
6749
|
-
node.childNodes.forEach(child => {
|
|
6750
|
-
if (child.nodeType === 3)
|
|
6751
|
-
text += child.textContent;
|
|
6752
|
-
else if (child.nodeType === 1 &&
|
|
6753
|
-
child.getAttribute('aria-hidden') !== 'true') {
|
|
6754
|
-
read(child);
|
|
6755
|
-
}
|
|
6756
|
-
});
|
|
6757
|
-
};
|
|
6758
|
-
read(item);
|
|
6759
|
-
return text.trim().toLowerCase();
|
|
6760
|
-
}
|
|
6761
|
-
/**
|
|
6762
|
-
* The item typing `search` moves to, or none. A single character looks from
|
|
6763
|
-
* the item after the focused one, so typing the same letter again cycles
|
|
6764
|
-
* through the items that start with it, and so does a run of one letter
|
|
6765
|
-
* ("dd"). A longer search looks from the focused item, so typing more of
|
|
6766
|
-
* its own text keeps it.
|
|
6767
|
-
*/
|
|
6768
|
-
function typeaheadMatch(items, search, current) {
|
|
6769
|
-
const repeated = Array.from(search).every(char => char === search[0]);
|
|
6770
|
-
const wanted = repeated ? search[0] : search;
|
|
6771
|
-
const start = current < 0 ? 0 : current + (wanted.length === 1 ? 1 : 0);
|
|
6772
|
-
for (let i = 0; i < items.length; i += 1) {
|
|
6773
|
-
const item = items[(start + i) % items.length];
|
|
6774
|
-
if (label(item).startsWith(wanted))
|
|
6775
|
-
return item;
|
|
6776
|
-
}
|
|
6777
|
-
return undefined;
|
|
6778
|
-
}
|
|
6779
|
-
/**
|
|
6780
|
-
* Moves through and activates the items of a `role="menu"` element from the
|
|
6781
|
-
* keyboard. Put the `onKeyDown` it returns on that element. It reads the
|
|
6782
|
-
* items from the element each time, so items that come and go are found,
|
|
6783
|
-
* and it renders nothing.
|
|
6784
|
-
*
|
|
6785
|
-
* It acts only while focus is on the menu element itself or on one of its
|
|
6786
|
-
* items, so anything else focusable inside the menu, such as a filter
|
|
6787
|
-
* field, keeps its own keys.
|
|
6788
|
-
*
|
|
6789
|
-
* - Down and Up move to the next and previous enabled item, wrapping, and
|
|
6790
|
-
* Home and End go to the first and last. From no item, Down goes to the
|
|
6791
|
-
* first and Up to the last.
|
|
6792
|
-
* - Enter and Space activate the focused item. A `<button>`, and a link with
|
|
6793
|
-
* an `href` for Enter, get the browser's own click, and anything else gets
|
|
6794
|
-
* one from here, so its `onClick` runs either way. A key held down
|
|
6795
|
-
* activates it once. A key handler on the item that called
|
|
6796
|
-
* `preventDefault()` has claimed the key, and nothing else runs it.
|
|
6797
|
-
* - A printable character moves to the next enabled item whose text starts
|
|
6798
|
-
* with what has been typed. Characters typed within `TYPEAHEAD_TIMEOUT` of
|
|
6799
|
-
* each other add up to one search, until `reset` starts a new one.
|
|
6800
|
-
*
|
|
6801
|
-
* Items are `MENU_ITEM_SELECTOR` matches, and disabled ones are skipped by
|
|
6802
|
-
* all of the above. Escape and Tab are left to the menu's owner, since what
|
|
6803
|
-
* closing means is up to it, and so is any other key.
|
|
6804
|
-
*
|
|
6805
|
-
* @function useMenuKeys
|
|
6806
|
-
* @returns The `keydown` handler for the menu element, and a way to forget
|
|
6807
|
-
* what has been typed.
|
|
6808
|
-
*/
|
|
6809
|
-
function useMenuKeys() {
|
|
6810
|
-
const typedRef = React.useRef({ text: '' });
|
|
6811
|
-
React.useEffect(() => {
|
|
6812
|
-
const typed = typedRef.current;
|
|
6813
|
-
return () => clearTimeout(typed.timer);
|
|
6814
|
-
}, []);
|
|
6815
|
-
return React.useMemo(() => {
|
|
6816
|
-
const reset = () => {
|
|
6817
|
-
const typed = typedRef.current;
|
|
6818
|
-
clearTimeout(typed.timer);
|
|
6819
|
-
typed.text = '';
|
|
6820
|
-
};
|
|
6821
|
-
const onKeyDown = (event) => {
|
|
6822
|
-
const menu = event.currentTarget;
|
|
6823
|
-
const focused = getActiveElementInTree(menu);
|
|
6824
|
-
if (focused !== menu && !isItemOf(menu, focused))
|
|
6825
|
-
return;
|
|
6826
|
-
const items = enabledMenuItems(menu);
|
|
6827
|
-
if (!items.length)
|
|
6828
|
-
return;
|
|
6829
|
-
const current = items.indexOf(focused);
|
|
6830
|
-
switch (event.key) {
|
|
6831
|
-
case 'ArrowDown':
|
|
6832
|
-
event.preventDefault();
|
|
6833
|
-
items[current >= 0 ? (current + 1) % items.length : 0].focus();
|
|
6834
|
-
return;
|
|
6835
|
-
case 'ArrowUp':
|
|
6836
|
-
event.preventDefault();
|
|
6837
|
-
items[current >= 0
|
|
6838
|
-
? (current - 1 + items.length) % items.length
|
|
6839
|
-
: items.length - 1].focus();
|
|
6840
|
-
return;
|
|
6841
|
-
case 'Home':
|
|
6842
|
-
event.preventDefault();
|
|
6843
|
-
items[0].focus();
|
|
6844
|
-
return;
|
|
6845
|
-
case 'End':
|
|
6846
|
-
event.preventDefault();
|
|
6847
|
-
items[items.length - 1].focus();
|
|
6848
|
-
return;
|
|
6849
|
-
case 'Enter':
|
|
6850
|
-
case ' ': {
|
|
6851
|
-
if (current < 0 || event.defaultPrevented)
|
|
6852
|
-
return;
|
|
6853
|
-
const item = items[current];
|
|
6854
|
-
if (activatesItself(item, event.key)) {
|
|
6855
|
-
// The browser clicks on Enter's keydown, and a held key sends one
|
|
6856
|
-
// per auto-repeat. Space clicks on keyup, once however long it's
|
|
6857
|
-
// held.
|
|
6858
|
-
if (event.repeat && event.key === 'Enter')
|
|
6859
|
-
event.preventDefault();
|
|
6860
|
-
return;
|
|
6861
|
-
}
|
|
6862
|
-
// Space would scroll the page, and a held key repeats.
|
|
6863
|
-
event.preventDefault();
|
|
6864
|
-
if (!event.repeat)
|
|
6865
|
-
item.click();
|
|
6866
|
-
return;
|
|
6867
|
-
}
|
|
6868
|
-
}
|
|
6869
|
-
if (event.key.length !== 1 ||
|
|
6870
|
-
event.ctrlKey ||
|
|
6871
|
-
event.metaKey ||
|
|
6872
|
-
event.altKey ||
|
|
6873
|
-
event.defaultPrevented) {
|
|
6874
|
-
return;
|
|
6875
|
-
}
|
|
6876
|
-
event.preventDefault();
|
|
6877
|
-
const typed = typedRef.current;
|
|
6878
|
-
typed.text += event.key.toLowerCase();
|
|
6879
|
-
clearTimeout(typed.timer);
|
|
6880
|
-
typed.timer = setTimeout(reset, TYPEAHEAD_TIMEOUT);
|
|
6881
|
-
typeaheadMatch(items, typed.text, current)?.focus();
|
|
6882
|
-
};
|
|
6883
|
-
return { onKeyDown, reset };
|
|
6884
|
-
}, []);
|
|
6885
|
-
}
|
|
6886
|
-
|
|
6887
6886
|
/**
|
|
6888
6887
|
* Where the menu goes along one axis, from the point at `at`: after it when
|
|
6889
6888
|
* there's room, before it when there's room there instead, and otherwise
|
|
@@ -8573,38 +8572,6 @@ const LinkButton = React.forwardRef(function LinkButton({ variant = 'text', colo
|
|
|
8573
8572
|
});
|
|
8574
8573
|
LinkButton.displayName = 'LinkButton';
|
|
8575
8574
|
|
|
8576
|
-
const validButtonsSizes = ['small', 'medium', 'large'];
|
|
8577
|
-
/**
|
|
8578
|
-
* The `Buttons` component lets you group multiple `Button` elements together with Bulma's spacing, alignment, and add-on features.
|
|
8579
|
-
*
|
|
8580
|
-
* @function
|
|
8581
|
-
* @param {ButtonsProps} props - Props for the Buttons component.
|
|
8582
|
-
* @returns {JSX.Element} The rendered group of buttons.
|
|
8583
|
-
* @see {@link https://bulma.io/documentation/elements/button/#group | Bulma Button Group documentation}
|
|
8584
|
-
*/
|
|
8585
|
-
const ButtonsComponent = ({ className, textColor, color, bgColor, isCentered, isRight, hasAddons, size, children, ...props }) => {
|
|
8586
|
-
const buttonsClasses = usePrefixedClassNames('buttons', {
|
|
8587
|
-
'is-centered': isCentered,
|
|
8588
|
-
'is-right': isRight,
|
|
8589
|
-
'has-addons': hasAddons,
|
|
8590
|
-
[`are-${size}`]: size && validButtonsSizes.includes(size),
|
|
8591
|
-
});
|
|
8592
|
-
/**
|
|
8593
|
-
* Generates Bulma helper classes and separates out remaining props.
|
|
8594
|
-
*/
|
|
8595
|
-
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
8596
|
-
color: textColor ?? color,
|
|
8597
|
-
backgroundColor: bgColor,
|
|
8598
|
-
...props,
|
|
8599
|
-
});
|
|
8600
|
-
const combinedClasses = classNames(buttonsClasses, className, bulmaHelperClasses);
|
|
8601
|
-
return (jsxRuntime.jsx("div", { className: combinedClasses, ...rest, children: children }));
|
|
8602
|
-
};
|
|
8603
|
-
const Buttons = withSubComponents(ButtonsComponent, {
|
|
8604
|
-
Button,
|
|
8605
|
-
LinkButton,
|
|
8606
|
-
}, 'Buttons');
|
|
8607
|
-
|
|
8608
8575
|
/**
|
|
8609
8576
|
* The `ToggleButton` component renders a button that stays pressed until it's pressed again, and says whether it's on through `aria-pressed`.
|
|
8610
8577
|
*
|
|
@@ -8630,7 +8597,7 @@ const Buttons = withSubComponents(ButtonsComponent, {
|
|
|
8630
8597
|
* </ToggleButton>
|
|
8631
8598
|
* </Buttons>
|
|
8632
8599
|
*/
|
|
8633
|
-
const ToggleButton = React.forwardRef(function ToggleButton({ pressed: controlledPressed, defaultPressed = false, onPressedChange, color = 'link', className, onClick, ...props }, ref) {
|
|
8600
|
+
const ToggleButton = React.forwardRef(function ToggleButton({ pressed: controlledPressed, defaultPressed = false, onPressedChange, color = 'link', isLoading, 'aria-busy': ariaBusy, 'aria-disabled': ariaDisabled, className, onClick, ...props }, ref) {
|
|
8634
8601
|
const isControlled = controlledPressed !== undefined;
|
|
8635
8602
|
const [internalPressed, setInternalPressed] = React.useState(defaultPressed);
|
|
8636
8603
|
const pressed = controlledPressed ?? internalPressed;
|
|
@@ -8641,7 +8608,13 @@ const ToggleButton = React.forwardRef(function ToggleButton({ pressed: controlle
|
|
|
8641
8608
|
// The caller's onClick runs first, so a `preventDefault()` there vetoes
|
|
8642
8609
|
// the toggle, as it does in Radix, MUI and Zag. Enter and Space reach here
|
|
8643
8610
|
// through the button's own click, and a disabled button gets no click.
|
|
8611
|
+
//
|
|
8612
|
+
// A loading one gets the click but drops it. Bulma's spinner only stops a
|
|
8613
|
+
// pointer, so without this a key flipped a toggle the mouse couldn't.
|
|
8614
|
+
// `disabled` would stop both, but it takes focus away mid-save.
|
|
8644
8615
|
const handleClick = (event) => {
|
|
8616
|
+
if (isLoading)
|
|
8617
|
+
return;
|
|
8645
8618
|
onClick?.(event);
|
|
8646
8619
|
if (event.defaultPrevented)
|
|
8647
8620
|
return;
|
|
@@ -8650,13 +8623,48 @@ const ToggleButton = React.forwardRef(function ToggleButton({ pressed: controlle
|
|
|
8650
8623
|
if (!isControlled)
|
|
8651
8624
|
setInternalPressed(next);
|
|
8652
8625
|
};
|
|
8653
|
-
// The props are spread first, so
|
|
8654
|
-
//
|
|
8655
|
-
//
|
|
8656
|
-
|
|
8626
|
+
// The props are spread first, so what a toggle depends on wins over
|
|
8627
|
+
// anything a JavaScript caller or a spread object passes along: it stays
|
|
8628
|
+
// a button, never submits, reports the state it's in, and says it's busy
|
|
8629
|
+
// and won't respond while it's loading. A caller's own `aria-busy` or
|
|
8630
|
+
// `aria-disabled` stays when it isn't loading.
|
|
8631
|
+
return (jsxRuntime.jsx(Button, { ...props, ref: ref, as: "button", type: "button", isLoading: isLoading, "aria-busy": isLoading || ariaBusy, "aria-disabled": isLoading || ariaDisabled, "aria-pressed": pressed, color: pressed ? color : undefined, className: classNames(selectedClass, className), onClick: handleClick }));
|
|
8657
8632
|
});
|
|
8658
8633
|
ToggleButton.displayName = 'ToggleButton';
|
|
8659
8634
|
|
|
8635
|
+
const validButtonsSizes = ['small', 'medium', 'large'];
|
|
8636
|
+
/**
|
|
8637
|
+
* The `Buttons` component lets you group multiple `Button` elements together with Bulma's spacing, alignment, and add-on features.
|
|
8638
|
+
*
|
|
8639
|
+
* @function
|
|
8640
|
+
* @param {ButtonsProps} props - Props for the Buttons component.
|
|
8641
|
+
* @returns {JSX.Element} The rendered group of buttons.
|
|
8642
|
+
* @see {@link https://bulma.io/documentation/elements/button/#group | Bulma Button Group documentation}
|
|
8643
|
+
*/
|
|
8644
|
+
const ButtonsComponent = ({ className, textColor, color, bgColor, isCentered, isRight, hasAddons, size, children, ...props }) => {
|
|
8645
|
+
const buttonsClasses = usePrefixedClassNames('buttons', {
|
|
8646
|
+
'is-centered': isCentered,
|
|
8647
|
+
'is-right': isRight,
|
|
8648
|
+
'has-addons': hasAddons,
|
|
8649
|
+
[`are-${size}`]: size && validButtonsSizes.includes(size),
|
|
8650
|
+
});
|
|
8651
|
+
/**
|
|
8652
|
+
* Generates Bulma helper classes and separates out remaining props.
|
|
8653
|
+
*/
|
|
8654
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
8655
|
+
color: textColor ?? color,
|
|
8656
|
+
backgroundColor: bgColor,
|
|
8657
|
+
...props,
|
|
8658
|
+
});
|
|
8659
|
+
const combinedClasses = classNames(buttonsClasses, className, bulmaHelperClasses);
|
|
8660
|
+
return (jsxRuntime.jsx("div", { className: combinedClasses, ...rest, children: children }));
|
|
8661
|
+
};
|
|
8662
|
+
const Buttons = withSubComponents(ButtonsComponent, {
|
|
8663
|
+
Button,
|
|
8664
|
+
LinkButton,
|
|
8665
|
+
ToggleButton,
|
|
8666
|
+
}, 'Buttons');
|
|
8667
|
+
|
|
8660
8668
|
/**
|
|
8661
8669
|
* The `Code` component renders a styled `<code>` element with Bulma helper class integration.
|
|
8662
8670
|
*
|