@astryxdesign/core 0.4.1 → 0.4.2-canary.356d2f9
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/CHANGELOG.md +55 -0
- package/dist/Avatar/Avatar.d.ts +4 -1
- package/dist/Avatar/Avatar.d.ts.map +1 -1
- package/dist/Avatar/Avatar.js +19 -23
- package/dist/AvatarGroup/AvatarGroupOverflow.d.ts.map +1 -1
- package/dist/AvatarGroup/AvatarGroupOverflow.js +7 -2
- package/dist/Button/Button.d.ts.map +1 -1
- package/dist/Button/Button.js +9 -7
- package/dist/Chat/ChatMessage.d.ts +7 -0
- package/dist/Chat/ChatMessage.d.ts.map +1 -1
- package/dist/Chat/ChatMessageBubble.d.ts +13 -1
- package/dist/Chat/ChatMessageBubble.d.ts.map +1 -1
- package/dist/Chat/ChatMessageBubble.js +19 -1
- package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
- package/dist/CheckboxInput/CheckboxInput.js +5 -0
- package/dist/CommandPalette/CommandPaletteFooter.d.ts.map +1 -1
- package/dist/CommandPalette/CommandPaletteFooter.js +5 -3
- package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
- package/dist/DateTimeInput/DateTimeInput.js +2 -2
- package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuSubMenu.js +19 -15
- package/dist/HoverCard/HoverCard.d.ts +1 -1
- package/dist/HoverCard/HoverCard.d.ts.map +1 -1
- package/dist/HoverCard/HoverCard.js +5 -13
- package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
- package/dist/HoverCard/useHoverCard.js +6 -5
- package/dist/InputGroup/groupStyles.d.ts.map +1 -1
- package/dist/InputGroup/groupStyles.js +7 -2
- package/dist/Layer/layerHost.d.ts +24 -0
- package/dist/Layer/layerHost.d.ts.map +1 -0
- package/dist/Layer/layerHost.js +79 -0
- package/dist/Layer/useLayer.d.ts +14 -6
- package/dist/Layer/useLayer.d.ts.map +1 -1
- package/dist/Layer/useLayer.js +158 -30
- package/dist/NavItem/navItemStyles.stylex.d.ts +17 -5
- package/dist/NavItem/navItemStyles.stylex.d.ts.map +1 -1
- package/dist/NavItem/navItemStyles.stylex.js +11 -5
- package/dist/RadioList/RadioListItem.d.ts.map +1 -1
- package/dist/RadioList/RadioListItem.js +5 -0
- package/dist/SideNav/SideNav.d.ts +7 -9
- package/dist/SideNav/SideNav.d.ts.map +1 -1
- package/dist/SideNav/SideNav.js +32 -5
- package/dist/SideNav/SideNavCollapseButton.d.ts +25 -9
- package/dist/SideNav/SideNavCollapseButton.d.ts.map +1 -1
- package/dist/SideNav/SideNavCollapseButton.js +39 -15
- package/dist/SideNav/SideNavCollapseContext.d.ts +21 -0
- package/dist/SideNav/SideNavCollapseContext.d.ts.map +1 -1
- package/dist/SideNav/SideNavCollapseContext.js +16 -2
- package/dist/SideNav/SideNavHeading.d.ts.map +1 -1
- package/dist/SideNav/SideNavHeading.js +89 -32
- package/dist/SideNav/SideNavItem.d.ts +7 -2
- package/dist/SideNav/SideNavItem.d.ts.map +1 -1
- package/dist/SideNav/SideNavItem.js +119 -75
- package/dist/SideNav/SideNavSection.d.ts.map +1 -1
- package/dist/SideNav/SideNavSection.js +7 -16
- package/dist/SideNav/index.d.ts +1 -1
- package/dist/SideNav/index.d.ts.map +1 -1
- package/dist/Slider/Slider.d.ts.map +1 -1
- package/dist/Slider/Slider.js +56 -15
- package/dist/Switch/Switch.d.ts.map +1 -1
- package/dist/Switch/Switch.js +5 -0
- package/dist/Thumbnail/Thumbnail.d.ts.map +1 -1
- package/dist/Thumbnail/Thumbnail.js +5 -0
- package/dist/TopNav/TopNavHeading.d.ts.map +1 -1
- package/dist/TopNav/TopNavHeading.js +14 -6
- package/dist/TopNav/TopNavMegaMenu.d.ts.map +1 -1
- package/dist/TopNav/TopNavMegaMenu.js +25 -93
- package/dist/TopNav/TopNavMegaMenuItem.js +1 -1
- package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
- package/dist/TopNav/TopNavMenu.js +10 -5
- package/dist/astryx.css +40 -8
- package/dist/astryx.umd.js +50 -50
- package/dist/astryx.umd.js.map +4 -4
- package/dist/hooks/containerReveal.stylex.d.ts +71 -4
- package/dist/hooks/containerReveal.stylex.d.ts.map +1 -1
- package/dist/hooks/containerReveal.stylex.js +57 -5
- package/dist/hooks/index.d.ts +2 -2
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +1 -1
- package/dist/hooks/useContainerReveal.d.ts +60 -3
- package/dist/hooks/useContainerReveal.d.ts.map +1 -1
- package/dist/hooks/useContainerReveal.js +27 -5
- package/dist/hooks/useFocusTrap.d.ts.map +1 -1
- package/dist/hooks/useFocusTrap.js +16 -2
- package/dist/hooks/useMenuHover.d.ts +50 -5
- package/dist/hooks/useMenuHover.d.ts.map +1 -1
- package/dist/hooks/useMenuHover.js +171 -51
- package/dist/theme/defineTheme.d.ts +16 -5
- package/dist/theme/defineTheme.d.ts.map +1 -1
- package/dist/theme/defineTheme.js +44 -48
- package/dist/theme/derivedVarRegistry.d.ts.map +1 -1
- package/dist/theme/derivedVarRegistry.js +7 -0
- package/dist/theme/expandColorScale.d.ts.map +1 -1
- package/dist/theme/expandColorScale.js +1 -0
- package/dist/theme/expandMotionScale.d.ts +1 -0
- package/dist/theme/expandMotionScale.d.ts.map +1 -1
- package/dist/theme/expandMotionScale.js +1 -0
- package/dist/theme/expandRadiusScale.d.ts +1 -0
- package/dist/theme/expandRadiusScale.d.ts.map +1 -1
- package/dist/theme/expandRadiusScale.js +1 -0
- package/dist/theme/expandTypeScale.d.ts +1 -0
- package/dist/theme/expandTypeScale.d.ts.map +1 -1
- package/dist/theme/expandTypeScale.js +1 -0
- package/dist/theme/mergeComponents.d.ts +20 -0
- package/dist/theme/mergeComponents.d.ts.map +1 -0
- package/dist/theme/mergeComponents.js +56 -0
- package/dist/theme/onMediaTokens.d.ts +6 -1
- package/dist/theme/onMediaTokens.d.ts.map +1 -1
- package/dist/theme/onMediaTokens.js +11 -3
- package/dist/theme/tokens.stylex.d.ts +3 -1
- package/dist/theme/tokens.stylex.d.ts.map +1 -1
- package/dist/theme/tokens.stylex.js +3 -1
- package/locales/en.json +84 -0
- package/locales/pseudo.json +63 -0
- package/package.json +3 -3
- package/src/Avatar/Avatar.doc.mjs +5 -2
- package/src/Avatar/Avatar.test.tsx +51 -0
- package/src/Avatar/Avatar.tsx +30 -20
- package/src/AvatarGroup/AvatarGroup.doc.mjs +1 -1
- package/src/AvatarGroup/AvatarGroup.test.tsx +14 -0
- package/src/AvatarGroup/AvatarGroupOverflow.tsx +3 -2
- package/src/Button/Button.tsx +5 -3
- package/src/ButtonGroup/ButtonGroup.test.tsx +7 -4
- package/src/Card/Card.doc.mjs +2 -0
- package/src/Chat/Chat.doc.mjs +4 -1
- package/src/Chat/ChatMessage.doc.mjs +3 -3
- package/src/Chat/ChatMessage.tsx +7 -0
- package/src/Chat/ChatMessageBubble.doc.mjs +7 -0
- package/src/Chat/ChatMessageBubble.test.tsx +95 -6
- package/src/Chat/ChatMessageBubble.tsx +25 -0
- package/src/CheckboxInput/CheckboxInput.tsx +20 -0
- package/src/CodeBlock/CodeBlock.doc.mjs +3 -0
- package/src/CommandPalette/CommandPaletteFooter.tsx +6 -3
- package/src/DateTimeInput/DateTimeInput.tsx +4 -2
- package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +72 -1
- package/src/DropdownMenu/DropdownMenuSubMenu.tsx +24 -20
- package/src/HoverCard/HoverCard.doc.mjs +3 -3
- package/src/HoverCard/HoverCard.test.tsx +283 -55
- package/src/HoverCard/HoverCard.tsx +5 -13
- package/src/HoverCard/useHoverCard.tsx +8 -11
- package/src/InputGroup/groupStyles.ts +7 -8
- package/src/Item/Item.doc.mjs +4 -0
- package/src/Layer/layerHost.test.ts +99 -0
- package/src/Layer/layerHost.ts +141 -0
- package/src/Layer/useLayer.doc.mjs +14 -4
- package/src/Layer/useLayer.test.tsx +332 -7
- package/src/Layer/useLayer.tsx +235 -36
- package/src/MobileNav/MobileNav.doc.mjs +7 -0
- package/src/NavItem/navItemStyles.stylex.ts +31 -5
- package/src/RadioList/RadioListItem.tsx +20 -0
- package/src/SideNav/SideNav.doc.mjs +13 -4
- package/src/SideNav/SideNav.test.tsx +858 -2
- package/src/SideNav/SideNav.tsx +37 -16
- package/src/SideNav/SideNavCollapseButton.doc.mjs +28 -6
- package/src/SideNav/SideNavCollapseButton.tsx +67 -15
- package/src/SideNav/SideNavCollapseContext.ts +29 -9
- package/src/SideNav/SideNavHeading.tsx +53 -25
- package/src/SideNav/SideNavItem.doc.mjs +13 -0
- package/src/SideNav/SideNavItem.tsx +97 -84
- package/src/SideNav/SideNavSection.tsx +6 -20
- package/src/SideNav/index.ts +2 -0
- package/src/Slider/Slider.test.tsx +69 -33
- package/src/Slider/Slider.tsx +65 -13
- package/src/Switch/Switch.tsx +20 -0
- package/src/TabList/TabList.doc.mjs +3 -0
- package/src/Text/Text.doc.mjs +1 -1
- package/src/Thumbnail/Thumbnail.doc.mjs +3 -0
- package/src/Thumbnail/Thumbnail.tsx +10 -0
- package/src/Timestamp/Timestamp.test.tsx +40 -22
- package/src/Toolbar/Toolbar.test.tsx +14 -14
- package/src/TopNav/TopNav.test.tsx +86 -1
- package/src/TopNav/TopNavHeading.tsx +21 -14
- package/src/TopNav/TopNavMegaMenu.test.tsx +43 -0
- package/src/TopNav/TopNavMegaMenu.tsx +29 -105
- package/src/TopNav/TopNavMegaMenuItem.tsx +1 -1
- package/src/TopNav/TopNavMenu.test.tsx +41 -0
- package/src/TopNav/TopNavMenu.tsx +14 -4
- package/src/TreeList/TreeList.doc.mjs +1 -0
- package/src/hooks/containerReveal.stylex.ts +140 -9
- package/src/hooks/index.ts +6 -1
- package/src/hooks/useContainerReveal.doc.mjs +11 -5
- package/src/hooks/useContainerReveal.test.tsx +70 -3
- package/src/hooks/useContainerReveal.ts +86 -7
- package/src/hooks/useFocusTrap.test.tsx +16 -0
- package/src/hooks/useFocusTrap.ts +20 -2
- package/src/hooks/useMenuHover.test.tsx +364 -0
- package/src/hooks/useMenuHover.ts +243 -47
- package/src/theme/defineTheme.test.ts +127 -0
- package/src/theme/defineTheme.ts +57 -51
- package/src/theme/derivedVarRegistry.test.ts +78 -12
- package/src/theme/derivedVarRegistry.ts +4 -0
- package/src/theme/expandColorScale.ts +1 -0
- package/src/theme/expandMotionScale.ts +1 -0
- package/src/theme/expandRadiusScale.ts +1 -0
- package/src/theme/expandTypeScale.ts +1 -0
- package/src/theme/mergeComponents.ts +59 -0
- package/src/theme/onMediaTokens.ts +9 -2
- package/src/theme/themingTargets.test.ts +152 -26
- package/src/theme/tokens.stylex.ts +3 -1
- package/src/theme/tokens.test.ts +12 -0
- package/src/theme/useTheme.test.tsx +18 -0
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* SYNC: When TopNavMegaMenu changes, update tests to match new behavior
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import * as stylex from '@stylexjs/stylex';
|
|
13
|
+
import {focusOutlineStyles} from '../utils/focusOutline.stylex';
|
|
12
14
|
import {describe, it, expect, vi, beforeAll, afterAll, afterEach} from 'vitest';
|
|
13
15
|
import {render, screen, act, fireEvent} from '@testing-library/react';
|
|
14
16
|
import userEvent from '@testing-library/user-event';
|
|
@@ -817,3 +819,44 @@ describe('TopNavMegaMenuItem', () => {
|
|
|
817
819
|
expect(screen.getByRole('link', {name: /Analytics/})).toBeInTheDocument();
|
|
818
820
|
});
|
|
819
821
|
});
|
|
822
|
+
|
|
823
|
+
// =============================================================================
|
|
824
|
+
// The shared focus ring (#4654) — see the note in SideNav.test.tsx: jsdom will
|
|
825
|
+
// not derive `:focus-visible` here, so what is pinned is that the focusable
|
|
826
|
+
// element composes the shared utility's classes rather than falling back to
|
|
827
|
+
// the browser's own outline.
|
|
828
|
+
// =============================================================================
|
|
829
|
+
|
|
830
|
+
const sharedFocusRingClasses = stylex
|
|
831
|
+
.props(focusOutlineStyles.focusVisible)
|
|
832
|
+
.className!.split(' ');
|
|
833
|
+
|
|
834
|
+
function expectSharedFocusRing(el: Element) {
|
|
835
|
+
const classes = el.className.split(' ');
|
|
836
|
+
for (const c of sharedFocusRingClasses) {
|
|
837
|
+
expect(classes).toContain(c);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
describe('TopNavMegaMenu — drawer focus ring', () => {
|
|
842
|
+
it('draws the shared ring on the drawer section header', () => {
|
|
843
|
+
render(
|
|
844
|
+
<TopNavRenderContext value="drawer">
|
|
845
|
+
<TopNavMegaMenu
|
|
846
|
+
label="Products"
|
|
847
|
+
items={<TopNavMegaMenuItem title="Analytics" href="/analytics" />}
|
|
848
|
+
/>
|
|
849
|
+
</TopNavRenderContext>,
|
|
850
|
+
);
|
|
851
|
+
expectSharedFocusRing(screen.getByRole('button', {name: 'Products'}));
|
|
852
|
+
});
|
|
853
|
+
|
|
854
|
+
it('draws the shared ring on a drawer item', () => {
|
|
855
|
+
render(
|
|
856
|
+
<TopNavRenderContext value="drawer">
|
|
857
|
+
<TopNavMegaMenuItem title="Analytics" href="/analytics" />
|
|
858
|
+
</TopNavRenderContext>,
|
|
859
|
+
);
|
|
860
|
+
expectSharedFocusRing(screen.getByRole('link', {name: /Analytics/}));
|
|
861
|
+
});
|
|
862
|
+
});
|
|
@@ -50,6 +50,7 @@ import {
|
|
|
50
50
|
borderVars,
|
|
51
51
|
} from '../theme/tokens.stylex';
|
|
52
52
|
import {usePopover} from '../Popover/usePopover';
|
|
53
|
+
import {useMenuHover} from '../hooks/useMenuHover';
|
|
53
54
|
import {Grid} from '../Grid/Grid';
|
|
54
55
|
import {Icon} from '../Icon';
|
|
55
56
|
import {mergeProps, mergeRefs} from '../utils';
|
|
@@ -370,7 +371,9 @@ TopNavMegaMenu.displayName = 'TopNavMegaMenu';
|
|
|
370
371
|
// DefaultMegaMenu — desktop popover mode
|
|
371
372
|
// =============================================================================
|
|
372
373
|
|
|
373
|
-
|
|
374
|
+
/** The panel is a grid of links, not `role="menuitem"` rows. */
|
|
375
|
+
const PANEL_ITEM_SELECTOR =
|
|
376
|
+
'a[href], button:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
374
377
|
|
|
375
378
|
function DefaultMegaMenu({
|
|
376
379
|
ref,
|
|
@@ -382,19 +385,13 @@ function DefaultMegaMenu({
|
|
|
382
385
|
onOpenChange,
|
|
383
386
|
}: TopNavMegaMenuProps) {
|
|
384
387
|
const slot = useTopNavSlot();
|
|
385
|
-
const showTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
386
|
-
const hideTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
387
388
|
const triggerButtonRef = useRef<HTMLButtonElement | null>(null);
|
|
388
|
-
const hoverOpenedAtRef = useRef(0);
|
|
389
|
-
const stickyRef = useRef(false);
|
|
390
389
|
|
|
391
390
|
const handlePopoverShow = useCallback(() => {
|
|
392
391
|
onOpenChange?.(true);
|
|
393
392
|
}, [onOpenChange]);
|
|
394
393
|
|
|
395
394
|
const handlePopoverHide = useCallback(() => {
|
|
396
|
-
hoverOpenedAtRef.current = 0;
|
|
397
|
-
stickyRef.current = false;
|
|
398
395
|
onOpenChange?.(false);
|
|
399
396
|
}, [onOpenChange]);
|
|
400
397
|
|
|
@@ -430,107 +427,31 @@ function DefaultMegaMenu({
|
|
|
430
427
|
};
|
|
431
428
|
}, [popover]);
|
|
432
429
|
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
}, [clearTimeouts, delay, popover]);
|
|
451
|
-
|
|
452
|
-
const scheduleHide = useCallback(() => {
|
|
453
|
-
clearTimeouts();
|
|
454
|
-
hideTimeoutRef.current = setTimeout(() => {
|
|
455
|
-
popover.hide();
|
|
456
|
-
}, hideDelay);
|
|
457
|
-
}, [clearTimeouts, hideDelay, popover]);
|
|
458
|
-
|
|
459
|
-
const focusFirstPanelItem = useCallback(() => {
|
|
460
|
-
popover.contentRef.current
|
|
461
|
-
?.querySelector<HTMLElement>(
|
|
462
|
-
'a[href], button:not([disabled]), [tabindex]:not([tabindex="-1"])',
|
|
463
|
-
)
|
|
464
|
-
?.focus();
|
|
465
|
-
}, [popover.contentRef]);
|
|
466
|
-
|
|
467
|
-
const handleTriggerMouseEnter = useCallback(() => {
|
|
468
|
-
clearTimeouts();
|
|
469
|
-
if (!popover.isOpen) {
|
|
470
|
-
scheduleShow();
|
|
471
|
-
}
|
|
472
|
-
}, [clearTimeouts, popover.isOpen, scheduleShow]);
|
|
473
|
-
|
|
474
|
-
const handlePanelMouseEnter = useCallback(() => {
|
|
475
|
-
clearTimeouts();
|
|
476
|
-
}, [clearTimeouts]);
|
|
477
|
-
|
|
478
|
-
const handleMouseLeave = useCallback(() => {
|
|
479
|
-
if (!stickyRef.current) {
|
|
480
|
-
scheduleHide();
|
|
481
|
-
}
|
|
482
|
-
}, [scheduleHide]);
|
|
483
|
-
|
|
484
|
-
const handleClick = useCallback(
|
|
485
|
-
(event: React.MouseEvent<HTMLButtonElement>) => {
|
|
486
|
-
// Cancel the native invoker toggle so this guard is the single source of
|
|
487
|
-
// truth for trigger activation. popoverTarget still establishes the
|
|
488
|
-
// invoker relationship used by native light-dismiss and stacking.
|
|
489
|
-
event.preventDefault();
|
|
490
|
-
clearTimeouts();
|
|
491
|
-
|
|
492
|
-
if (event.detail === 0) {
|
|
493
|
-
stickyRef.current = true;
|
|
494
|
-
hoverOpenedAtRef.current = 0;
|
|
495
|
-
if (popover.isOpen) {
|
|
496
|
-
focusFirstPanelItem();
|
|
497
|
-
} else {
|
|
498
|
-
popover.show();
|
|
499
|
-
}
|
|
500
|
-
} else if (!popover.isOpen) {
|
|
501
|
-
stickyRef.current = true;
|
|
502
|
-
popover.show({skipAutoFocus: true});
|
|
503
|
-
} else if (Date.now() - hoverOpenedAtRef.current < CLICK_GUARD_MS) {
|
|
504
|
-
// A click that naturally follows a hover-open confirms the open state
|
|
505
|
-
// instead of toggling the panel shut. From here it behaves like any
|
|
506
|
-
// other click-open and stays pinned until explicit dismissal.
|
|
507
|
-
stickyRef.current = true;
|
|
508
|
-
hoverOpenedAtRef.current = 0;
|
|
509
|
-
} else {
|
|
510
|
-
popover.hide();
|
|
511
|
-
triggerButtonRef.current?.focus();
|
|
512
|
-
}
|
|
513
|
-
},
|
|
514
|
-
[clearTimeouts, focusFirstPanelItem, popover],
|
|
515
|
-
);
|
|
516
|
-
|
|
517
|
-
useEffect(() => {
|
|
518
|
-
return () => {
|
|
519
|
-
clearTimeouts();
|
|
520
|
-
};
|
|
521
|
-
}, [clearTimeouts]);
|
|
430
|
+
const {
|
|
431
|
+
triggerProps: hoverTriggerProps,
|
|
432
|
+
contentProps,
|
|
433
|
+
menuRef,
|
|
434
|
+
setTriggerEl,
|
|
435
|
+
} = useMenuHover<HTMLDivElement>({
|
|
436
|
+
show: popover.show,
|
|
437
|
+
hide: popover.hide,
|
|
438
|
+
isOpen: popover.isOpen,
|
|
439
|
+
isEnabled: true,
|
|
440
|
+
showDelay: delay,
|
|
441
|
+
hideDelay,
|
|
442
|
+
itemSelector: PANEL_ITEM_SELECTOR,
|
|
443
|
+
// Trigger sits outside an auto popover; the invoker relationship exempts it
|
|
444
|
+
// from light dismiss.
|
|
445
|
+
popoverId: popover.id,
|
|
446
|
+
});
|
|
522
447
|
|
|
523
448
|
return (
|
|
524
449
|
<>
|
|
525
450
|
<button
|
|
526
|
-
ref={mergeRefs(triggerButtonRef, ref)}
|
|
451
|
+
ref={mergeRefs(triggerButtonRef, setTriggerEl, ref)}
|
|
527
452
|
type="button"
|
|
528
453
|
{...popover.triggerProps}
|
|
529
|
-
|
|
530
|
-
popoverTarget={popover.id}
|
|
531
|
-
onClick={handleClick}
|
|
532
|
-
onMouseEnter={handleTriggerMouseEnter}
|
|
533
|
-
onMouseLeave={handleMouseLeave}
|
|
454
|
+
{...hoverTriggerProps}
|
|
534
455
|
{...mergeProps(
|
|
535
456
|
themeProps('top-nav-mega-menu'),
|
|
536
457
|
focusOutlineProps.focusVisible(
|
|
@@ -551,10 +472,10 @@ function DefaultMegaMenu({
|
|
|
551
472
|
// role="group" — a mega menu is a browsing grid of links, not an
|
|
552
473
|
// ARIA menu of menuitems (per the WAI-ARIA APG, the menu role is
|
|
553
474
|
// for action menus; link mega menus are the documented anti-case).
|
|
475
|
+
ref={menuRef}
|
|
554
476
|
role="group"
|
|
555
477
|
aria-label={label}
|
|
556
|
-
|
|
557
|
-
onMouseLeave={handleMouseLeave}
|
|
478
|
+
{...contentProps}
|
|
558
479
|
{...stylex.props(styles.panelContainer)}>
|
|
559
480
|
<div {...stylex.props(styles.panelContent)}>
|
|
560
481
|
{/* Menu items section */}
|
|
@@ -602,7 +523,10 @@ function DrawerMegaMenu({
|
|
|
602
523
|
aria-controls={`${menuId}-items`}
|
|
603
524
|
{...mergeProps(
|
|
604
525
|
themeProps('top-nav-mega-menu', {mode: 'drawer'}),
|
|
605
|
-
|
|
526
|
+
focusOutlineProps.focusVisible(
|
|
527
|
+
navItemStyles.item,
|
|
528
|
+
styles.drawerHeader,
|
|
529
|
+
),
|
|
606
530
|
)}>
|
|
607
531
|
{label}
|
|
608
532
|
<Icon
|
|
@@ -215,7 +215,7 @@ export function TopNavMegaMenuItem({
|
|
|
215
215
|
{...elementProps}
|
|
216
216
|
{...mergeProps(
|
|
217
217
|
themeProps('top-nav-mega-menu-item', {mode: 'drawer'}),
|
|
218
|
-
|
|
218
|
+
focusOutlineProps.focusVisible(navItemStyles.item, styles.drawerItem),
|
|
219
219
|
)}>
|
|
220
220
|
{icon && <div {...stylex.props(styles.drawerItemIcon)}>{icon}</div>}
|
|
221
221
|
<div {...stylex.props(styles.drawerItemContent)}>
|
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
import {describe, it, expect, vi} from 'vitest';
|
|
13
13
|
import {render, screen, fireEvent} from '@testing-library/react';
|
|
14
14
|
import userEvent from '@testing-library/user-event';
|
|
15
|
+
import * as stylex from '@stylexjs/stylex';
|
|
16
|
+
import {focusOutlineStyles} from '../utils/focusOutline.stylex';
|
|
17
|
+
import {TopNavRenderContext} from './TopNavRenderContext';
|
|
15
18
|
import {TopNavMenu} from './TopNavMenu';
|
|
16
19
|
|
|
17
20
|
const mockItems = [
|
|
@@ -171,3 +174,41 @@ describe('keyboard navigation (APG menu pattern)', () => {
|
|
|
171
174
|
expect(trigger).toHaveAttribute('aria-expanded', 'false');
|
|
172
175
|
});
|
|
173
176
|
});
|
|
177
|
+
|
|
178
|
+
// =============================================================================
|
|
179
|
+
// The shared focus ring (#4654) — see the note in SideNav.test.tsx: jsdom will
|
|
180
|
+
// not derive `:focus-visible` here, so what is pinned is that the focusable
|
|
181
|
+
// element composes the shared utility's classes rather than falling back to
|
|
182
|
+
// the browser's own outline.
|
|
183
|
+
// =============================================================================
|
|
184
|
+
|
|
185
|
+
const sharedFocusRingClasses = stylex
|
|
186
|
+
.props(focusOutlineStyles.focusVisible)
|
|
187
|
+
.className!.split(' ');
|
|
188
|
+
|
|
189
|
+
function expectSharedFocusRing(el: Element) {
|
|
190
|
+
const classes = el.className.split(' ');
|
|
191
|
+
for (const c of sharedFocusRingClasses) {
|
|
192
|
+
expect(classes).toContain(c);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
describe('TopNavMenu — drawer focus ring', () => {
|
|
197
|
+
it('draws the shared ring on the drawer section header', () => {
|
|
198
|
+
render(
|
|
199
|
+
<TopNavRenderContext value="drawer">
|
|
200
|
+
<TopNavMenu label="Products" items={mockItems} />
|
|
201
|
+
</TopNavRenderContext>,
|
|
202
|
+
);
|
|
203
|
+
expectSharedFocusRing(screen.getByRole('button', {name: /Products/}));
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('draws the shared ring on a drawer item', () => {
|
|
207
|
+
render(
|
|
208
|
+
<TopNavRenderContext value="drawer">
|
|
209
|
+
<TopNavMenu label="Products" items={mockItems} />
|
|
210
|
+
</TopNavRenderContext>,
|
|
211
|
+
);
|
|
212
|
+
expectSharedFocusRing(screen.getByRole('link', {name: /Analytics/}));
|
|
213
|
+
});
|
|
214
|
+
});
|
|
@@ -342,7 +342,7 @@ export function TopNavMenu({
|
|
|
342
342
|
role: 'none',
|
|
343
343
|
});
|
|
344
344
|
|
|
345
|
-
const {triggerProps, contentProps, menuRef, setTriggerEl} =
|
|
345
|
+
const {triggerProps, contentProps, menuRef, setTriggerEl, close} =
|
|
346
346
|
useMenuHover<HTMLDivElement>({
|
|
347
347
|
show: popover.show,
|
|
348
348
|
hide: popover.hide,
|
|
@@ -350,6 +350,9 @@ export function TopNavMenu({
|
|
|
350
350
|
isEnabled: true,
|
|
351
351
|
showDelay: delay,
|
|
352
352
|
hideDelay,
|
|
353
|
+
// Trigger sits outside an auto popover; the invoker relationship exempts
|
|
354
|
+
// it from light dismiss.
|
|
355
|
+
popoverId: popover.id,
|
|
353
356
|
});
|
|
354
357
|
|
|
355
358
|
const setTriggerRef = mergeRefs<HTMLButtonElement>(
|
|
@@ -368,7 +371,8 @@ export function TopNavMenu({
|
|
|
368
371
|
useListFocus<HTMLDivElement>({
|
|
369
372
|
itemSelector: '[role="menuitem"]',
|
|
370
373
|
hasRovingTabIndex: true,
|
|
371
|
-
|
|
374
|
+
// Not popover.hide: Escape must also restore focus to the trigger.
|
|
375
|
+
onEscape: close,
|
|
372
376
|
});
|
|
373
377
|
|
|
374
378
|
// First-character typeahead over the menu items (menus-11).
|
|
@@ -432,7 +436,10 @@ export function TopNavMenu({
|
|
|
432
436
|
onClick={() => setDrawerExpanded(v => !v)}
|
|
433
437
|
aria-expanded={drawerExpanded}
|
|
434
438
|
aria-controls={`${menuId}-items`}
|
|
435
|
-
{...
|
|
439
|
+
{...focusOutlineProps.focusVisible(
|
|
440
|
+
navItemStyles.item,
|
|
441
|
+
drawerStyles.header,
|
|
442
|
+
)}>
|
|
436
443
|
{label}
|
|
437
444
|
<Icon
|
|
438
445
|
icon="chevronDown"
|
|
@@ -459,7 +466,10 @@ export function TopNavMenu({
|
|
|
459
466
|
item.onClick?.();
|
|
460
467
|
closeMobileNav();
|
|
461
468
|
}}
|
|
462
|
-
{...
|
|
469
|
+
{...focusOutlineProps.focusVisible(
|
|
470
|
+
navItemStyles.item,
|
|
471
|
+
drawerStyles.item,
|
|
472
|
+
)}>
|
|
463
473
|
{item.icon && (
|
|
464
474
|
<span {...stylex.props(drawerStyles.itemIcon)}>
|
|
465
475
|
{item.icon}
|
|
@@ -33,6 +33,7 @@ export const docs = {
|
|
|
33
33
|
vars: [
|
|
34
34
|
{name: '--tree-list-indent', description: 'Per-level indentation step. Each nesting level indents its rows by this distance, and the guide lines follow it so they stay aligned. Set it on the `tree-list` target to retune the metric (e.g. `var(--spacing-5)` for a wider indent).', default: 'var(--spacing-4)'},
|
|
35
35
|
{name: '--tree-list-row-gap', description: 'Vertical gap between adjacent rows. Default `2px` (var(--spacing-0-5)) gives a subtle separation; set it on the `tree-list` target to widen or close the gap. The connector guides span the gap automatically (the line stays continuous) and do not overhang the last row, so no guide-height tuning is needed.', default: 'var(--spacing-0-5)'},
|
|
36
|
+
{name: '--_tree-indent', description: 'Distance one row is indented, computed per row from --tree-list-indent and the row depth. Set --tree-list-indent to retune indentation; this is the resolved value.', default: '0px', private: true},
|
|
36
37
|
],
|
|
37
38
|
},
|
|
38
39
|
components: [
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* @file containerReveal.stylex.ts
|
|
5
5
|
* @input Uses StyleX, theme tokens
|
|
6
|
-
* @output The container style that publishes the reveal state,
|
|
7
|
-
*
|
|
8
|
-
* variant) that read it.
|
|
6
|
+
* @output The container style that publishes the reveal state, the suspended
|
|
7
|
+
* and hover-delay container variants, and the four content style blocks
|
|
8
|
+
* (reveal / conceal, each with a layout-preserved variant) that read it.
|
|
9
9
|
* @position Internal to useContainerReveal.
|
|
10
10
|
*
|
|
11
11
|
* HOW THE SCOPING WORKS: the container declares its own reveal state as
|
|
@@ -22,8 +22,14 @@ import {durationVars, easeVars} from '../theme/tokens.stylex';
|
|
|
22
22
|
|
|
23
23
|
const REST_DELAY = '0s, ' + durationVars['--duration-fast'];
|
|
24
24
|
|
|
25
|
+
// The hover-intent gate: how long the pointer must dwell before the hover
|
|
26
|
+
// branch takes effect. Declared on every container so a nested one never
|
|
27
|
+
// inherits its ancestor's dwell.
|
|
28
|
+
const HOVER_DELAY = 'var(--_hover-delay, 0s)';
|
|
29
|
+
|
|
25
30
|
export const styles = stylex.create({
|
|
26
31
|
container: {
|
|
32
|
+
'--_hover-delay': '0s',
|
|
27
33
|
'--_reveal-opacity': {
|
|
28
34
|
default: 0,
|
|
29
35
|
':hover': {'@media (hover: hover)': 1},
|
|
@@ -37,13 +43,21 @@ export const styles = stylex.create({
|
|
|
37
43
|
'@media (any-pointer: coarse)': 'static',
|
|
38
44
|
},
|
|
39
45
|
// The position flip is discrete, so it transitions with allow-discrete and
|
|
40
|
-
// a state-conditional delay:
|
|
41
|
-
// fades in) and the fade duration on exit (stays in flow
|
|
42
|
-
// finishes, then snaps out) — without
|
|
43
|
-
// at full opacity and flicker.
|
|
46
|
+
// a state-conditional delay: the dwell on entry (flips into flow when the
|
|
47
|
+
// gate opens, then fades in) and the fade duration on exit (stays in flow
|
|
48
|
+
// until the fade finishes, then snaps out) — without the exit half the
|
|
49
|
+
// content would snap out of flow at full opacity and flicker.
|
|
44
50
|
'--_reveal-delay': {
|
|
45
51
|
default: REST_DELAY,
|
|
46
|
-
':hover': {'@media (hover: hover)': '
|
|
52
|
+
':hover': {'@media (hover: hover)': HOVER_DELAY + ', ' + HOVER_DELAY},
|
|
53
|
+
':focus-within': '0s, 0s',
|
|
54
|
+
'@media (any-pointer: coarse)': '0s, 0s',
|
|
55
|
+
},
|
|
56
|
+
// Reduced motion drops the exit sequencing (there is no fade left to wait
|
|
57
|
+
// for) but keeps the dwell: an intent gate is timing, not motion.
|
|
58
|
+
'--_reveal-delay-reduced': {
|
|
59
|
+
default: '0s, 0s',
|
|
60
|
+
':hover': {'@media (hover: hover)': HOVER_DELAY + ', ' + HOVER_DELAY},
|
|
47
61
|
':focus-within': '0s, 0s',
|
|
48
62
|
'@media (any-pointer: coarse)': '0s, 0s',
|
|
49
63
|
},
|
|
@@ -54,7 +68,98 @@ export const styles = stylex.create({
|
|
|
54
68
|
default: 1,
|
|
55
69
|
':hover': {'@media (hover: hover)': 0},
|
|
56
70
|
},
|
|
71
|
+
// Single-value dwell for the opacity-only blocks, which have no discrete
|
|
72
|
+
// position to sequence.
|
|
73
|
+
'--_fade-delay': {
|
|
74
|
+
default: '0s',
|
|
75
|
+
':hover': {'@media (hover: hover)': HOVER_DELAY},
|
|
76
|
+
':focus-within': '0s',
|
|
77
|
+
'@media (any-pointer: coarse)': '0s',
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
// stateInactive is `container` with the hover branch pinned to its rest
|
|
81
|
+
// value, so the pointer stops driving the reveal while a caller holds the
|
|
82
|
+
// container inactive. Keyboard focus and coarse pointers keep their branches:
|
|
83
|
+
// an inactive container must never hide content from a keyboard or touch
|
|
84
|
+
// user. Every property repeats the full condition shape of `container` —
|
|
85
|
+
// StyleX replaces styles per property AND condition, so a plain `default`
|
|
86
|
+
// here would lose to the earlier block's `:hover` rule.
|
|
87
|
+
stateInactive: {
|
|
88
|
+
'--_reveal-opacity': {
|
|
89
|
+
default: 0,
|
|
90
|
+
':hover': {'@media (hover: hover)': 0},
|
|
91
|
+
':focus-within': 1,
|
|
92
|
+
'@media (any-pointer: coarse)': 1,
|
|
93
|
+
},
|
|
94
|
+
'--_reveal-position': {
|
|
95
|
+
default: 'absolute',
|
|
96
|
+
':hover': {'@media (hover: hover)': 'absolute'},
|
|
97
|
+
':focus-within': 'static',
|
|
98
|
+
'@media (any-pointer: coarse)': 'static',
|
|
99
|
+
},
|
|
100
|
+
'--_reveal-delay': {
|
|
101
|
+
default: REST_DELAY,
|
|
102
|
+
':hover': {'@media (hover: hover)': REST_DELAY},
|
|
103
|
+
':focus-within': '0s, 0s',
|
|
104
|
+
'@media (any-pointer: coarse)': '0s, 0s',
|
|
105
|
+
},
|
|
106
|
+
'--_reveal-delay-reduced': {
|
|
107
|
+
default: '0s, 0s',
|
|
108
|
+
':hover': {'@media (hover: hover)': '0s, 0s'},
|
|
109
|
+
':focus-within': '0s, 0s',
|
|
110
|
+
'@media (any-pointer: coarse)': '0s, 0s',
|
|
111
|
+
},
|
|
112
|
+
'--_conceal-opacity': {
|
|
113
|
+
default: 1,
|
|
114
|
+
':hover': {'@media (hover: hover)': 1},
|
|
115
|
+
},
|
|
116
|
+
'--_fade-delay': {
|
|
117
|
+
default: '0s',
|
|
118
|
+
':hover': {'@media (hover: hover)': '0s'},
|
|
119
|
+
':focus-within': '0s',
|
|
120
|
+
'@media (any-pointer: coarse)': '0s',
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
// stateActive pins the other end: every branch reads as pointed-at, so
|
|
124
|
+
// revealed content stays in and inverted content stays out, with no dwell to
|
|
125
|
+
// wait through.
|
|
126
|
+
stateActive: {
|
|
127
|
+
'--_reveal-opacity': {
|
|
128
|
+
default: 1,
|
|
129
|
+
':hover': {'@media (hover: hover)': 1},
|
|
130
|
+
':focus-within': 1,
|
|
131
|
+
'@media (any-pointer: coarse)': 1,
|
|
132
|
+
},
|
|
133
|
+
'--_reveal-position': {
|
|
134
|
+
default: 'static',
|
|
135
|
+
':hover': {'@media (hover: hover)': 'static'},
|
|
136
|
+
':focus-within': 'static',
|
|
137
|
+
'@media (any-pointer: coarse)': 'static',
|
|
138
|
+
},
|
|
139
|
+
'--_reveal-delay': {
|
|
140
|
+
default: '0s, 0s',
|
|
141
|
+
':hover': {'@media (hover: hover)': '0s, 0s'},
|
|
142
|
+
':focus-within': '0s, 0s',
|
|
143
|
+
'@media (any-pointer: coarse)': '0s, 0s',
|
|
144
|
+
},
|
|
145
|
+
'--_reveal-delay-reduced': {
|
|
146
|
+
default: '0s, 0s',
|
|
147
|
+
':hover': {'@media (hover: hover)': '0s, 0s'},
|
|
148
|
+
':focus-within': '0s, 0s',
|
|
149
|
+
'@media (any-pointer: coarse)': '0s, 0s',
|
|
150
|
+
},
|
|
151
|
+
'--_conceal-opacity': {
|
|
152
|
+
default: 0,
|
|
153
|
+
':hover': {'@media (hover: hover)': 0},
|
|
154
|
+
},
|
|
155
|
+
'--_fade-delay': {
|
|
156
|
+
default: '0s',
|
|
157
|
+
':hover': {'@media (hover: hover)': '0s'},
|
|
158
|
+
':focus-within': '0s',
|
|
159
|
+
'@media (any-pointer: coarse)': '0s',
|
|
160
|
+
},
|
|
57
161
|
},
|
|
162
|
+
hoverDelay: (delay: string) => ({'--_hover-delay': delay}),
|
|
58
163
|
// The fallbacks make content spread outside a reveal container fail visible
|
|
59
164
|
// rather than invisible.
|
|
60
165
|
reveal: {
|
|
@@ -67,7 +172,8 @@ export const styles = stylex.create({
|
|
|
67
172
|
transitionBehavior: 'allow-discrete',
|
|
68
173
|
transitionDelay: {
|
|
69
174
|
default: 'var(--_reveal-delay, 0s, 0s)',
|
|
70
|
-
'@media (prefers-reduced-motion: reduce)':
|
|
175
|
+
'@media (prefers-reduced-motion: reduce)':
|
|
176
|
+
'var(--_reveal-delay-reduced, 0s, 0s)',
|
|
71
177
|
},
|
|
72
178
|
opacity: 'var(--_reveal-opacity, 1)',
|
|
73
179
|
position: 'var(--_reveal-position, static)',
|
|
@@ -79,6 +185,7 @@ export const styles = stylex.create({
|
|
|
79
185
|
'@media (prefers-reduced-motion: reduce)': '0s',
|
|
80
186
|
},
|
|
81
187
|
transitionTimingFunction: easeVars['--ease-standard'],
|
|
188
|
+
transitionDelay: 'var(--_fade-delay, 0s)',
|
|
82
189
|
opacity: 'var(--_reveal-opacity, 1)',
|
|
83
190
|
},
|
|
84
191
|
conceal: {
|
|
@@ -88,6 +195,7 @@ export const styles = stylex.create({
|
|
|
88
195
|
'@media (prefers-reduced-motion: reduce)': '0s',
|
|
89
196
|
},
|
|
90
197
|
transitionTimingFunction: easeVars['--ease-standard'],
|
|
198
|
+
transitionDelay: 'var(--_fade-delay, 0s)',
|
|
91
199
|
opacity: 'var(--_conceal-opacity, 1)',
|
|
92
200
|
},
|
|
93
201
|
concealLayoutPreserved: {
|
|
@@ -97,6 +205,29 @@ export const styles = stylex.create({
|
|
|
97
205
|
'@media (prefers-reduced-motion: reduce)': '0s',
|
|
98
206
|
},
|
|
99
207
|
transitionTimingFunction: easeVars['--ease-standard'],
|
|
208
|
+
transitionDelay: 'var(--_fade-delay, 0s)',
|
|
100
209
|
opacity: 'var(--_conceal-opacity, 1)',
|
|
101
210
|
},
|
|
211
|
+
// Per-element overrides. These read no container state at all — they are the
|
|
212
|
+
// caller saying what THIS element looks like, whatever the container is
|
|
213
|
+
// doing, so they are plain values rather than custom properties.
|
|
214
|
+
contentShown: {
|
|
215
|
+
opacity: 1,
|
|
216
|
+
position: 'static',
|
|
217
|
+
transitionDelay: '0s',
|
|
218
|
+
},
|
|
219
|
+
// Hidden yields to focus: a forced-hidden element is still mounted and still
|
|
220
|
+
// tabbable, so it has to reappear when focus lands inside it — otherwise a
|
|
221
|
+
// keyboard user tabs into something they cannot see.
|
|
222
|
+
contentHidden: {
|
|
223
|
+
opacity: {default: 0, ':focus-within': 1},
|
|
224
|
+
position: {default: 'absolute', ':focus-within': 'static'},
|
|
225
|
+
transitionDelay: '0s',
|
|
226
|
+
},
|
|
227
|
+
// Layout-preserved content has no discrete position to flip, so its hidden
|
|
228
|
+
// variant is opacity alone.
|
|
229
|
+
contentHiddenLayoutPreserved: {
|
|
230
|
+
opacity: {default: 0, ':focus-within': 1},
|
|
231
|
+
transitionDelay: '0s',
|
|
232
|
+
},
|
|
102
233
|
});
|
package/src/hooks/index.ts
CHANGED
|
@@ -11,7 +11,11 @@
|
|
|
11
11
|
* SYNC: When modified, update this header
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
export {
|
|
14
|
+
export {
|
|
15
|
+
hasActiveFocusTrapEscape,
|
|
16
|
+
isImeKeyEvent,
|
|
17
|
+
useFocusTrap,
|
|
18
|
+
} from './useFocusTrap';
|
|
15
19
|
export type {UseFocusTrapOptions, UseFocusTrapReturn} from './useFocusTrap';
|
|
16
20
|
|
|
17
21
|
export {useAnnounce} from './useAnnounce';
|
|
@@ -101,5 +105,6 @@ export {useContainerReveal} from './useContainerReveal';
|
|
|
101
105
|
export type {
|
|
102
106
|
UseContainerRevealOptions,
|
|
103
107
|
UseContainerRevealReturn,
|
|
108
|
+
ContainerRevealOptions,
|
|
104
109
|
ContentRevealOptions,
|
|
105
110
|
} from './useContainerReveal';
|
|
@@ -23,13 +23,13 @@ export const docs = {
|
|
|
23
23
|
returns: [
|
|
24
24
|
{
|
|
25
25
|
name: 'getContainerProps',
|
|
26
|
-
type: '() => {className?: string; style?: CSSProperties}',
|
|
27
|
-
description: 'Spread onto the container whose hover/focus-within drives the reveal.',
|
|
26
|
+
type: '(options?: ContainerRevealOptions) => {className?: string; style?: CSSProperties}',
|
|
27
|
+
description: 'Spread onto the container whose hover/focus-within drives the reveal. Accepts hoverDelay (ms the pointer must dwell before the reveal starts — a hover-intent gate like Tooltip\'s and HoverCard\'s delay, so a cursor sweeping across a list leaves nothing painted behind it) and forceState ("active" | "inactive") to pin the trigger state when a caller owns it — a motion gate, a scroll, a row whose menu is open. "inactive" still yields to keyboard focus and coarse pointers.',
|
|
28
28
|
},
|
|
29
29
|
{
|
|
30
30
|
name: 'getContentRevealProps',
|
|
31
31
|
type: '(options?: ContentRevealOptions) => {className?: string; style?: CSSProperties}',
|
|
32
|
-
description: 'Spread onto each revealed
|
|
32
|
+
description: 'Spread onto each revealed / concealed child. Accepts isRevealInverted to conceal-on-hover instead of reveal-on-hover, isLayoutPreserved to reserve the layout box while hidden (opacity-only) and avoid layout shift, and forceVisibility ("shown" | "hidden") to pin this one element\'s appearance whatever the container is doing. "hidden" yields to focus.',
|
|
33
33
|
},
|
|
34
34
|
],
|
|
35
35
|
usage: {
|
|
@@ -40,6 +40,9 @@ export const docs = {
|
|
|
40
40
|
{ guidance: true, description: 'Use for secondary affordances: reveal-on-hover row actions (edit/copy/remove on list or table rows) and overlay controls on a card or media tile (e.g. Thumbnail\'s remove button).' },
|
|
41
41
|
{ guidance: true, description: 'Gate the reveal with isEnabled when a consumer prop decides whether content is revealed on hover or always shown; it can change at any time.' },
|
|
42
42
|
{ guidance: true, description: 'Pass isLayoutPreserved for absolutely-positioned or overlay content to reserve its box and avoid layout shift when it appears.' },
|
|
43
|
+
{ guidance: true, description: 'Set a hoverDelay (100-250ms) on rows in a long list, so a cursor travelling across the list does not light up every row it passes; keyboard and touch still reveal immediately.' },
|
|
44
|
+
{ guidance: true, description: 'Reach for forceState when something other than the pointer owns the interaction (a drag, a scroll or motion gate, an open row menu), and forceVisibility when just one element should ignore the container.' },
|
|
45
|
+
{ guidance: false, description: 'Reach past the API into the hook\'s private custom properties (--_reveal-opacity and friends) to suppress a reveal; use forceState / forceVisibility, which survive a rename.' },
|
|
43
46
|
{ guidance: false, description: 'Use it to hide content that must always be discoverable; keep essential actions visible instead of gating them behind hover.' },
|
|
44
47
|
],
|
|
45
48
|
},
|
|
@@ -58,8 +61,8 @@ export const docsDense = {
|
|
|
58
61
|
'options.isEnabled': 'when false hook is inert: no container styles, content getters return no styles, content always shown. Read every render, so it can flip after mount.',
|
|
59
62
|
},
|
|
60
63
|
returnDescriptions: {
|
|
61
|
-
getContainerProps: 'spread onto container whose hover/focus-within drives reveal.',
|
|
62
|
-
getContentRevealProps: 'spread onto each revealed / concealed child. Accepts isRevealInverted (conceal-on-hover)
|
|
64
|
+
getContainerProps: 'spread onto container whose hover/focus-within drives reveal. Accepts hoverDelay (ms dwell before reveal starts — hover-intent gate like Tooltip / HoverCard delay) + forceState ("active" | "inactive") to pin trigger state when a caller owns it. "inactive" yields to keyboard focus + coarse pointers.',
|
|
65
|
+
getContentRevealProps: 'spread onto each revealed / concealed child. Accepts isRevealInverted (conceal-on-hover), isLayoutPreserved (reserve layout box while hidden) + forceVisibility ("shown" | "hidden") to pin this element regardless of container. "hidden" yields to focus.',
|
|
63
66
|
},
|
|
64
67
|
usage: {
|
|
65
68
|
description:
|
|
@@ -69,6 +72,9 @@ export const docsDense = {
|
|
|
69
72
|
{ guidance: true, description: 'Use for secondary affordances: reveal-on-hover row actions (edit/copy/remove on list / table rows) + overlay controls on card / media tile (e.g. Thumbnail remove button).' },
|
|
70
73
|
{ guidance: true, description: 'Gate reveal w/ isEnabled when a consumer prop decides revealed-on-hover vs always shown; can change at any time.' },
|
|
71
74
|
{ guidance: true, description: 'Pass isLayoutPreserved for absolutely-positioned / overlay content to reserve its box + avoid layout shift.' },
|
|
75
|
+
{ guidance: true, description: 'Set hoverDelay (100-250ms) on rows in a long list so a travelling cursor does not light up every row; keyboard + touch still reveal immediately.' },
|
|
76
|
+
{ guidance: true, description: 'forceState when something else owns the interaction (drag, scroll / motion gate, open row menu); forceVisibility when one element should ignore the container.' },
|
|
77
|
+
{ guidance: false, description: 'Reach past the API into private custom properties (--_reveal-opacity etc) to suppress a reveal; use forceState / forceVisibility.' },
|
|
72
78
|
{ guidance: false, description: 'Use to hide content that must always be discoverable; keep essential actions visible instead of gating behind hover.' },
|
|
73
79
|
],
|
|
74
80
|
},
|