@astryxdesign/core 0.7.2-canary.fbc03f7 → 0.7.2-canary.fdd384f
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/AppShell/AppShell.d.ts.map +1 -1
- package/dist/AppShell/AppShell.js +40 -1
- package/dist/CommandPalette/CommandPaletteInput.d.ts +1 -1
- package/dist/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
- package/dist/CommandPalette/CommandPaletteInput.js +37 -5
- package/dist/Heading/Heading.d.ts.map +1 -1
- package/dist/Heading/Heading.js +4 -2
- package/dist/MobileNav/MobileNav.d.ts +9 -0
- package/dist/MobileNav/MobileNav.d.ts.map +1 -1
- package/dist/MobileNav/MobileNav.js +30 -3
- package/dist/SideNav/SideNavItem.d.ts +8 -1
- package/dist/SideNav/SideNavItem.d.ts.map +1 -1
- package/dist/SideNav/SideNavItem.js +71 -9
- package/dist/Text/Text.d.ts.map +1 -1
- package/dist/Text/Text.js +4 -2
- package/dist/TopNav/TopNavMegaMenu.d.ts.map +1 -1
- package/dist/TopNav/TopNavMegaMenu.js +3 -3
- package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
- package/dist/TopNav/TopNavMenu.js +3 -3
- package/dist/TreeList/TreeList.d.ts.map +1 -1
- package/dist/TreeList/TreeList.js +1 -0
- package/dist/TreeList/TreeListBranches.d.ts.map +1 -1
- package/dist/TreeList/TreeListBranches.js +2 -2
- package/dist/TreeList/TreeListItem.d.ts.map +1 -1
- package/dist/TreeList/TreeListItem.js +57 -9
- package/dist/astryx.css +11 -1
- package/package.json +3 -3
- package/src/AppShell/AppShell.tsx +45 -1
- package/src/Breadcrumbs/Breadcrumbs.test.tsx +30 -1
- package/src/Card/Card.doc.mjs +1 -1
- package/src/CommandPalette/CommandPaletteInput.test.tsx +53 -4
- package/src/CommandPalette/CommandPaletteInput.tsx +33 -3
- package/src/CommandPalette/__tests__/CommandPaletteInputAudit.a11y.chromium.spec.ts +335 -0
- package/src/CommandPalette/__tests__/CommandPaletteItemAudit.a11y.chromium.spec.ts +292 -0
- package/src/ContextMenu/ContextMenu.doc.mjs +2 -3
- package/src/ContextMenu/__tests__/ContextMenuAudit.a11y.chromium.spec.ts +438 -0
- package/src/DropdownMenu/DropdownMenu.test.tsx +41 -1
- package/src/DropdownMenu/DropdownMenuDivider.spec.md +192 -0
- package/src/DropdownMenu/__tests__/DropdownMenuDividerAudit.a11y.chromium.spec.ts +280 -0
- package/src/Heading/Heading.test.tsx +57 -0
- package/src/Heading/Heading.tsx +6 -2
- package/src/MobileNav/MobileNav.tsx +30 -3
- package/src/MobileNav/MobileNavCloseEdgeCases.test.tsx +173 -15
- package/src/SideNav/SideNav.test.tsx +179 -0
- package/src/SideNav/SideNavItem.doc.mjs +2 -2
- package/src/SideNav/SideNavItem.tsx +96 -17
- package/src/Text/Text.test.tsx +57 -0
- package/src/Text/Text.tsx +6 -2
- package/src/TopNav/TopNavMegaMenu.test.tsx +22 -0
- package/src/TopNav/TopNavMegaMenu.tsx +7 -3
- package/src/TopNav/TopNavMenu.test.tsx +19 -0
- package/src/TopNav/TopNavMenu.tsx +11 -3
- package/src/TreeList/TreeList.doc.mjs +1 -0
- package/src/TreeList/TreeList.test.tsx +327 -6
- package/src/TreeList/TreeList.tsx +5 -1
- package/src/TreeList/TreeListBranches.tsx +6 -0
- package/src/TreeList/TreeListItem.tsx +88 -13
- package/src/theme/derivedVarRegistry.test.ts +4 -0
|
@@ -13,13 +13,14 @@
|
|
|
13
13
|
* - /packages/core/src/TreeList/TreeList.tsx
|
|
14
14
|
* - /packages/cli/assets/templates/blocks/components/TreeList/ (showcase blocks)
|
|
15
15
|
*/
|
|
16
|
-
import { useId, useMemo } from 'react';
|
|
16
|
+
import { useId, useMemo, useRef } from 'react';
|
|
17
17
|
import * as stylex from '@stylexjs/stylex';
|
|
18
18
|
import "../theme/tokens.stylex.js";
|
|
19
19
|
import { colorVars, radiusVars, spacingVars, durationVars, easeVars, typeScaleVars } from "../theme/tokens.stylex.js";
|
|
20
20
|
import { focusOutlineProps } from "../utils/focusOutline.stylex.js";
|
|
21
21
|
import { interactionOverlayStyles } from "../utils/interactionOverlay.stylex.js";
|
|
22
22
|
import { usePressFeedback } from "../hooks/usePressFeedback.js";
|
|
23
|
+
import { hasInteractiveAncestor, useClickableContainer } from "../hooks/useClickableContainer.js";
|
|
23
24
|
import { Icon } from "../Icon/index.js";
|
|
24
25
|
import { mergeProps } from "../utils/index.js";
|
|
25
26
|
import { useLinkComponent } from "../Link/useLinkComponent.js";
|
|
@@ -38,6 +39,7 @@ const styles = {
|
|
|
38
39
|
kmVPX3: "x1717udv",
|
|
39
40
|
kVAEAm: "x1n2onr6",
|
|
40
41
|
kzqmXN: "xh8yej3",
|
|
42
|
+
"--_tree-focus-z": "x70muob x1sk5w8o",
|
|
41
43
|
kI3sdo: "x1a2a7pz",
|
|
42
44
|
$$css: true
|
|
43
45
|
},
|
|
@@ -51,7 +53,10 @@ const styles = {
|
|
|
51
53
|
kVAEAm: "x1n2onr6",
|
|
52
54
|
kB7OPa: "x9f619",
|
|
53
55
|
k9WMMc: "x1yc453h",
|
|
54
|
-
|
|
56
|
+
$$css: true
|
|
57
|
+
},
|
|
58
|
+
focusLift: {
|
|
59
|
+
kY2c9j: "x6tapwc x3x83r7",
|
|
55
60
|
$$css: true
|
|
56
61
|
},
|
|
57
62
|
interactive: {
|
|
@@ -177,6 +182,7 @@ export function TreeListItem({
|
|
|
177
182
|
const labelId = useId();
|
|
178
183
|
const descriptionId = useId();
|
|
179
184
|
const LinkComponent = useLinkComponent();
|
|
185
|
+
const linkRef = useRef(null);
|
|
180
186
|
const isInteractive = onClick != null || href != null;
|
|
181
187
|
const handleToggle = useMemo(() => hasChildren && onToggle != null ? e => {
|
|
182
188
|
e.stopPropagation();
|
|
@@ -188,8 +194,10 @@ export function TreeListItem({
|
|
|
188
194
|
if (isDisabled) {
|
|
189
195
|
return;
|
|
190
196
|
}
|
|
191
|
-
|
|
192
|
-
|
|
197
|
+
// Controls inside the row, native or role-based, keep their own
|
|
198
|
+
// activation (FR7): the same rule link rows get from the shared
|
|
199
|
+
// enlarged-surface hook below.
|
|
200
|
+
if (hasInteractiveAncestor(e.target, e.currentTarget)) {
|
|
193
201
|
return;
|
|
194
202
|
}
|
|
195
203
|
if (onClick != null) {
|
|
@@ -202,6 +210,38 @@ export function TreeListItem({
|
|
|
202
210
|
return undefined;
|
|
203
211
|
}, [onClick, hasChildren, onToggle, id, isDisabled]);
|
|
204
212
|
|
|
213
|
+
// A link row (an `href` and no other row action) is an enlarged surface for
|
|
214
|
+
// its link: the indent is part of the row, so a click anywhere in it must
|
|
215
|
+
// follow the link, as Enter already does. Built on the shared
|
|
216
|
+
// enlarged-surface hook (family:navigation-destinations), which skips
|
|
217
|
+
// clicks on nested controls, native or role-based, and text selections.
|
|
218
|
+
// - Clicks are delegated: the hook re-dispatches the click on the anchor
|
|
219
|
+
// with its modifier keys, so the link component and the browser handle
|
|
220
|
+
// same-tab, new-tab and new-window activation, and stops the original, so
|
|
221
|
+
// ancestors see one click per press.
|
|
222
|
+
// - Middle-click has no synthetic equivalent, so it takes the hook's href
|
|
223
|
+
// path, which opens the destination in a new tab after the shared
|
|
224
|
+
// navigation rule.
|
|
225
|
+
// The container ref is attached to link rows only, so every other row stays
|
|
226
|
+
// a plain element and both hook instances stay inert on it.
|
|
227
|
+
const forwardsToLink = href != null && handleClick == null;
|
|
228
|
+
const linkRowRef = useRef(null);
|
|
229
|
+
const {
|
|
230
|
+
onClick: delegateToLink
|
|
231
|
+
} = useClickableContainer({
|
|
232
|
+
containerRef: linkRowRef,
|
|
233
|
+
interactiveRef: linkRef,
|
|
234
|
+
disabled: isDisabled || !forwardsToLink
|
|
235
|
+
});
|
|
236
|
+
const {
|
|
237
|
+
onMouseUp: openLinkOnMiddleClick
|
|
238
|
+
} = useClickableContainer({
|
|
239
|
+
containerRef: linkRowRef,
|
|
240
|
+
href,
|
|
241
|
+
target,
|
|
242
|
+
disabled: isDisabled || !forwardsToLink
|
|
243
|
+
});
|
|
244
|
+
|
|
205
245
|
// Per-level indent distance. The per-level step is the public, themeable
|
|
206
246
|
// `--tree-list-indent` lever (default `--spacing-4`, set on the tree-list
|
|
207
247
|
// root). A leaf adds a fixed chevron-column offset (chevron width + gap) so
|
|
@@ -211,8 +251,8 @@ export function TreeListItem({
|
|
|
211
251
|
// every row sits flush, like a parent at that level. That offset is tied to
|
|
212
252
|
// the chevron's own dimensions, not the indent step, so it does not scale
|
|
213
253
|
// with the lever. Published as the private `--_tree-indent` and consumed by
|
|
214
|
-
// `
|
|
215
|
-
//
|
|
254
|
+
// `indentedContent`'s stylesheet `padding-inline-start`; the step stays
|
|
255
|
+
// themeable through `--tree-list-indent` (see #4308).
|
|
216
256
|
const reservesChevronColumn = !hasChildren && hasExpandableItems;
|
|
217
257
|
const indentDistance = reservesChevronColumn ? `calc(${nestedLevel} * var(--tree-list-indent) + ${spacingVars['--spacing-4']} + ${spacingVars['--spacing-2']})` : `calc(${nestedLevel} * var(--tree-list-indent))`;
|
|
218
258
|
const indentStyle = {
|
|
@@ -308,6 +348,7 @@ export function TreeListItem({
|
|
|
308
348
|
},
|
|
309
349
|
children: startContent
|
|
310
350
|
}), href != null ? /*#__PURE__*/_jsx(LinkComponent, {
|
|
351
|
+
ref: linkRef,
|
|
311
352
|
href: href,
|
|
312
353
|
target: target,
|
|
313
354
|
"aria-disabled": isDisabled || undefined,
|
|
@@ -389,7 +430,7 @@ export function TreeListItem({
|
|
|
389
430
|
density,
|
|
390
431
|
selected: isSelected ? 'selected' : null,
|
|
391
432
|
disabled: isDisabled ? 'disabled' : null
|
|
392
|
-
}), isInteractive || hasChildren && onClick == null ? focusOutlineProps.focusWithinOrPublished(styles.contentWrapper, densityStyles[density], styles.interactive, interactionOverlayStyles.backgroundImage, isDisabled && styles.disabled, isSelected && styles.selected, xstyle) : stylex.props(styles.contentWrapper, densityStyles[density], isDisabled && styles.disabled, isSelected && styles.selected, xstyle),
|
|
433
|
+
}), isInteractive || hasChildren && onClick == null ? focusOutlineProps.focusWithinOrPublished(styles.contentWrapper, styles.focusLift, densityStyles[density], styles.interactive, interactionOverlayStyles.backgroundImage, isDisabled && styles.disabled, isSelected && styles.selected, xstyle) : stylex.props(styles.contentWrapper, densityStyles[density], isDisabled && styles.disabled, isSelected && styles.selected, xstyle),
|
|
393
434
|
// Consumer row props are merged last so useContainerReveal can
|
|
394
435
|
// publish both its classes and inline custom properties. Seed the
|
|
395
436
|
// private indent first, then preserve the standard inline-style
|
|
@@ -398,8 +439,15 @@ export function TreeListItem({
|
|
|
398
439
|
...indentStyle,
|
|
399
440
|
...style
|
|
400
441
|
}),
|
|
401
|
-
|
|
402
|
-
|
|
442
|
+
ref: forwardsToLink ? linkRowRef : undefined,
|
|
443
|
+
onClick: forwardsToLink ? delegateToLink : handleClick,
|
|
444
|
+
onMouseUp: forwardsToLink ? openLinkOnMiddleClick : undefined,
|
|
445
|
+
children: /*#__PURE__*/_jsx("div", {
|
|
446
|
+
...{
|
|
447
|
+
className: "x78zum5 x98rzlu xeuugli x1h91t0o x14kbac9 xw2xrtw"
|
|
448
|
+
},
|
|
449
|
+
children: innerContent
|
|
450
|
+
})
|
|
403
451
|
})
|
|
404
452
|
}), isExpanded && renderedChildren != null && /*#__PURE__*/_jsx("ul", {
|
|
405
453
|
role: "group",
|
package/dist/astryx.css
CHANGED
|
@@ -174,6 +174,7 @@
|
|
|
174
174
|
.x1ur4yav{--_thumbnail-hit-inset:0px}
|
|
175
175
|
.x87gnkx{--_toast-slide-y:calc(-1 * var(--spacing-2))}
|
|
176
176
|
.x1heqljd{--_toast-slide-y:var(--spacing-2)}
|
|
177
|
+
.x70muob{--_tree-focus-z:auto}
|
|
177
178
|
.x16bblx1{--button-focus-offset:var(--focus-outline-offset)}
|
|
178
179
|
.x1v15ycx{--button-icon-only-aspect:1 / 1}
|
|
179
180
|
.x1mtvhk1{--color-syntax-background:transparent}
|
|
@@ -416,6 +417,7 @@
|
|
|
416
417
|
.x1mjsb9v:focus-within{--_reveal-delay:0s,0s}
|
|
417
418
|
.xi33vf3:focus-within{--_reveal-opacity:1}
|
|
418
419
|
.xoa21yc:focus-within{--_reveal-position:static}
|
|
420
|
+
.x1sk5w8o:focus-visible{--_tree-focus-z:2}
|
|
419
421
|
.x11woleg:focus-visible{--indicator-color:var(--color-accent)}
|
|
420
422
|
.x1i79oho.x1i79oho:hover:where(:not(:disabled,[aria-disabled="true"])){--indicator-color:var(--resize-hint-gate,var(--color-accent))}
|
|
421
423
|
.x1qs65gq:has(> [data-size="lg"]){--_field-status-overlap:calc(var(--size-element-lg) / 2)}
|
|
@@ -683,6 +685,7 @@
|
|
|
683
685
|
.xpqajaz:not(#\#):not(#\#):not(#\#){align-items:end}
|
|
684
686
|
.xuk3077:not(#\#):not(#\#):not(#\#){align-items:flex-end}
|
|
685
687
|
.x1cy8zhl:not(#\#):not(#\#):not(#\#){align-items:flex-start}
|
|
688
|
+
.x1h91t0o:not(#\#):not(#\#):not(#\#){align-items:inherit}
|
|
686
689
|
.xaz6w31:not(#\#):not(#\#):not(#\#){align-items:last baseline}
|
|
687
690
|
.x7a106z:not(#\#):not(#\#):not(#\#){align-items:start}
|
|
688
691
|
.x1qjc9v5:not(#\#):not(#\#):not(#\#){align-items:stretch}
|
|
@@ -1141,7 +1144,6 @@
|
|
|
1141
1144
|
.xpdk1p0:not(#\#):not(#\#):not(#\#){margin-inline-start:calc(calc(var(--spacing-4) + max(var(--astryx-layer-inset-inline-start, 0px), var(--astryx-layer-inset-inline-end, 0px))));margin-inline-start:calc(calc(max(var(--spacing-4), env(safe-area-inset-left, 0px), env(safe-area-inset-right, 0px)) + max(var(--astryx-layer-inset-inline-start, 0px), var(--astryx-layer-inset-inline-end, 0px))))}
|
|
1142
1145
|
.xch736x:not(#\#):not(#\#):not(#\#){margin-inline-start:max(var(--container-padding-inline-start,0px) - var(--_item-inset-inline),0px)}
|
|
1143
1146
|
.x13hpdyo:not(#\#):not(#\#):not(#\#){margin-inline-start:var(--_avatar-group-overlap)}
|
|
1144
|
-
.x12vn87o:not(#\#):not(#\#):not(#\#){margin-inline-start:var(--_tree-indent,0px)}
|
|
1145
1147
|
.xladpz9:not(#\#):not(#\#):not(#\#){margin-inline-start:var(--spacing-0-5)}
|
|
1146
1148
|
.x17lnmll:not(#\#):not(#\#):not(#\#){margin-inline-start:var(--spacing-1-5)}
|
|
1147
1149
|
.x11g1kdw:not(#\#):not(#\#):not(#\#){margin-inline-start:var(--spacing-1)}
|
|
@@ -1228,6 +1230,7 @@
|
|
|
1228
1230
|
.xxjme2g:not(#\#):not(#\#):not(#\#){padding-inline-start:max(var(--container-padding-inline-start,0px),calc(100cqi - var(--layout-alignment-width) + var(--container-padding-inline-start,0px)))}
|
|
1229
1231
|
.xua885n:not(#\#):not(#\#):not(#\#){padding-inline-start:max(var(--container-padding-inline-start,0px),var(--_item-inset-inline))}
|
|
1230
1232
|
.xf4btlh:not(#\#):not(#\#):not(#\#){padding-inline-start:max(var(--spacing-4),env(safe-area-inset-left,0px))}
|
|
1233
|
+
.xw2xrtw:not(#\#):not(#\#):not(#\#){padding-inline-start:var(--_tree-indent,0px)}
|
|
1231
1234
|
.xqmdmw:not(#\#):not(#\#):not(#\#){padding-inline-start:var(--astryx-dialog-padding-inline-start,var(--astryx-dialog-padding-inline,var(--astryx-dialog-padding,max(var(--spacing-4),env(safe-area-inset-left,0px)))))}
|
|
1232
1235
|
.x1jni19p:not(#\#):not(#\#):not(#\#){padding-inline-start:var(--astryx-number-input-padding-inline-start,var(--astryx-number-input-padding-inline,var(--astryx-number-input-padding,var(--spacing-2))))}
|
|
1233
1236
|
.x1c35znw:not(#\#):not(#\#):not(#\#){padding-inline-start:var(--container-padding-inline-start)}
|
|
@@ -1355,6 +1358,8 @@
|
|
|
1355
1358
|
.xsqj5wx:not(#\#):not(#\#):not(#\#){transform:var(--x-transform)}
|
|
1356
1359
|
.xd00j3c:not(#\#):not(#\#):not(#\#){transition-behavior:allow-discrete}
|
|
1357
1360
|
.x5h36tt:not(#\#):not(#\#):not(#\#){transition-delay:.15s}
|
|
1361
|
+
.xf1rc6:not(#\#):not(#\#):not(#\#){transition-delay:0s,0s}
|
|
1362
|
+
.xostmdb:not(#\#):not(#\#):not(#\#){transition-delay:0s,var(--duration-medium)}
|
|
1358
1363
|
.x1ahifba:not(#\#):not(#\#):not(#\#){transition-delay:0s}
|
|
1359
1364
|
.x4avkj2:not(#\#):not(#\#):not(#\#){transition-delay:var(--_fade-delay,0s)}
|
|
1360
1365
|
.x1rq8cwj:not(#\#):not(#\#):not(#\#){transition-delay:var(--_reveal-delay,0s,0s)}
|
|
@@ -1370,6 +1375,7 @@
|
|
|
1370
1375
|
.x1tw3moq:not(#\#):not(#\#):not(#\#){transition-duration:var(--duration-fast),0s}
|
|
1371
1376
|
.xuedmi6:not(#\#):not(#\#):not(#\#){transition-duration:var(--duration-fast)}
|
|
1372
1377
|
.xgneliz:not(#\#):not(#\#):not(#\#){transition-duration:var(--duration-medium-min)}
|
|
1378
|
+
.x1eizr8u:not(#\#):not(#\#):not(#\#){transition-duration:var(--duration-medium),0s}
|
|
1373
1379
|
.x80gvsz:not(#\#):not(#\#):not(#\#){transition-duration:var(--duration-medium)}
|
|
1374
1380
|
.x1eaenvl:not(#\#):not(#\#):not(#\#){transition-property:background-color,border-color,color}
|
|
1375
1381
|
.xts7igz:not(#\#):not(#\#):not(#\#){transition-property:background-color,border-color}
|
|
@@ -1392,6 +1398,7 @@
|
|
|
1392
1398
|
.x1mpt4pi:not(#\#):not(#\#):not(#\#){transition-property:color,text-decoration}
|
|
1393
1399
|
.xt3l3uh:not(#\#):not(#\#):not(#\#){transition-property:color}
|
|
1394
1400
|
.xrgi2yo:not(#\#):not(#\#):not(#\#){transition-property:display}
|
|
1401
|
+
.x6kj92:not(#\#):not(#\#):not(#\#){transition-property:grid-template-rows,overflow}
|
|
1395
1402
|
.xjshnde:not(#\#):not(#\#):not(#\#){transition-property:grid-template-rows,padding}
|
|
1396
1403
|
.x1qn9uv2:not(#\#):not(#\#):not(#\#){transition-property:grid-template-rows}
|
|
1397
1404
|
.x157bb6t:not(#\#):not(#\#):not(#\#){transition-property:mask-image}
|
|
@@ -1446,6 +1453,7 @@
|
|
|
1446
1453
|
.xzkaem6:not(#\#):not(#\#):not(#\#){z-index:3}
|
|
1447
1454
|
.x6cuj84:not(#\#):not(#\#):not(#\#){z-index:500}
|
|
1448
1455
|
.x1q2oy4v:not(#\#):not(#\#):not(#\#){z-index:9999}
|
|
1456
|
+
.x6tapwc:not(#\#):not(#\#):not(#\#){z-index:var(--_tree-focus-z,auto)}
|
|
1449
1457
|
.x1wphrdb[data-astryx-press="fading"]:not(#\#):not(#\#):not(#\#){--_press-overlay-transition:0s}
|
|
1450
1458
|
.xbb2ue6[data-astryx-press="on"]:not(#\#):not(#\#):not(#\#){--_press-overlay-transition:0s}
|
|
1451
1459
|
.x11z29ks[data-astryx-press="on"]:not(#\#):not(#\#):not(#\#){--astryx-press-alpha:1}
|
|
@@ -1541,6 +1549,7 @@
|
|
|
1541
1549
|
.xxdn8bs:has(> div > .astryx-layout-content):not(#\#):not(#\#):not(#\#){padding-inline-end:max(0px,calc((100% - var(--layout-content-width)) / 2))}
|
|
1542
1550
|
.xmnmr5y:has(.astryx-nav-icon):not(#\#):not(#\#):not(#\#){padding-inline-start:0}
|
|
1543
1551
|
.x1vvd0s7:has(> div > .astryx-layout-content):not(#\#):not(#\#):not(#\#){padding-inline-start:max(0px,calc((100% - var(--layout-content-width)) / 2))}
|
|
1552
|
+
.x3x83r7:has(:focus-visible):not(#\#):not(#\#):not(#\#){z-index:2}
|
|
1544
1553
|
.xbw113y:dir(rtl):not(#\#):not(#\#):not(#\#){transform:translateX(-100%)}
|
|
1545
1554
|
.x1eqdj8u:dir(rtl):not(#\#):not(#\#):not(#\#){transform:translateX(100%)}
|
|
1546
1555
|
.x1xrp5p4:first-child:not(#\#):not(#\#):not(#\#){border-end-start-radius:var(--radius-element)}
|
|
@@ -1602,6 +1611,7 @@
|
|
|
1602
1611
|
@media (pointer: coarse){.x1bjav06.x1bjav06:not(#\#):not(#\#):not(#\#){pointer-events:none}}
|
|
1603
1612
|
@media (prefers-reduced-motion: reduce){.xg83hb1.xg83hb1:not(#\#):not(#\#):not(#\#){scroll-behavior:auto}}
|
|
1604
1613
|
@media (forced-colors: active){.xrih18v.xrih18v:not(#\#):not(#\#):not(#\#){scrollbar-color:auto}}
|
|
1614
|
+
@media (prefers-reduced-motion: reduce){.x1k6wr6r.x1k6wr6r:not(#\#):not(#\#):not(#\#){transition-delay:0s,0s}}
|
|
1605
1615
|
@media (prefers-reduced-motion: reduce){.xwgk2z2.xwgk2z2:not(#\#):not(#\#):not(#\#){transition-delay:var(--_reveal-delay-reduced,0s,0s)}}
|
|
1606
1616
|
@media (prefers-reduced-motion: reduce){.xsagj69.xsagj69:not(#\#):not(#\#):not(#\#){transition-duration:.01ms}}
|
|
1607
1617
|
@media (prefers-reduced-motion: reduce){.xzg1mie.xzg1mie:not(#\#):not(#\#):not(#\#){transition-duration:.01s}}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astryxdesign/core",
|
|
3
|
-
"version": "0.7.2-canary.
|
|
3
|
+
"version": "0.7.2-canary.fdd384f",
|
|
4
4
|
"displayName": "Astryx Core",
|
|
5
5
|
"description": "The component library. Accessible, themeable React components with built-in spacing, dark mode, and StyleX styling.",
|
|
6
6
|
"author": "Meta Open Source",
|
|
@@ -792,8 +792,8 @@
|
|
|
792
792
|
"react-dom": ">=19.0.0"
|
|
793
793
|
},
|
|
794
794
|
"devDependencies": {
|
|
795
|
-
"@astryxdesign/a11y-spec": "0.7.2-canary.
|
|
796
|
-
"@astryxdesign/cli": "0.7.2-canary.
|
|
795
|
+
"@astryxdesign/a11y-spec": "0.7.2-canary.fdd384f",
|
|
796
|
+
"@astryxdesign/cli": "0.7.2-canary.fdd384f",
|
|
797
797
|
"@babel/cli": "^7.29.7",
|
|
798
798
|
"@babel/core": "^7.29.7",
|
|
799
799
|
"@babel/preset-react": "^7.29.7",
|
|
@@ -43,6 +43,7 @@ import {LayoutHeader} from '../Layout/LayoutHeader';
|
|
|
43
43
|
import {LayoutPanel} from '../Layout/LayoutPanel';
|
|
44
44
|
import {LayoutContent} from '../Layout/LayoutContent';
|
|
45
45
|
import {MobileNavToggle} from '../MobileNav/MobileNavToggle';
|
|
46
|
+
import {MAX_CLOSE_DELAY_MS as MOBILE_NAV_MAX_CLOSE_DELAY_MS} from '../MobileNav/MobileNav';
|
|
46
47
|
import {SideNavRenderContext} from '../SideNav/SideNavRenderContext';
|
|
47
48
|
import {TopNavRenderContext} from '../TopNav/TopNavRenderContext';
|
|
48
49
|
import {TopNavMobileContentContext} from '../TopNav/TopNavMobileContentContext';
|
|
@@ -81,6 +82,20 @@ const ActivityWrapper = HasActivity
|
|
|
81
82
|
|
|
82
83
|
const MAIN_CONTENT_ID = 'astryx-app-shell-main';
|
|
83
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Extra time, on top of MobileNav's own worst-case close delay, that
|
|
87
|
+
* AppShell waits before flipping its `Activity` boundary to `'hidden'`.
|
|
88
|
+
*
|
|
89
|
+
* React's `Activity` hides its subtree with `display: none !important`
|
|
90
|
+
* synchronously in the same commit as the mode flip, and re-runs the
|
|
91
|
+
* cleanup of every effect inside it — which would otherwise clear
|
|
92
|
+
* MobileNav's own scheduled `dialog.close()` before it fires, or cut off
|
|
93
|
+
* its slide-out CSS transition before it plays (#5701). Waiting strictly
|
|
94
|
+
* longer than MobileNav's own cap, rather than the same duration, means
|
|
95
|
+
* this never turns into a race decided by browser timer-firing order.
|
|
96
|
+
*/
|
|
97
|
+
const ACTIVITY_HIDE_BUFFER_MS = 50;
|
|
98
|
+
|
|
84
99
|
// =============================================================================
|
|
85
100
|
// Types
|
|
86
101
|
// =============================================================================
|
|
@@ -525,6 +540,34 @@ export function AppShell({
|
|
|
525
540
|
const [uncontrolledMobileOpen, setUncontrolledMobileOpen] = useState(false);
|
|
526
541
|
const isMobileNavOpen = mobileNavConfig?.isOpen ?? uncontrolledMobileOpen;
|
|
527
542
|
|
|
543
|
+
// Holds the mobile nav's `Activity` boundary visible for a beat after a
|
|
544
|
+
// close, so MobileNav's own close effect gets a real chance to run its
|
|
545
|
+
// slide-out transition and schedule its delayed `dialog.close()` before
|
|
546
|
+
// Activity hides the subtree (#5701). Only the CLOSE is deferred: opening
|
|
547
|
+
// reads `isMobileNavOpen` directly below, so a controlled open is visible in
|
|
548
|
+
// the commit that carries it instead of one effect later.
|
|
549
|
+
const [isMobileNavCloseHeld, setIsMobileNavCloseHeld] =
|
|
550
|
+
useState(isMobileNavOpen);
|
|
551
|
+
// Arm the hold during render, from the open state itself, so no Effect has
|
|
552
|
+
// to copy props into state; the Effect below only owns the release timer.
|
|
553
|
+
if (isMobileNavOpen && !isMobileNavCloseHeld) {
|
|
554
|
+
setIsMobileNavCloseHeld(true);
|
|
555
|
+
}
|
|
556
|
+
useEffect(() => {
|
|
557
|
+
if (!HasActivity || isMobileNavOpen) {
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
const delayMs = window.matchMedia('(prefers-reduced-motion: reduce)')
|
|
561
|
+
.matches
|
|
562
|
+
? 0
|
|
563
|
+
: MOBILE_NAV_MAX_CLOSE_DELAY_MS + ACTIVITY_HIDE_BUFFER_MS;
|
|
564
|
+
const timeout = setTimeout(() => {
|
|
565
|
+
setIsMobileNavCloseHeld(false);
|
|
566
|
+
}, delayMs);
|
|
567
|
+
return () => clearTimeout(timeout);
|
|
568
|
+
}, [isMobileNavOpen]);
|
|
569
|
+
const isMobileNavActivityVisible = isMobileNavOpen || isMobileNavCloseHeld;
|
|
570
|
+
|
|
528
571
|
const mobileNavOnOpenChange = mobileNavConfig?.onOpenChange;
|
|
529
572
|
const setMobileNavOpen = useCallback(
|
|
530
573
|
(open: boolean) => {
|
|
@@ -864,7 +907,8 @@ export function AppShell({
|
|
|
864
907
|
!mobileNavDisabled &&
|
|
865
908
|
mobileNavReactNode == null &&
|
|
866
909
|
!mobileNavConfigContent && (
|
|
867
|
-
<ActivityWrapper
|
|
910
|
+
<ActivityWrapper
|
|
911
|
+
mode={isMobileNavActivityVisible ? 'visible' : 'hidden'}>
|
|
868
912
|
{/* SideNav drawer — always mounted so presence detection works.
|
|
869
913
|
Hidden when TopNav owns the drawer (combined mode passes
|
|
870
914
|
sideNav via TopNav's mobile content context instead). */}
|
|
@@ -7,12 +7,13 @@ import {Breadcrumbs} from './Breadcrumbs';
|
|
|
7
7
|
import {BreadcrumbItem} from './BreadcrumbItem';
|
|
8
8
|
import {
|
|
9
9
|
BreadcrumbMenuItem,
|
|
10
|
+
BreadcrumbMenuDivider,
|
|
10
11
|
BreadcrumbMenuGroup,
|
|
11
12
|
BreadcrumbMenuCheckboxItem,
|
|
12
13
|
BreadcrumbMenuRadioGroup,
|
|
13
14
|
BreadcrumbMenuRadioItem,
|
|
14
15
|
} from './index';
|
|
15
|
-
import type {BreadcrumbMenuOption} from './index';
|
|
16
|
+
import type {BreadcrumbMenuDividerProps, BreadcrumbMenuOption} from './index';
|
|
16
17
|
import {LinkProvider} from '../Link/LinkProvider';
|
|
17
18
|
|
|
18
19
|
function CustomLink({
|
|
@@ -771,6 +772,34 @@ describe('BreadcrumbItem menu', () => {
|
|
|
771
772
|
expect(separators).toHaveLength(3);
|
|
772
773
|
});
|
|
773
774
|
|
|
775
|
+
it('renders the separator and props types through the BreadcrumbMenuDivider aliases', async () => {
|
|
776
|
+
const user = userEvent.setup();
|
|
777
|
+
const dividerProps: BreadcrumbMenuDividerProps = {
|
|
778
|
+
'data-testid': 'breadcrumb-menu-divider',
|
|
779
|
+
};
|
|
780
|
+
render(
|
|
781
|
+
<Breadcrumbs>
|
|
782
|
+
<BreadcrumbItem
|
|
783
|
+
menu={
|
|
784
|
+
<>
|
|
785
|
+
<BreadcrumbMenuItem label="Overview" />
|
|
786
|
+
<BreadcrumbMenuDivider {...dividerProps} />
|
|
787
|
+
<BreadcrumbMenuItem label="Settings" />
|
|
788
|
+
</>
|
|
789
|
+
}>
|
|
790
|
+
Project
|
|
791
|
+
</BreadcrumbItem>
|
|
792
|
+
<BreadcrumbItem isCurrent>Current</BreadcrumbItem>
|
|
793
|
+
</Breadcrumbs>,
|
|
794
|
+
);
|
|
795
|
+
await user.click(screen.getByRole('button', {name: 'Project'}));
|
|
796
|
+
|
|
797
|
+
expect(screen.getByTestId('breadcrumb-menu-divider')).toHaveAttribute(
|
|
798
|
+
'role',
|
|
799
|
+
'separator',
|
|
800
|
+
);
|
|
801
|
+
});
|
|
802
|
+
|
|
774
803
|
it('carries the group semantics through the BreadcrumbMenuGroup alias', async () => {
|
|
775
804
|
// The alias must bring the semantics, not just the component: a group
|
|
776
805
|
// that renders but is not named leaves a screen reader with a run of
|
package/src/Card/Card.doc.mjs
CHANGED
|
@@ -83,7 +83,7 @@ export const docs = {
|
|
|
83
83
|
code: `
|
|
84
84
|
import {Card} from '@astryxdesign/core/Card';
|
|
85
85
|
import {Grid} from '@astryxdesign/core/Grid';
|
|
86
|
-
import {VStack} from '@astryxdesign/core/
|
|
86
|
+
import {VStack} from '@astryxdesign/core/VStack';
|
|
87
87
|
import {Heading} from '@astryxdesign/core/Heading';
|
|
88
88
|
import {Text} from '@astryxdesign/core/Text';
|
|
89
89
|
|
|
@@ -8,12 +8,19 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import {createRef} from 'react';
|
|
11
|
-
import {describe, it, expect, vi} from 'vitest';
|
|
12
|
-
import {render, screen, fireEvent} from '@testing-library/react';
|
|
11
|
+
import {describe, it, expect, vi, beforeEach} from 'vitest';
|
|
12
|
+
import {render, screen, fireEvent, act} from '@testing-library/react';
|
|
13
|
+
import * as stylex from '@stylexjs/stylex';
|
|
13
14
|
import {CommandPaletteInput} from './CommandPaletteInput';
|
|
14
15
|
import {CommandPaletteContext} from './CommandPaletteContext';
|
|
15
16
|
import {Dialog} from '../Dialog';
|
|
16
17
|
import type {CommandPaletteContextValue} from './CommandPaletteContext';
|
|
18
|
+
import {focusOutlineStyles} from '../utils/focusOutline.stylex';
|
|
19
|
+
import {__resetInteractionModalityForTest} from '../utils/interactionModality';
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
__resetInteractionModalityForTest();
|
|
23
|
+
});
|
|
17
24
|
|
|
18
25
|
function makeContext(
|
|
19
26
|
overrides: Partial<CommandPaletteContextValue> = {},
|
|
@@ -279,6 +286,45 @@ describe('CommandPaletteInput', () => {
|
|
|
279
286
|
},
|
|
280
287
|
);
|
|
281
288
|
|
|
289
|
+
it('draws the shared ring only when keyboard navigation acquired focus', () => {
|
|
290
|
+
render(<CommandPaletteInput hasAutoFocus={false} />);
|
|
291
|
+
const input = screen.getByRole('searchbox');
|
|
292
|
+
const ringClasses = stylex
|
|
293
|
+
.props(focusOutlineStyles.focusVisible)
|
|
294
|
+
.className!.split(' ');
|
|
295
|
+
const isRinged = () =>
|
|
296
|
+
ringClasses.every(className => input.classList.contains(className));
|
|
297
|
+
|
|
298
|
+
fireEvent.pointerDown(input);
|
|
299
|
+
fireEvent.focus(input);
|
|
300
|
+
expect(isRinged()).toBe(false);
|
|
301
|
+
|
|
302
|
+
fireEvent.blur(input);
|
|
303
|
+
fireEvent.keyDown(input, {key: 'Tab'});
|
|
304
|
+
fireEvent.focus(input);
|
|
305
|
+
expect(isRinged()).toBe(true);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('preserves caller focus and blur handlers while tracking modality', () => {
|
|
309
|
+
const onFocus = vi.fn();
|
|
310
|
+
const onBlur = vi.fn();
|
|
311
|
+
render(
|
|
312
|
+
<CommandPaletteInput
|
|
313
|
+
hasAutoFocus={false}
|
|
314
|
+
onFocus={onFocus}
|
|
315
|
+
onBlur={onBlur}
|
|
316
|
+
/>,
|
|
317
|
+
);
|
|
318
|
+
const input = screen.getByRole('searchbox');
|
|
319
|
+
|
|
320
|
+
fireEvent.keyDown(input, {key: 'Tab'});
|
|
321
|
+
fireEvent.focus(input);
|
|
322
|
+
fireEvent.blur(input);
|
|
323
|
+
|
|
324
|
+
expect(onFocus).toHaveBeenCalledTimes(1);
|
|
325
|
+
expect(onBlur).toHaveBeenCalledTimes(1);
|
|
326
|
+
});
|
|
327
|
+
|
|
282
328
|
it('does not schedule focus when hasAutoFocus is false', async () => {
|
|
283
329
|
const focusSpy = vi.spyOn(HTMLElement.prototype, 'focus');
|
|
284
330
|
render(<CommandPaletteInput hasAutoFocus={false} />);
|
|
@@ -481,8 +527,11 @@ describe('CommandPaletteInput popup context', () => {
|
|
|
481
527
|
</CommandPaletteContext>,
|
|
482
528
|
);
|
|
483
529
|
|
|
484
|
-
await
|
|
485
|
-
|
|
530
|
+
await act(async () => {
|
|
531
|
+
await new Promise<void>(resolve =>
|
|
532
|
+
requestAnimationFrame(() => resolve()),
|
|
533
|
+
);
|
|
486
534
|
});
|
|
535
|
+
expect(screen.getByRole('combobox')).toHaveFocus();
|
|
487
536
|
});
|
|
488
537
|
});
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* - /packages/cli/assets/templates/blocks/components/CommandPalette/ (showcase blocks)
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import {useCallback, useEffect, useRef, type ReactNode} from 'react';
|
|
16
|
+
import {useCallback, useEffect, useRef, useState, type ReactNode} from 'react';
|
|
17
17
|
import * as stylex from '@stylexjs/stylex';
|
|
18
18
|
import {Icon} from '../Icon';
|
|
19
19
|
import {Spinner} from '../Spinner';
|
|
@@ -28,6 +28,11 @@ import {useCommandPaletteContext} from './CommandPaletteContext';
|
|
|
28
28
|
import {useDialogContext} from '../Dialog/DialogContext';
|
|
29
29
|
import type {BaseProps} from '../BaseProps';
|
|
30
30
|
import {themeProps} from '../utils/themeProps';
|
|
31
|
+
import {focusOutlineProps} from '../utils/focusOutline.stylex';
|
|
32
|
+
import {
|
|
33
|
+
getInteractionModality,
|
|
34
|
+
useInteractionModalityTracking,
|
|
35
|
+
} from '../utils/interactionModality';
|
|
31
36
|
import {useTranslator} from '../i18n';
|
|
32
37
|
|
|
33
38
|
import {useMergedRefs} from '../hooks/useMergedRefs';
|
|
@@ -71,7 +76,6 @@ const styles = stylex.create({
|
|
|
71
76
|
minWidth: 0,
|
|
72
77
|
borderWidth: 0,
|
|
73
78
|
borderStyle: 'none',
|
|
74
|
-
outline: 'none',
|
|
75
79
|
backgroundColor: 'transparent',
|
|
76
80
|
color: colorVars['--color-text-primary'],
|
|
77
81
|
fontFamily: typographyVars['--font-family-body'],
|
|
@@ -169,6 +173,8 @@ export function CommandPaletteInput({
|
|
|
169
173
|
endContent,
|
|
170
174
|
onChange,
|
|
171
175
|
onKeyDown,
|
|
176
|
+
onFocus,
|
|
177
|
+
onBlur,
|
|
172
178
|
ref,
|
|
173
179
|
xstyle,
|
|
174
180
|
className,
|
|
@@ -184,6 +190,26 @@ export function CommandPaletteInput({
|
|
|
184
190
|
const hasListbox = ctx ? ctx.selectableItems.length > 0 : true;
|
|
185
191
|
const dialogContext = useDialogContext();
|
|
186
192
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
193
|
+
const [isKeyboardFocus, setIsKeyboardFocus] = useState(false);
|
|
194
|
+
|
|
195
|
+
// Text-entry controls match :focus-visible after pointer focus in Chromium.
|
|
196
|
+
// Keep the shared selector, but narrow its ring to focus acquired through
|
|
197
|
+
// keyboard navigation so a click or pointer-opened palette stays unringed.
|
|
198
|
+
useInteractionModalityTracking();
|
|
199
|
+
const handleFocus = useCallback(
|
|
200
|
+
(event: React.FocusEvent<HTMLInputElement>) => {
|
|
201
|
+
setIsKeyboardFocus(getInteractionModality() === 'keyboard');
|
|
202
|
+
onFocus?.(event);
|
|
203
|
+
},
|
|
204
|
+
[onFocus],
|
|
205
|
+
);
|
|
206
|
+
const handleBlur = useCallback(
|
|
207
|
+
(event: React.FocusEvent<HTMLInputElement>) => {
|
|
208
|
+
setIsKeyboardFocus(false);
|
|
209
|
+
onBlur?.(event);
|
|
210
|
+
},
|
|
211
|
+
[onBlur],
|
|
212
|
+
);
|
|
187
213
|
|
|
188
214
|
// Use context values as fallback
|
|
189
215
|
const value = controlledValue ?? ctx?.search;
|
|
@@ -259,7 +285,9 @@ export function CommandPaletteInput({
|
|
|
259
285
|
<input
|
|
260
286
|
{...props}
|
|
261
287
|
ref={useMergedRefs(ref, inputRef)}
|
|
262
|
-
{...
|
|
288
|
+
{...(isKeyboardFocus
|
|
289
|
+
? focusOutlineProps.focusVisible(styles.input)
|
|
290
|
+
: focusOutlineProps.suppressed(styles.input))}
|
|
263
291
|
type="text"
|
|
264
292
|
role={ctx ? 'combobox' : standaloneRole}
|
|
265
293
|
aria-expanded={
|
|
@@ -300,6 +328,8 @@ export function CommandPaletteInput({
|
|
|
300
328
|
onChange?.(e);
|
|
301
329
|
}}
|
|
302
330
|
onKeyDown={handleKeyDown}
|
|
331
|
+
onFocus={handleFocus}
|
|
332
|
+
onBlur={handleBlur}
|
|
303
333
|
/>
|
|
304
334
|
{(ctx?.isBusy || hasEndContent) && (
|
|
305
335
|
<span {...stylex.props(styles.end)}>
|