@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
|
@@ -4,12 +4,17 @@
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @file SideNavItem.tsx
|
|
7
|
-
* @input Uses React, ReactNode, StyleX, Icon, IconType
|
|
7
|
+
* @input Uses React, ReactNode, StyleX, Icon, IconType, useMenuHover
|
|
8
8
|
* @output Exports SideNavItem component and SideNavItemProps
|
|
9
9
|
* @position Core implementation; used inside SideNav children
|
|
10
10
|
*
|
|
11
11
|
* Navigation item with icon, selected state, and nesting.
|
|
12
12
|
*
|
|
13
|
+
* Collapsed items with children open their submenu flyout through
|
|
14
|
+
* `useMenuHover`, the shared hover-intent hook (same one `SideNavHeading` and
|
|
15
|
+
* `TopNavMenu` use). Hover is a progressive enhancement over the popover's
|
|
16
|
+
* click behavior and is inert on coarse pointers.
|
|
17
|
+
*
|
|
13
18
|
* SYNC: When modified, update these files to stay in sync:
|
|
14
19
|
* - /packages/core/src/SideNav/SideNav.doc.mjs
|
|
15
20
|
* - /packages/core/src/SideNav/SideNav.test.tsx
|
|
@@ -35,17 +40,18 @@ import {
|
|
|
35
40
|
typeScaleVars,
|
|
36
41
|
durationVars,
|
|
37
42
|
easeVars,
|
|
38
|
-
borderVars,
|
|
39
43
|
radiusVars,
|
|
40
44
|
} from '../theme/tokens.stylex';
|
|
41
45
|
import {Icon, renderIconSlot, type IconType} from '../Icon';
|
|
42
46
|
import {useLinkComponent} from '../Link/useLinkComponent';
|
|
43
47
|
import type {LinkComponentType} from '../Link/types';
|
|
44
48
|
import {usePopover} from '../Popover/usePopover';
|
|
49
|
+
import {useMenuHover} from '../hooks/useMenuHover';
|
|
45
50
|
import {mergeProps, mergeRefs} from '../utils';
|
|
46
51
|
import type {BaseProps} from '../BaseProps';
|
|
47
52
|
import {Tooltip} from '../Tooltip';
|
|
48
53
|
import {navItemStyles, type NavItemSize} from '../NavItem/navItemStyles.stylex';
|
|
54
|
+
import {focusOutlineProps} from '../utils/focusOutline.stylex';
|
|
49
55
|
import {
|
|
50
56
|
useSideNavCollapse,
|
|
51
57
|
SideNavCollapseContext,
|
|
@@ -91,7 +97,10 @@ const styles = stylex.create({
|
|
|
91
97
|
display: 'grid',
|
|
92
98
|
gridTemplateRows: '1fr',
|
|
93
99
|
transitionProperty: 'grid-template-rows',
|
|
94
|
-
transitionDuration:
|
|
100
|
+
transitionDuration: {
|
|
101
|
+
default: durationVars['--duration-medium'],
|
|
102
|
+
'@media (prefers-reduced-motion: reduce)': '0s',
|
|
103
|
+
},
|
|
95
104
|
transitionTimingFunction: easeVars['--ease-standard'],
|
|
96
105
|
},
|
|
97
106
|
childrenCollapsed: {
|
|
@@ -114,7 +123,10 @@ const styles = stylex.create({
|
|
|
114
123
|
// target, not the glyph size, so keep the glyph on the inherited size.
|
|
115
124
|
fontSize: 'inherit',
|
|
116
125
|
transitionProperty: 'transform',
|
|
117
|
-
transitionDuration:
|
|
126
|
+
transitionDuration: {
|
|
127
|
+
default: durationVars['--duration-fast'],
|
|
128
|
+
'@media (prefers-reduced-motion: reduce)': '0s',
|
|
129
|
+
},
|
|
118
130
|
transitionTimingFunction: easeVars['--ease-standard'],
|
|
119
131
|
flexShrink: 0,
|
|
120
132
|
},
|
|
@@ -146,10 +158,14 @@ const styles = stylex.create({
|
|
|
146
158
|
},
|
|
147
159
|
// Primary action element inside the split-action row (link or button).
|
|
148
160
|
// Flex:1 so it fills remaining space, giving a wide click target.
|
|
161
|
+
// `alignSelf: stretch` makes it fill the row's height too — without it the
|
|
162
|
+
// element collapses to its line box (20px), under the 24px WCAG 2.5.8
|
|
163
|
+
// target-size floor even though the row it sits in is 32px tall.
|
|
149
164
|
// Resets both link and button appearance so it blends into the row.
|
|
150
165
|
splitAction: {
|
|
151
166
|
display: 'flex',
|
|
152
167
|
alignItems: 'center',
|
|
168
|
+
alignSelf: 'stretch',
|
|
153
169
|
gap: spacingVars['--spacing-2'],
|
|
154
170
|
flex: 1,
|
|
155
171
|
minWidth: 0,
|
|
@@ -167,16 +183,23 @@ const styles = stylex.create({
|
|
|
167
183
|
textAlign: 'start',
|
|
168
184
|
cursor: 'pointer',
|
|
169
185
|
},
|
|
170
|
-
//
|
|
186
|
+
// No border and no background: `usePopover` paints the panel this renders
|
|
187
|
+
// into. Drawing a second surface here put square corners inside its rounded
|
|
188
|
+
// ones. The radius matches so a theme retargeting `--radius-container`
|
|
189
|
+
// keeps the two in step.
|
|
171
190
|
popoverSurface: {
|
|
172
|
-
|
|
173
|
-
borderStyle: 'solid',
|
|
174
|
-
borderColor: colorVars['--color-border'],
|
|
191
|
+
borderRadius: radiusVars['--radius-container'],
|
|
175
192
|
paddingBlock: spacingVars['--spacing-1'],
|
|
176
193
|
paddingInline: spacingVars['--spacing-1'],
|
|
177
|
-
marginInlineStart: spacingVars['--spacing-1'],
|
|
178
194
|
minWidth: 180,
|
|
179
195
|
},
|
|
196
|
+
// The gap from the rail belongs on the positioned layer, where
|
|
197
|
+
// `DropdownMenu` keeps it. On the content div it insets the content instead,
|
|
198
|
+
// leaving the panel flush against the rail.
|
|
199
|
+
popoverGap: {
|
|
200
|
+
marginInlineStart: spacingVars['--spacing-1'],
|
|
201
|
+
marginInlineEnd: spacingVars['--spacing-1'],
|
|
202
|
+
},
|
|
180
203
|
popoverHeader: {
|
|
181
204
|
paddingInline: spacingVars['--spacing-2'],
|
|
182
205
|
paddingBlock: spacingVars['--spacing-1'],
|
|
@@ -361,6 +384,7 @@ export function SideNavItem({
|
|
|
361
384
|
'data-testid': testId,
|
|
362
385
|
ref,
|
|
363
386
|
xstyle,
|
|
387
|
+
...rest
|
|
364
388
|
}: SideNavItemProps) {
|
|
365
389
|
const t = useTranslator();
|
|
366
390
|
const {isCollapsed} = useSideNavCollapse();
|
|
@@ -376,7 +400,7 @@ export function SideNavItem({
|
|
|
376
400
|
hasLightDismiss: true,
|
|
377
401
|
hasAutoFocus: true,
|
|
378
402
|
hasCloseButton: false,
|
|
379
|
-
dialogLabel:
|
|
403
|
+
dialogLabel: t('@astryx.sideNavItem.submenuLabel', {label}),
|
|
380
404
|
});
|
|
381
405
|
|
|
382
406
|
// Collapse state for items with children
|
|
@@ -434,42 +458,22 @@ export function SideNavItem({
|
|
|
434
458
|
toggleItemCollapse();
|
|
435
459
|
};
|
|
436
460
|
|
|
437
|
-
//
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
const
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
clearPopoverTimeouts();
|
|
454
|
-
showTimeoutRef.current = setTimeout(() => {
|
|
455
|
-
popover.show({skipAutoFocus: true});
|
|
456
|
-
}, 150);
|
|
457
|
-
}, [clearPopoverTimeouts, popover]);
|
|
458
|
-
|
|
459
|
-
const schedulePopoverHide = useCallback(() => {
|
|
460
|
-
clearPopoverTimeouts();
|
|
461
|
-
hideTimeoutRef.current = setTimeout(() => {
|
|
462
|
-
popover.hide();
|
|
463
|
-
}, 200);
|
|
464
|
-
}, [clearPopoverTimeouts, popover]);
|
|
465
|
-
|
|
466
|
-
const handlePopoverMouseEnter = useCallback(() => {
|
|
467
|
-
schedulePopoverShow();
|
|
468
|
-
}, [schedulePopoverShow]);
|
|
469
|
-
|
|
470
|
-
const handlePopoverMouseLeave = useCallback(() => {
|
|
471
|
-
schedulePopoverHide();
|
|
472
|
-
}, [schedulePopoverHide]);
|
|
461
|
+
// Pointer half only. The hook's `onKeyDown`/`menuRef` drive a `useListFocus`
|
|
462
|
+
// over `[role="menuitem"]`, and this flyout is a focus-trapped dialog of
|
|
463
|
+
// links — wiring them would swallow arrow keys rather than navigate with
|
|
464
|
+
// them. Keyboard stays with `usePopover`'s trap, as in DropdownMenuSubMenu.
|
|
465
|
+
const {triggerProps: hoverTriggerProps, contentProps: hoverContentProps} =
|
|
466
|
+
useMenuHover({
|
|
467
|
+
show: popover.show,
|
|
468
|
+
hide: popover.hide,
|
|
469
|
+
isOpen: popover.isOpen,
|
|
470
|
+
isEnabled: isCollapsed && hasChildren,
|
|
471
|
+
// Standard popover toggling: the flyout opens beside the rail, not over
|
|
472
|
+
// the icon, so the click after a hover-open is a deliberate dismissal
|
|
473
|
+
// rather than the #3121 confirmation the nav menus need.
|
|
474
|
+
clickGuardMs: 0,
|
|
475
|
+
ownsFocus: false,
|
|
476
|
+
});
|
|
473
477
|
|
|
474
478
|
// In collapsed mode: hide items without icons
|
|
475
479
|
if (isCollapsed && !icon) {
|
|
@@ -484,16 +488,21 @@ export function SideNavItem({
|
|
|
484
488
|
displayIcon &&
|
|
485
489
|
renderIconSlot(displayIcon, {
|
|
486
490
|
size: 'sm',
|
|
487
|
-
|
|
491
|
+
// `inherit` so a selected row's icon follows the row to HighlightText
|
|
492
|
+
// under forced colors. Identical to `primary` otherwise: both token
|
|
493
|
+
// families are emitted from one expression.
|
|
494
|
+
color: isSelected ? 'inherit' : isDisabled ? 'disabled' : 'secondary',
|
|
488
495
|
});
|
|
489
496
|
|
|
490
|
-
// Shared collapsed item styles — used by trigger, link, and button
|
|
497
|
+
// Shared collapsed item styles — used by trigger, link, and button.
|
|
498
|
+
// All three render a focusable element, so each draws the shared ring.
|
|
491
499
|
const collapsedItemStyles = mergeProps(
|
|
492
500
|
themeProps('side-nav-item', {
|
|
493
501
|
size,
|
|
494
502
|
selected: isSelected ? 'selected' : null,
|
|
503
|
+
disabled: isDisabled ? 'disabled' : null,
|
|
495
504
|
}),
|
|
496
|
-
|
|
505
|
+
focusOutlineProps.focusVisible(
|
|
497
506
|
navItemStyles.item,
|
|
498
507
|
navItemStyles[size],
|
|
499
508
|
styles.itemCollapsed,
|
|
@@ -511,9 +520,8 @@ export function SideNavItem({
|
|
|
511
520
|
<button
|
|
512
521
|
ref={mergeRefs(ref, popover.triggerRef)}
|
|
513
522
|
type="button"
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
onMouseLeave={handlePopoverMouseLeave}
|
|
523
|
+
{...rest}
|
|
524
|
+
{...hoverTriggerProps}
|
|
517
525
|
aria-label={label}
|
|
518
526
|
data-testid={testId}
|
|
519
527
|
{...popover.triggerProps}
|
|
@@ -523,15 +531,15 @@ export function SideNavItem({
|
|
|
523
531
|
{popover.render(
|
|
524
532
|
<div
|
|
525
533
|
{...stylex.props(styles.popoverSurface)}
|
|
526
|
-
onMouseEnter={
|
|
527
|
-
onMouseLeave={
|
|
534
|
+
onMouseEnter={hoverContentProps.onMouseEnter}
|
|
535
|
+
onMouseLeave={hoverContentProps.onMouseLeave}
|
|
528
536
|
onClick={() => popover.hide()}>
|
|
529
537
|
<div {...stylex.props(styles.popoverHeader)}>{label}</div>
|
|
530
538
|
<SideNavCollapseContext value={EXPANDED_COLLAPSE_STATE}>
|
|
531
539
|
{children}
|
|
532
540
|
</SideNavCollapseContext>
|
|
533
541
|
</div>,
|
|
534
|
-
{placement: 'end', alignment: 'start'},
|
|
542
|
+
{placement: 'end', alignment: 'start', xstyle: styles.popoverGap},
|
|
535
543
|
)}
|
|
536
544
|
</div>
|
|
537
545
|
);
|
|
@@ -552,6 +560,7 @@ export function SideNavItem({
|
|
|
552
560
|
as={as}
|
|
553
561
|
isDisabled={isDisabled}
|
|
554
562
|
onClick={handleClick}
|
|
563
|
+
{...rest}
|
|
555
564
|
{...collapsedAriaProps}
|
|
556
565
|
{...collapsedItemStyles}>
|
|
557
566
|
{collapsedIcon}
|
|
@@ -571,7 +580,8 @@ export function SideNavItem({
|
|
|
571
580
|
{displayIcon &&
|
|
572
581
|
renderIconSlot(displayIcon, {
|
|
573
582
|
size: 'sm',
|
|
574
|
-
|
|
583
|
+
// `inherit` — see the collapsed path above.
|
|
584
|
+
color: isSelected ? 'inherit' : isDisabled ? 'disabled' : 'secondary',
|
|
575
585
|
})}
|
|
576
586
|
{!isCollapsed && <span {...stylex.props(styles.label)}>{label}</span>}
|
|
577
587
|
{!isCollapsed && endContent && (
|
|
@@ -591,45 +601,47 @@ export function SideNavItem({
|
|
|
591
601
|
</>
|
|
592
602
|
);
|
|
593
603
|
|
|
594
|
-
const
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
stylex.props(
|
|
600
|
-
navItemStyles.item,
|
|
601
|
-
navItemStyles[size],
|
|
602
|
-
isSelected && navItemStyles.selected,
|
|
603
|
-
isDisabled && navItemStyles.disabled,
|
|
604
|
-
),
|
|
605
|
-
);
|
|
606
|
-
|
|
607
|
-
// ── Split-action path: <div> row with <a> + <button> as siblings ──
|
|
608
|
-
//
|
|
609
|
-
// When both collapsible and href are set, we can't nest a <button>
|
|
610
|
-
// inside an <a>. Instead we use a <div> as the flex row container,
|
|
611
|
-
// an <a display:contents> for the link (with ::before covering the row),
|
|
612
|
-
// and a <button> sibling lifted above the overlay via z-index.
|
|
604
|
+
const itemThemeProps = themeProps('side-nav-item', {
|
|
605
|
+
size,
|
|
606
|
+
selected: isSelected ? 'selected' : null,
|
|
607
|
+
disabled: isDisabled ? 'disabled' : null,
|
|
608
|
+
});
|
|
613
609
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
610
|
+
const itemStyleArgs = [
|
|
611
|
+
navItemStyles.item,
|
|
612
|
+
navItemStyles[size],
|
|
613
|
+
isSelected && navItemStyles.selected,
|
|
614
|
+
isDisabled && navItemStyles.disabled,
|
|
615
|
+
] as const;
|
|
616
|
+
|
|
617
|
+
// Two shapes of the same row appearance:
|
|
618
|
+
// - `rowProps` for the split-action path, where the row is a plain <div>
|
|
619
|
+
// container and its children take focus (so the ring belongs on them);
|
|
620
|
+
// - `focusableRowProps` for every other path, where the row element is
|
|
621
|
+
// itself the focusable control.
|
|
622
|
+
const rowProps = mergeProps(itemThemeProps, stylex.props(...itemStyleArgs));
|
|
623
|
+
const focusableRowProps = mergeProps(
|
|
624
|
+
itemThemeProps,
|
|
625
|
+
focusOutlineProps.focusVisible(...itemStyleArgs),
|
|
626
|
+
);
|
|
619
627
|
|
|
628
|
+
// Split-action path: collapsible plus a primary action renders the primary
|
|
629
|
+
// element and the chevron toggle as siblings in a <div> row, since a
|
|
630
|
+
// <button> cannot nest inside an <a>.
|
|
620
631
|
let itemElement;
|
|
621
632
|
|
|
622
633
|
if (hasIndependentToggle) {
|
|
623
634
|
itemElement = (
|
|
624
|
-
<div data-testid={testId} {...
|
|
635
|
+
<div data-testid={testId} {...rowProps}>
|
|
625
636
|
<NavItemElement
|
|
626
637
|
ref={ref}
|
|
627
638
|
href={href}
|
|
628
639
|
as={as}
|
|
629
640
|
isDisabled={isDisabled}
|
|
630
641
|
onClick={handleClick}
|
|
642
|
+
{...rest}
|
|
631
643
|
aria-current={isSelected ? ('page' as const) : undefined}
|
|
632
|
-
{...
|
|
644
|
+
{...focusOutlineProps.focusVisible(styles.splitAction)}>
|
|
633
645
|
{itemContent}
|
|
634
646
|
</NavItemElement>
|
|
635
647
|
<button
|
|
@@ -642,7 +654,7 @@ export function SideNavItem({
|
|
|
642
654
|
}
|
|
643
655
|
aria-expanded={!isItemCollapsed}
|
|
644
656
|
aria-controls={`${id}-children`}
|
|
645
|
-
{...
|
|
657
|
+
{...focusOutlineProps.focusVisible(styles.expandToggle)}>
|
|
646
658
|
<Icon
|
|
647
659
|
icon="chevronDown"
|
|
648
660
|
size="lg"
|
|
@@ -671,8 +683,9 @@ export function SideNavItem({
|
|
|
671
683
|
as={as}
|
|
672
684
|
isDisabled={isDisabled}
|
|
673
685
|
onClick={handleClick}
|
|
686
|
+
{...rest}
|
|
674
687
|
{...ariaProps}
|
|
675
|
-
{...
|
|
688
|
+
{...focusableRowProps}>
|
|
676
689
|
{itemContent}
|
|
677
690
|
</NavItemElement>
|
|
678
691
|
);
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
import {mergeProps} from '../utils';
|
|
30
30
|
import type {BaseProps} from '../BaseProps';
|
|
31
31
|
import {useSideNavCollapse} from './SideNavCollapseContext';
|
|
32
|
+
import {VisuallyHidden} from '../VisuallyHidden';
|
|
32
33
|
import {themeProps} from '../utils/themeProps';
|
|
33
34
|
// =============================================================================
|
|
34
35
|
// Styles
|
|
@@ -167,20 +168,6 @@ export function SideNavSection({
|
|
|
167
168
|
|
|
168
169
|
const shouldHideHeader = isHeaderHidden || isCollapsed;
|
|
169
170
|
|
|
170
|
-
const visuallyHiddenStyle: React.CSSProperties = shouldHideHeader
|
|
171
|
-
? {
|
|
172
|
-
position: 'absolute',
|
|
173
|
-
width: 1,
|
|
174
|
-
height: 1,
|
|
175
|
-
padding: 0,
|
|
176
|
-
margin: -1,
|
|
177
|
-
overflow: 'hidden',
|
|
178
|
-
clip: 'rect(0, 0, 0, 0)',
|
|
179
|
-
whiteSpace: 'nowrap',
|
|
180
|
-
borderWidth: 0,
|
|
181
|
-
}
|
|
182
|
-
: {};
|
|
183
|
-
|
|
184
171
|
return (
|
|
185
172
|
<div
|
|
186
173
|
ref={ref}
|
|
@@ -194,12 +181,11 @@ export function SideNavSection({
|
|
|
194
181
|
role="group"
|
|
195
182
|
aria-labelledby={titleId}
|
|
196
183
|
data-testid={testId}>
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
</div>
|
|
184
|
+
{shouldHideHeader ? (
|
|
185
|
+
<VisuallyHidden as="div">{headerContent}</VisuallyHidden>
|
|
186
|
+
) : (
|
|
187
|
+
<div {...stylex.props(styles.header)}>{headerContent}</div>
|
|
188
|
+
)}
|
|
203
189
|
<div {...stylex.props(styles.items)}>{children}</div>
|
|
204
190
|
</div>
|
|
205
191
|
);
|
package/src/SideNav/index.ts
CHANGED
|
@@ -29,6 +29,8 @@ export type {SideNavCollapseButtonProps} from './SideNavCollapseButton';
|
|
|
29
29
|
export {useSideNavCollapse} from './SideNavCollapseContext';
|
|
30
30
|
export type {
|
|
31
31
|
SideNavCollapseState,
|
|
32
|
+
SideNavCollapsibleConfig,
|
|
33
|
+
SideNavControlledCollapsible,
|
|
32
34
|
SideNavImperativeCollapseHandle,
|
|
33
35
|
} from './SideNavCollapseContext';
|
|
34
36
|
|
|
@@ -81,8 +81,8 @@ describe('Slider', () => {
|
|
|
81
81
|
});
|
|
82
82
|
|
|
83
83
|
it.each([
|
|
84
|
-
{value: 150, expectedValue: 100, expectedPosition: '100%'},
|
|
85
|
-
{value: -50, expectedValue: 0, expectedPosition: '0%'},
|
|
84
|
+
{value: 150, expectedValue: 100, expectedPosition: 'calc(100% - 10px)'},
|
|
85
|
+
{value: -50, expectedValue: 0, expectedPosition: 'calc(0% + 10px)'},
|
|
86
86
|
])(
|
|
87
87
|
'clamps a controlled value of $value to $expectedValue',
|
|
88
88
|
({value, expectedValue, expectedPosition}) => {
|
|
@@ -90,11 +90,47 @@ describe('Slider', () => {
|
|
|
90
90
|
const slider = screen.getByRole('slider');
|
|
91
91
|
expect(slider).toHaveAttribute('aria-valuenow', String(expectedValue));
|
|
92
92
|
// Thumb positions via the logical `inset-inline-start` so it mirrors
|
|
93
|
-
// under RTL (see RTL Phase 4)
|
|
93
|
+
// under RTL (see RTL Phase 4), offset by half a thumb so the extremes
|
|
94
|
+
// stay inside the component box.
|
|
94
95
|
expect(slider).toHaveStyle({insetInlineStart: expectedPosition});
|
|
95
96
|
},
|
|
96
97
|
);
|
|
97
98
|
|
|
99
|
+
// Regression: #5050 — at min/max the thumb centred on the container edge,
|
|
100
|
+
// so half of it (10px of a 20px thumb) hung outside the component.
|
|
101
|
+
it.each([
|
|
102
|
+
{value: 0, position: 'calc(0% + 10px)'},
|
|
103
|
+
{value: 50, position: 'calc(50% + 0px)'},
|
|
104
|
+
{value: 100, position: 'calc(100% - 10px)'},
|
|
105
|
+
])(
|
|
106
|
+
'insets the thumb at value $value so it stays in bounds',
|
|
107
|
+
({value, position}) => {
|
|
108
|
+
render(<Slider label="Volume" value={value} min={0} max={100} />);
|
|
109
|
+
expect(screen.getByRole('slider')).toHaveStyle({
|
|
110
|
+
insetInlineStart: position,
|
|
111
|
+
});
|
|
112
|
+
},
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
it.each([
|
|
116
|
+
{value: 0, position: 'calc(0% + 10px)'},
|
|
117
|
+
{value: 100, position: 'calc(100% - 10px)'},
|
|
118
|
+
])(
|
|
119
|
+
'insets a vertical thumb at value $value so it stays in bounds',
|
|
120
|
+
({value, position}) => {
|
|
121
|
+
render(
|
|
122
|
+
<Slider
|
|
123
|
+
label="Volume"
|
|
124
|
+
value={value}
|
|
125
|
+
min={0}
|
|
126
|
+
max={100}
|
|
127
|
+
orientation="vertical"
|
|
128
|
+
/>,
|
|
129
|
+
);
|
|
130
|
+
expect(screen.getByRole('slider')).toHaveStyle({bottom: position});
|
|
131
|
+
},
|
|
132
|
+
);
|
|
133
|
+
|
|
98
134
|
it('range mode sets correct aria values on both thumbs', () => {
|
|
99
135
|
render(
|
|
100
136
|
<Slider
|
|
@@ -580,22 +616,23 @@ describe('Slider', () => {
|
|
|
580
616
|
/>,
|
|
581
617
|
);
|
|
582
618
|
const track = screen.getByRole('slider').parentElement!;
|
|
583
|
-
track.getBoundingClientRect = () =>
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
});
|
|
619
|
+
track.getBoundingClientRect = () => ({
|
|
620
|
+
left: 0,
|
|
621
|
+
top: 0,
|
|
622
|
+
right: 200,
|
|
623
|
+
bottom: 20,
|
|
624
|
+
width: 200,
|
|
625
|
+
height: 20,
|
|
626
|
+
x: 0,
|
|
627
|
+
y: 0,
|
|
628
|
+
toJSON: () => {},
|
|
629
|
+
});
|
|
595
630
|
|
|
596
|
-
// Click at
|
|
631
|
+
// Click at x=50 of a 200px track. The thumb's travel is the track minus
|
|
632
|
+
// half a thumb at each end (180px from x=10), so the fraction is
|
|
633
|
+
// (50 - 10) / 180 → value 22.
|
|
597
634
|
fireEvent.pointerDown(track, {clientX: 50, clientY: 10, pointerId: 1});
|
|
598
|
-
expect(handleChange).toHaveBeenLastCalledWith(
|
|
635
|
+
expect(handleChange).toHaveBeenLastCalledWith(22);
|
|
599
636
|
});
|
|
600
637
|
|
|
601
638
|
it('mirrors a track click to the RTL value when the track is rtl', () => {
|
|
@@ -611,18 +648,17 @@ describe('Slider', () => {
|
|
|
611
648
|
/>,
|
|
612
649
|
);
|
|
613
650
|
const track = screen.getByRole('slider').parentElement!;
|
|
614
|
-
track.getBoundingClientRect = () =>
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
});
|
|
651
|
+
track.getBoundingClientRect = () => ({
|
|
652
|
+
left: 0,
|
|
653
|
+
top: 0,
|
|
654
|
+
right: 200,
|
|
655
|
+
bottom: 20,
|
|
656
|
+
width: 200,
|
|
657
|
+
height: 20,
|
|
658
|
+
x: 0,
|
|
659
|
+
y: 0,
|
|
660
|
+
toJSON: () => {},
|
|
661
|
+
});
|
|
626
662
|
|
|
627
663
|
// Force the track's computed direction to rtl (isRtlElement reads this).
|
|
628
664
|
const realGetComputedStyle = window.getComputedStyle;
|
|
@@ -635,11 +671,11 @@ describe('Slider', () => {
|
|
|
635
671
|
return realGetComputedStyle(el, pseudo ?? undefined);
|
|
636
672
|
});
|
|
637
673
|
|
|
638
|
-
// Same physical click at
|
|
639
|
-
//
|
|
640
|
-
//
|
|
674
|
+
// Same physical click at x=50. Under RTL the inline start is the right
|
|
675
|
+
// edge, so the fraction is (right - 10 - x) / 180 = 140/180 → value 78
|
|
676
|
+
// (the mirror of the LTR value 22).
|
|
641
677
|
fireEvent.pointerDown(track, {clientX: 50, clientY: 10, pointerId: 1});
|
|
642
|
-
expect(handleChange).toHaveBeenLastCalledWith(
|
|
678
|
+
expect(handleChange).toHaveBeenLastCalledWith(78);
|
|
643
679
|
|
|
644
680
|
gcsSpy.mockRestore();
|
|
645
681
|
});
|