@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
package/src/Slider/Slider.tsx
CHANGED
|
@@ -174,6 +174,15 @@ const styles = stylex.create({
|
|
|
174
174
|
},
|
|
175
175
|
trackContainerHorizontal: {
|
|
176
176
|
height: THUMB_SIZE,
|
|
177
|
+
// The whole track is the tap target (a click anywhere on it moves the
|
|
178
|
+
// slider), but it is only THUMB_SIZE (20px) tall — under the WCAG 2.5.8 AA
|
|
179
|
+
// 24px minimum. Floor its block size to 24px on touch pointers only. The
|
|
180
|
+
// rail and thumb center on 50%, so they stay put; only the invisible
|
|
181
|
+
// tappable area grows. Desktop (fine pointer) is unchanged.
|
|
182
|
+
minBlockSize: {
|
|
183
|
+
default: null,
|
|
184
|
+
'@media (pointer: coarse)': '24px',
|
|
185
|
+
},
|
|
177
186
|
width: '100%',
|
|
178
187
|
cursor: 'pointer',
|
|
179
188
|
},
|
|
@@ -366,6 +375,44 @@ function getPercent(val: number, min: number, max: number): number {
|
|
|
366
375
|
return ((val - min) / (max - min)) * 100;
|
|
367
376
|
}
|
|
368
377
|
|
|
378
|
+
/**
|
|
379
|
+
* Thumb travel is inset by half a thumb at each end — the geometry a native
|
|
380
|
+
* `input[type=range]` uses — so the thumb stays inside the component box at
|
|
381
|
+
* min and max instead of overhanging it by half its width (#5050). The fill
|
|
382
|
+
* and the marks map through the same inset, and `travelFraction` inverts it,
|
|
383
|
+
* so the thumb tracks the pointer that grabbed it. Both directions read
|
|
384
|
+
* THUMB_SIZE, so a theme cannot resize the thumb through CSS alone.
|
|
385
|
+
*/
|
|
386
|
+
const THUMB_INSET = THUMB_SIZE / 2;
|
|
387
|
+
|
|
388
|
+
function insetPosition(percent: number): string {
|
|
389
|
+
return cssLength(percent, THUMB_INSET - (percent / 100) * THUMB_SIZE);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/** Distance between two inset positions; the inset itself cancels out. */
|
|
393
|
+
function insetSpan(fromPercent: number, toPercent: number): string {
|
|
394
|
+
const delta = toPercent - fromPercent;
|
|
395
|
+
return cssLength(delta, -(delta / 100) * THUMB_SIZE);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/** Inverse of `insetPosition`: an offset from the box start back to 0–1. */
|
|
399
|
+
function travelFraction(offset: number, size: number): number {
|
|
400
|
+
const travel = size - THUMB_SIZE;
|
|
401
|
+
if (travel > 0) {
|
|
402
|
+
return (offset - THUMB_INSET) / travel;
|
|
403
|
+
}
|
|
404
|
+
// Narrower than the thumb, so there is no travel to map onto: fall back to
|
|
405
|
+
// the raw fraction rather than dividing by zero.
|
|
406
|
+
return size > 0 ? offset / size : 0;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function cssLength(percent: number, px: number): string {
|
|
410
|
+
// Percentages of the box and step arithmetic both carry binary
|
|
411
|
+
// floating-point error into the DOM (`calc(33% + 3.3999999999999995px)`).
|
|
412
|
+
const round = (n: number) => Number(n.toFixed(3));
|
|
413
|
+
return `calc(${round(percent)}% ${px < 0 ? '-' : '+'} ${Math.abs(round(px))}px)`;
|
|
414
|
+
}
|
|
415
|
+
|
|
369
416
|
// =============================================================================
|
|
370
417
|
// Component
|
|
371
418
|
// =============================================================================
|
|
@@ -483,17 +530,21 @@ export function Slider({ref, ...props}: SliderProps) {
|
|
|
483
530
|
}
|
|
484
531
|
const rect = track.getBoundingClientRect();
|
|
485
532
|
|
|
533
|
+
// Inverse of `insetPosition`: the pointer maps onto the thumb's travel
|
|
534
|
+
// (the box minus half a thumb at each end), so pressing on the thumb
|
|
535
|
+
// leaves it where it is instead of jumping.
|
|
486
536
|
let percent: number;
|
|
487
537
|
if (isHorizontal) {
|
|
488
538
|
// In RTL the inline-start (value = min) is the right edge, so measure
|
|
489
539
|
// the pointer fraction from the right instead of the left. Detected
|
|
490
540
|
// from the track's computed direction (lazy, only on pointer move).
|
|
491
|
-
percent =
|
|
492
|
-
?
|
|
493
|
-
|
|
541
|
+
percent = travelFraction(
|
|
542
|
+
isRtlElement(track) ? rect.right - clientX : clientX - rect.left,
|
|
543
|
+
rect.width,
|
|
544
|
+
);
|
|
494
545
|
} else {
|
|
495
546
|
// Vertical: bottom = min, top = max
|
|
496
|
-
percent = 1 - (clientY - rect.top
|
|
547
|
+
percent = 1 - travelFraction(clientY - rect.top, rect.height);
|
|
497
548
|
}
|
|
498
549
|
percent = clamp(percent, 0, 1);
|
|
499
550
|
const raw = min + percent * (max - min);
|
|
@@ -723,8 +774,8 @@ export function Slider({ref, ...props}: SliderProps) {
|
|
|
723
774
|
const percent = getPercent(val, min, max);
|
|
724
775
|
|
|
725
776
|
const positionStyle = isHorizontal
|
|
726
|
-
? {insetInlineStart:
|
|
727
|
-
: {bottom:
|
|
777
|
+
? {insetInlineStart: insetPosition(percent)}
|
|
778
|
+
: {bottom: insetPosition(percent), left: '50%'};
|
|
728
779
|
|
|
729
780
|
// In range mode each thumb keeps a short individual name that composes
|
|
730
781
|
// with the group label (announced via the group's aria-labelledby), per
|
|
@@ -809,22 +860,23 @@ export function Slider({ref, ...props}: SliderProps) {
|
|
|
809
860
|
return thumbElement;
|
|
810
861
|
};
|
|
811
862
|
|
|
812
|
-
// Filled track position
|
|
863
|
+
// Filled track position — ends at the thumb centre, so it uses the same
|
|
864
|
+
// inset mapping as the thumb.
|
|
813
865
|
const filledStyle = (() => {
|
|
814
866
|
if (isRange) {
|
|
815
867
|
const [v0, v1] = values;
|
|
816
868
|
const p0 = getPercent(v0, min, max);
|
|
817
869
|
const p1 = getPercent(v1, min, max);
|
|
818
870
|
if (isHorizontal) {
|
|
819
|
-
return {insetInlineStart:
|
|
871
|
+
return {insetInlineStart: insetPosition(p0), width: insetSpan(p0, p1)};
|
|
820
872
|
}
|
|
821
|
-
return {bottom:
|
|
873
|
+
return {bottom: insetPosition(p0), height: insetSpan(p0, p1)};
|
|
822
874
|
}
|
|
823
875
|
const p = getPercent(values[0], min, max);
|
|
824
876
|
if (isHorizontal) {
|
|
825
|
-
return {insetInlineStart: '0%', width:
|
|
877
|
+
return {insetInlineStart: '0%', width: insetPosition(p)};
|
|
826
878
|
}
|
|
827
|
-
return {bottom: '0%', height:
|
|
879
|
+
return {bottom: '0%', height: insetPosition(p)};
|
|
828
880
|
})();
|
|
829
881
|
|
|
830
882
|
// Text value display
|
|
@@ -949,8 +1001,8 @@ export function Slider({ref, ...props}: SliderProps) {
|
|
|
949
1001
|
{marks.map(mark => {
|
|
950
1002
|
const percent = getPercent(mark.value, min, max);
|
|
951
1003
|
const markPos = isHorizontal
|
|
952
|
-
? {insetInlineStart:
|
|
953
|
-
: {bottom:
|
|
1004
|
+
? {insetInlineStart: insetPosition(percent)}
|
|
1005
|
+
: {bottom: insetPosition(percent)};
|
|
954
1006
|
return (
|
|
955
1007
|
<div key={mark.value}>
|
|
956
1008
|
<div
|
package/src/Switch/Switch.tsx
CHANGED
|
@@ -156,6 +156,26 @@ const styles = stylex.create({
|
|
|
156
156
|
opacity: 0,
|
|
157
157
|
cursor: 'pointer',
|
|
158
158
|
zIndex: 1,
|
|
159
|
+
minInlineSize: {
|
|
160
|
+
default: null,
|
|
161
|
+
'@media (pointer: coarse)': '24px',
|
|
162
|
+
},
|
|
163
|
+
minBlockSize: {
|
|
164
|
+
default: null,
|
|
165
|
+
'@media (pointer: coarse)': '24px',
|
|
166
|
+
},
|
|
167
|
+
insetBlockStart: {
|
|
168
|
+
default: null,
|
|
169
|
+
'@media (pointer: coarse)': '50%',
|
|
170
|
+
},
|
|
171
|
+
insetInlineStart: {
|
|
172
|
+
default: null,
|
|
173
|
+
'@media (pointer: coarse)': '50%',
|
|
174
|
+
},
|
|
175
|
+
transform: {
|
|
176
|
+
default: null,
|
|
177
|
+
'@media (pointer: coarse)': 'translate(-50%, -50%)',
|
|
178
|
+
},
|
|
159
179
|
},
|
|
160
180
|
inputDisabled: {
|
|
161
181
|
cursor: 'not-allowed',
|
|
@@ -22,6 +22,9 @@ export const docs = {
|
|
|
22
22
|
{className: 'astryx-tab-menu-dropdown'},
|
|
23
23
|
{className: 'astryx-tab-menu-item'},
|
|
24
24
|
],
|
|
25
|
+
vars: [
|
|
26
|
+
{name: '--_tab-indicator-bottom', description: 'Vertical offset of the selected-tab indicator from the tab bottom edge. A host that draws its own bottom divider (Toolbar) sets this so the indicator sits on the divider instead of above it.', default: '-1px', private: true},
|
|
27
|
+
],
|
|
25
28
|
},
|
|
26
29
|
description: 'Nav wrapper that provides TabListContext (value, onChange, size) to Tab and TabMenu children.',
|
|
27
30
|
props: [
|
package/src/Text/Text.doc.mjs
CHANGED
|
@@ -15,7 +15,7 @@ export const docs = {
|
|
|
15
15
|
},
|
|
16
16
|
theming: {
|
|
17
17
|
targets: [
|
|
18
|
-
{className: 'astryx-heading', visualProps: ['level', 'color']},
|
|
18
|
+
{className: 'astryx-heading', visualProps: ['level', 'color', 'type']},
|
|
19
19
|
{className: 'astryx-text', visualProps: ['type', 'size', 'color']},
|
|
20
20
|
],
|
|
21
21
|
},
|
|
@@ -77,6 +77,9 @@ export const docs = {
|
|
|
77
77
|
targets: [
|
|
78
78
|
{className: 'astryx-thumbnail'},
|
|
79
79
|
],
|
|
80
|
+
vars: [
|
|
81
|
+
{name: '--_thumbnail-hit-inset', description: 'Outset of the remove button\u2019s invisible hit area, applied to a ::after overlay. 0 on a fine pointer; negative on a coarse one, which grows the 20px button to the 24px touch target without changing what is drawn.', default: '0px', private: true},
|
|
82
|
+
],
|
|
80
83
|
},
|
|
81
84
|
usage: {
|
|
82
85
|
description:
|
|
@@ -207,6 +207,7 @@ const styles = stylex.create({
|
|
|
207
207
|
},
|
|
208
208
|
removeButtonOverrides: {
|
|
209
209
|
'--_button-radius': `calc(${radiusVars['--radius-element']} - ${spacingVars['--spacing-1']})`,
|
|
210
|
+
position: 'relative',
|
|
210
211
|
height: 20,
|
|
211
212
|
minWidth: 20,
|
|
212
213
|
// Fixed colors instead of luminance-adapting theme: a translucent scrim
|
|
@@ -214,6 +215,15 @@ const styles = stylex.create({
|
|
|
214
215
|
// without sampling pixel brightness.
|
|
215
216
|
backgroundColor: colorVars['--color-overlay'],
|
|
216
217
|
color: colorVars['--color-on-dark'],
|
|
218
|
+
'--_thumbnail-hit-inset': {
|
|
219
|
+
default: '0px',
|
|
220
|
+
'@media (pointer: coarse)': '-2px',
|
|
221
|
+
},
|
|
222
|
+
'::after': {
|
|
223
|
+
content: '""',
|
|
224
|
+
position: 'absolute',
|
|
225
|
+
inset: 'var(--_thumbnail-hit-inset)',
|
|
226
|
+
},
|
|
217
227
|
},
|
|
218
228
|
disabled: {
|
|
219
229
|
opacity: 0.5,
|
|
@@ -16,6 +16,24 @@ import {formatTooltipLines} from './tooltipEntries';
|
|
|
16
16
|
import {__resetLiveRegionsForTest} from '../hooks/useAnnounce';
|
|
17
17
|
import {InternationalizationProvider} from '../i18n';
|
|
18
18
|
|
|
19
|
+
async function openTimestampHoverCard(): Promise<HTMLElement> {
|
|
20
|
+
const timestamp = document.querySelector('time');
|
|
21
|
+
if (timestamp == null) {
|
|
22
|
+
throw new Error('Expected Timestamp to render a <time> element');
|
|
23
|
+
}
|
|
24
|
+
const trigger = timestamp.parentElement;
|
|
25
|
+
if (trigger == null) {
|
|
26
|
+
throw new Error('Expected Timestamp to render a hover-card trigger');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
fireEvent.mouseEnter(trigger);
|
|
30
|
+
await waitFor(() => {
|
|
31
|
+
expect(HTMLElement.prototype.showPopover).toHaveBeenCalled();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
return screen.getByRole('dialog', {hidden: true});
|
|
35
|
+
}
|
|
36
|
+
|
|
19
37
|
describe('Timestamp', () => {
|
|
20
38
|
// The hover card is loaded lazily (React.lazy + Suspense), so its chunk
|
|
21
39
|
// resolves asynchronously the first time a card renders. Warm the module
|
|
@@ -646,14 +664,13 @@ describe('Timestamp', () => {
|
|
|
646
664
|
);
|
|
647
665
|
const el = screen.getByTestId('ts');
|
|
648
666
|
|
|
649
|
-
// The card
|
|
650
|
-
//
|
|
651
|
-
//
|
|
652
|
-
//
|
|
653
|
-
//
|
|
667
|
+
// The card carries the full absolute time in its visible form (short
|
|
668
|
+
// timezone abbreviation) as its single default copyable row. The
|
|
669
|
+
// aria-label spells the timezone out in full (WCAG 3.1.4), so the two
|
|
670
|
+
// strings intentionally differ — compare the card against an
|
|
671
|
+
// independently formatted short-form string after opening it.
|
|
654
672
|
// Compare with normalized whitespace: Intl output can contain narrow
|
|
655
673
|
// no-break spaces that jest-dom's matcher normalization would break on.
|
|
656
|
-
const card = await screen.findByRole('dialog', {hidden: true});
|
|
657
674
|
const normalize = (s: string) => s.replace(/\s+/g, ' ');
|
|
658
675
|
const datetime = new Date(el.getAttribute('datetime') ?? '');
|
|
659
676
|
const expected = new Intl.DateTimeFormat(undefined, {
|
|
@@ -665,14 +682,14 @@ describe('Timestamp', () => {
|
|
|
665
682
|
second: '2-digit',
|
|
666
683
|
timeZoneName: 'short',
|
|
667
684
|
}).format(datetime);
|
|
668
|
-
expect(normalize(card.textContent ?? '')).toContain(normalize(expected));
|
|
669
|
-
|
|
670
685
|
// Tab onto the timestamp — the only tab stop in the document.
|
|
671
686
|
await user.tab();
|
|
672
687
|
expect(el).toHaveFocus();
|
|
673
688
|
await waitFor(() => {
|
|
674
689
|
expect(HTMLElement.prototype.showPopover).toHaveBeenCalled();
|
|
675
690
|
});
|
|
691
|
+
const card = screen.getByRole('dialog', {hidden: true});
|
|
692
|
+
expect(normalize(card.textContent ?? '')).toContain(normalize(expected));
|
|
676
693
|
});
|
|
677
694
|
|
|
678
695
|
it('does not add a tab stop when the hover card is disabled', () => {
|
|
@@ -756,7 +773,7 @@ describe('Timestamp', () => {
|
|
|
756
773
|
.queryAllByRole('tooltip', {hidden: true})
|
|
757
774
|
.filter(el => !/^(Copy|Copied)$/.test(el.textContent?.trim() ?? '')),
|
|
758
775
|
).toHaveLength(0);
|
|
759
|
-
const card = await
|
|
776
|
+
const card = await openTimestampHoverCard();
|
|
760
777
|
// The default card is the named details card, exactly as the configured
|
|
761
778
|
// one is.
|
|
762
779
|
expect(card).toHaveAttribute('aria-label', 'Timestamp details');
|
|
@@ -786,7 +803,7 @@ describe('Timestamp', () => {
|
|
|
786
803
|
|
|
787
804
|
it("copies the default absolute row's value", async () => {
|
|
788
805
|
render(<Timestamp value={Date.now() / 1000 - 3600} format="relative" />);
|
|
789
|
-
const card = await
|
|
806
|
+
const card = await openTimestampHoverCard();
|
|
790
807
|
const rowValue = card.querySelector('dd')?.textContent ?? '';
|
|
791
808
|
expect(rowValue).toBeTruthy();
|
|
792
809
|
fireEvent.click(card.querySelector('button')!);
|
|
@@ -802,10 +819,11 @@ describe('Timestamp', () => {
|
|
|
802
819
|
|
|
803
820
|
it("shows a 'Copy' tooltip on the copy button, flipping to 'Copied' after a copy", async () => {
|
|
804
821
|
render(<Timestamp value={Date.now() / 1000 - 3600} format="relative" />);
|
|
805
|
-
const card = await
|
|
822
|
+
const card = await openTimestampHoverCard();
|
|
806
823
|
const button = card.querySelector('button')!;
|
|
807
824
|
// The visible tooltip content defaults to the short imperative 'Copy'
|
|
808
825
|
// (the full "Copy <value>" string stays the button's aria-label).
|
|
826
|
+
fireEvent.mouseEnter(button);
|
|
809
827
|
await waitFor(() =>
|
|
810
828
|
expect(
|
|
811
829
|
screen.getByText('Copy', {
|
|
@@ -872,7 +890,7 @@ describe('Timestamp', () => {
|
|
|
872
890
|
.queryAllByRole('tooltip', {hidden: true})
|
|
873
891
|
.filter(el => !/^(Copy|Copied)$/.test(el.textContent?.trim() ?? '')),
|
|
874
892
|
).toHaveLength(0);
|
|
875
|
-
const card = await
|
|
893
|
+
const card = await openTimestampHoverCard();
|
|
876
894
|
expect(card).toHaveAttribute('aria-label', 'Timestamp details');
|
|
877
895
|
});
|
|
878
896
|
|
|
@@ -888,7 +906,7 @@ describe('Timestamp', () => {
|
|
|
888
906
|
]}
|
|
889
907
|
/>,
|
|
890
908
|
);
|
|
891
|
-
const card = await
|
|
909
|
+
const card = await openTimestampHoverCard();
|
|
892
910
|
expect(card.querySelectorAll('dd')).toHaveLength(3);
|
|
893
911
|
// Entries are read-only unless they opt in, so no copy buttons and no
|
|
894
912
|
// trailing action column are rendered.
|
|
@@ -915,7 +933,7 @@ describe('Timestamp', () => {
|
|
|
915
933
|
]}
|
|
916
934
|
/>,
|
|
917
935
|
);
|
|
918
|
-
const card = await
|
|
936
|
+
const card = await openTimestampHoverCard();
|
|
919
937
|
// Three rows, but only the opted-in row carries a copy button.
|
|
920
938
|
expect(card.querySelectorAll('dd')).toHaveLength(3);
|
|
921
939
|
expect(card.querySelectorAll('button')).toHaveLength(1);
|
|
@@ -936,7 +954,7 @@ describe('Timestamp', () => {
|
|
|
936
954
|
]}
|
|
937
955
|
/>,
|
|
938
956
|
);
|
|
939
|
-
const card = await
|
|
957
|
+
const card = await openTimestampHoverCard();
|
|
940
958
|
const values = Array.from(card.querySelectorAll('dd')).map(
|
|
941
959
|
el => el.textContent,
|
|
942
960
|
);
|
|
@@ -960,7 +978,7 @@ describe('Timestamp', () => {
|
|
|
960
978
|
]}
|
|
961
979
|
/>,
|
|
962
980
|
);
|
|
963
|
-
const card = await
|
|
981
|
+
const card = await openTimestampHoverCard();
|
|
964
982
|
const [rowValue] = Array.from(card.querySelectorAll('dd')).map(
|
|
965
983
|
el => el.textContent ?? '',
|
|
966
984
|
);
|
|
@@ -991,7 +1009,7 @@ describe('Timestamp', () => {
|
|
|
991
1009
|
/>
|
|
992
1010
|
</InternationalizationProvider>,
|
|
993
1011
|
);
|
|
994
|
-
const card = await
|
|
1012
|
+
const card = await openTimestampHoverCard();
|
|
995
1013
|
fireEvent.click(card.querySelector('button')!);
|
|
996
1014
|
await waitFor(() => {
|
|
997
1015
|
expect(
|
|
@@ -1011,7 +1029,7 @@ describe('Timestamp', () => {
|
|
|
1011
1029
|
);
|
|
1012
1030
|
// Without entries an absolute format has no hover surface at all;
|
|
1013
1031
|
// configuring entries must not be silently ignored.
|
|
1014
|
-
const card = await
|
|
1032
|
+
const card = await openTimestampHoverCard();
|
|
1015
1033
|
expect(card.textContent).toContain('UTC');
|
|
1016
1034
|
// ...and the anchor becomes keyboard-reachable, as it is for relative.
|
|
1017
1035
|
expect(screen.getByTestId('ts')).toHaveAttribute('tabindex', '0');
|
|
@@ -1120,7 +1138,7 @@ describe('Timestamp', () => {
|
|
|
1120
1138
|
/>,
|
|
1121
1139
|
);
|
|
1122
1140
|
// autoThreshold={0} forces the absolute branch regardless of the clock.
|
|
1123
|
-
const card = await
|
|
1141
|
+
const card = await openTimestampHoverCard();
|
|
1124
1142
|
expect(card.textContent).toContain('2026-02-19 17:00:00');
|
|
1125
1143
|
expect(screen.getByTestId('ts')).toHaveAttribute('tabindex', '0');
|
|
1126
1144
|
});
|
|
@@ -1135,7 +1153,7 @@ describe('Timestamp', () => {
|
|
|
1135
1153
|
]}
|
|
1136
1154
|
/>,
|
|
1137
1155
|
);
|
|
1138
|
-
const card = await
|
|
1156
|
+
const card = await openTimestampHoverCard();
|
|
1139
1157
|
expect(card.textContent).toContain('2026-02-19 17:00:00');
|
|
1140
1158
|
});
|
|
1141
1159
|
|
|
@@ -1151,7 +1169,7 @@ describe('Timestamp', () => {
|
|
|
1151
1169
|
]}
|
|
1152
1170
|
/>,
|
|
1153
1171
|
);
|
|
1154
|
-
const card = await
|
|
1172
|
+
const card = await openTimestampHoverCard();
|
|
1155
1173
|
// An unlabeled entry still emits its label cell, so the grid stays
|
|
1156
1174
|
// aligned and the <dl> stays valid markup.
|
|
1157
1175
|
expect(card.querySelectorAll('dt')).toHaveLength(3);
|
|
@@ -1189,7 +1207,7 @@ describe('Timestamp', () => {
|
|
|
1189
1207
|
]}
|
|
1190
1208
|
/>,
|
|
1191
1209
|
);
|
|
1192
|
-
const card = await
|
|
1210
|
+
const card = await openTimestampHoverCard();
|
|
1193
1211
|
const values = Array.from(card.querySelectorAll('dd')).map(
|
|
1194
1212
|
el => el.textContent,
|
|
1195
1213
|
);
|
|
@@ -72,11 +72,11 @@ describe('Toolbar', () => {
|
|
|
72
72
|
expect(screen.getByTestId('end')).toBeInTheDocument();
|
|
73
73
|
|
|
74
74
|
// Three-slot layout produces 3 child divs (plus the aria-hidden
|
|
75
|
-
// keyboard-hint
|
|
75
|
+
// keyboard-hint layer infrastructure, excluded as an implementation detail)
|
|
76
76
|
const toolbar = screen.getByRole('toolbar');
|
|
77
|
-
expect(
|
|
78
|
-
|
|
79
|
-
);
|
|
77
|
+
expect(
|
|
78
|
+
toolbar.querySelectorAll(':scope > :not([popover]):not(template)'),
|
|
79
|
+
).toHaveLength(3);
|
|
80
80
|
});
|
|
81
81
|
|
|
82
82
|
it('renders two-slot layout without centerContent', () => {
|
|
@@ -91,11 +91,11 @@ describe('Toolbar', () => {
|
|
|
91
91
|
expect(screen.getByTestId('end')).toBeInTheDocument();
|
|
92
92
|
|
|
93
93
|
// Two-slot layout produces 2 child divs (plus the aria-hidden
|
|
94
|
-
// keyboard-hint
|
|
94
|
+
// keyboard-hint layer infrastructure, excluded here)
|
|
95
95
|
const toolbar = screen.getByRole('toolbar');
|
|
96
|
-
expect(
|
|
97
|
-
|
|
98
|
-
);
|
|
96
|
+
expect(
|
|
97
|
+
toolbar.querySelectorAll(':scope > :not([popover]):not(template)'),
|
|
98
|
+
).toHaveLength(2);
|
|
99
99
|
});
|
|
100
100
|
|
|
101
101
|
it('renders start-only layout', () => {
|
|
@@ -107,9 +107,9 @@ describe('Toolbar', () => {
|
|
|
107
107
|
);
|
|
108
108
|
expect(screen.getByTestId('start')).toBeInTheDocument();
|
|
109
109
|
const toolbar = screen.getByRole('toolbar');
|
|
110
|
-
expect(
|
|
111
|
-
|
|
112
|
-
);
|
|
110
|
+
expect(
|
|
111
|
+
toolbar.querySelectorAll(':scope > :not([popover]):not(template)'),
|
|
112
|
+
).toHaveLength(1);
|
|
113
113
|
});
|
|
114
114
|
|
|
115
115
|
it('renders end-only layout', () => {
|
|
@@ -121,9 +121,9 @@ describe('Toolbar', () => {
|
|
|
121
121
|
);
|
|
122
122
|
expect(screen.getByTestId('end')).toBeInTheDocument();
|
|
123
123
|
const toolbar = screen.getByRole('toolbar');
|
|
124
|
-
expect(
|
|
125
|
-
|
|
126
|
-
);
|
|
124
|
+
expect(
|
|
125
|
+
toolbar.querySelectorAll(':scope > :not([popover]):not(template)'),
|
|
126
|
+
).toHaveLength(1);
|
|
127
127
|
});
|
|
128
128
|
|
|
129
129
|
it('sets aria-orientation to horizontal by default', () => {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import {describe, it, expect, vi} from 'vitest';
|
|
13
|
-
import {fireEvent, render, screen} from '@testing-library/react';
|
|
13
|
+
import {fireEvent, render, screen, act} from '@testing-library/react';
|
|
14
14
|
import userEvent from '@testing-library/user-event';
|
|
15
15
|
import {TopNav} from './TopNav';
|
|
16
16
|
import {TopNavRenderContext} from './TopNavRenderContext';
|
|
@@ -660,3 +660,88 @@ describe('TopNavItem', () => {
|
|
|
660
660
|
});
|
|
661
661
|
});
|
|
662
662
|
});
|
|
663
|
+
|
|
664
|
+
describe('TopNavHeading hover/click guard', () => {
|
|
665
|
+
// tabIndex={-1} matches real menu items; a bare role=menuitem div is not
|
|
666
|
+
// focusable.
|
|
667
|
+
const menuItems = (
|
|
668
|
+
<>
|
|
669
|
+
<div role="menuitem" tabIndex={-1}>
|
|
670
|
+
Alpha
|
|
671
|
+
</div>
|
|
672
|
+
<div role="menuitem" tabIndex={-1}>
|
|
673
|
+
Beta
|
|
674
|
+
</div>
|
|
675
|
+
</>
|
|
676
|
+
);
|
|
677
|
+
|
|
678
|
+
it('keeps the menu open when a hover-open is immediately clicked', async () => {
|
|
679
|
+
vi.useFakeTimers({shouldAdvanceTime: true});
|
|
680
|
+
const user = userEvent.setup({advanceTimers: vi.advanceTimersByTime});
|
|
681
|
+
render(<TopNavHeading heading="My App" menu={menuItems} />);
|
|
682
|
+
const trigger = screen.getByRole('button', {name: 'Open menu'});
|
|
683
|
+
|
|
684
|
+
await user.hover(trigger);
|
|
685
|
+
act(() => {
|
|
686
|
+
vi.advanceTimersByTime(50);
|
|
687
|
+
});
|
|
688
|
+
expect(trigger).toHaveAttribute('aria-expanded', 'true');
|
|
689
|
+
|
|
690
|
+
await user.click(trigger);
|
|
691
|
+
expect(trigger).toHaveAttribute('aria-expanded', 'true');
|
|
692
|
+
|
|
693
|
+
vi.useRealTimers();
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
it('closes on a click that lands well after the hover-open', async () => {
|
|
697
|
+
vi.useFakeTimers({shouldAdvanceTime: true});
|
|
698
|
+
const user = userEvent.setup({advanceTimers: vi.advanceTimersByTime});
|
|
699
|
+
render(<TopNavHeading heading="My App" menu={menuItems} />);
|
|
700
|
+
const trigger = screen.getByRole('button', {name: 'Open menu'});
|
|
701
|
+
|
|
702
|
+
await user.hover(trigger);
|
|
703
|
+
act(() => {
|
|
704
|
+
vi.advanceTimersByTime(50);
|
|
705
|
+
});
|
|
706
|
+
act(() => {
|
|
707
|
+
vi.advanceTimersByTime(1200);
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
await user.click(trigger);
|
|
711
|
+
expect(trigger).toHaveAttribute('aria-expanded', 'false');
|
|
712
|
+
|
|
713
|
+
vi.useRealTimers();
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
it('leaves focus on the trigger for a hover-open, and moves it in on click', async () => {
|
|
717
|
+
vi.useFakeTimers({shouldAdvanceTime: true});
|
|
718
|
+
const user = userEvent.setup({advanceTimers: vi.advanceTimersByTime});
|
|
719
|
+
render(<TopNavHeading heading="My App" menu={menuItems} />);
|
|
720
|
+
const trigger = screen.getByRole('button', {name: 'Open menu'});
|
|
721
|
+
|
|
722
|
+
await user.hover(trigger);
|
|
723
|
+
act(() => {
|
|
724
|
+
vi.advanceTimersByTime(50);
|
|
725
|
+
});
|
|
726
|
+
const firstItem = screen.getAllByRole('menuitem', {hidden: true})[0];
|
|
727
|
+
expect(firstItem).not.toHaveFocus();
|
|
728
|
+
|
|
729
|
+
await user.click(trigger);
|
|
730
|
+
expect(firstItem).toHaveFocus();
|
|
731
|
+
|
|
732
|
+
vi.useRealTimers();
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
it('returns focus to the trigger on Escape', async () => {
|
|
736
|
+
const user = userEvent.setup();
|
|
737
|
+
render(<TopNavHeading heading="My App" menu={menuItems} />);
|
|
738
|
+
const trigger = screen.getByRole('button', {name: 'Open menu'});
|
|
739
|
+
|
|
740
|
+
await user.click(trigger);
|
|
741
|
+
expect(trigger).toHaveAttribute('aria-expanded', 'true');
|
|
742
|
+
|
|
743
|
+
await user.keyboard('{Escape}');
|
|
744
|
+
expect(trigger).toHaveAttribute('aria-expanded', 'false');
|
|
745
|
+
expect(trigger).toHaveFocus();
|
|
746
|
+
});
|
|
747
|
+
});
|
|
@@ -335,23 +335,27 @@ export function TopNavHeading({
|
|
|
335
335
|
hasCloseButton: false,
|
|
336
336
|
});
|
|
337
337
|
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
338
|
+
const {
|
|
339
|
+
triggerProps,
|
|
340
|
+
contentProps,
|
|
341
|
+
menuRef,
|
|
342
|
+
setTriggerEl,
|
|
343
|
+
close: closeMenu,
|
|
344
|
+
} = useMenuHover<HTMLDivElement>({
|
|
345
|
+
show: popover.show,
|
|
346
|
+
hide: popover.hide,
|
|
347
|
+
isOpen: popover.isOpen,
|
|
348
|
+
isEnabled: !!menu,
|
|
349
|
+
showDelay: 0,
|
|
350
|
+
});
|
|
342
351
|
|
|
343
|
-
const
|
|
344
|
-
useMenuHover<HTMLDivElement>({
|
|
345
|
-
show: popover.show,
|
|
346
|
-
hide: popover.hide,
|
|
347
|
-
isOpen: popover.isOpen,
|
|
348
|
-
isEnabled: !!menu,
|
|
349
|
-
showDelay: 0,
|
|
350
|
-
});
|
|
352
|
+
const closeMenuCtx = useMemo(() => ({closeMenu}), [closeMenu]);
|
|
351
353
|
|
|
354
|
+
// setTriggerEl belongs on the chevron button, not this root: it is the
|
|
355
|
+
// focus-restore target and a <div> cannot take focus. triggerRef stays here
|
|
356
|
+
// because the panel anchors to the whole heading.
|
|
352
357
|
const setRef = mergeRefs<HTMLElement>(
|
|
353
358
|
rootRef,
|
|
354
|
-
setTriggerEl,
|
|
355
359
|
ref,
|
|
356
360
|
menu ? popover.triggerRef : undefined,
|
|
357
361
|
);
|
|
@@ -426,7 +430,8 @@ export function TopNavHeading({
|
|
|
426
430
|
<button
|
|
427
431
|
type="button"
|
|
428
432
|
{...stylex.props(styles.popoverHeading)}
|
|
429
|
-
|
|
433
|
+
// A close affordance, not the trigger: dismiss only.
|
|
434
|
+
onClick={closeMenu}>
|
|
430
435
|
{logo && <span {...stylex.props(styles.logo)}>{logo}</span>}
|
|
431
436
|
{renderTextContent(
|
|
432
437
|
<Icon
|
|
@@ -505,6 +510,7 @@ export function TopNavHeading({
|
|
|
505
510
|
// Stays a <button>: this box is the popover trigger, so it carries
|
|
506
511
|
// the accessible name and handlers — only the glyph moves to Icon.
|
|
507
512
|
<button
|
|
513
|
+
ref={setTriggerEl}
|
|
508
514
|
type="button"
|
|
509
515
|
aria-label={t('@astryx.topNav.heading.openMenu')}
|
|
510
516
|
onClick={e => {
|
|
@@ -582,6 +588,7 @@ export function TopNavHeading({
|
|
|
582
588
|
// carries the accessible name and handlers — only the glyph
|
|
583
589
|
// moves to Icon.
|
|
584
590
|
<button
|
|
591
|
+
ref={setTriggerEl}
|
|
585
592
|
type="button"
|
|
586
593
|
aria-label={t('@astryx.topNav.heading.openMenu')}
|
|
587
594
|
onClick={e => {
|