@astryxdesign/core 0.7.6-canary.631ba02 → 0.7.6-canary.962dffe

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.
Files changed (56) hide show
  1. package/dist/AppShell/AppShell.d.ts.map +1 -1
  2. package/dist/AppShell/AppShell.js +183 -5
  3. package/dist/AppShell/observeMainOverflow.d.ts +9 -0
  4. package/dist/AppShell/observeMainOverflow.d.ts.map +1 -0
  5. package/dist/AppShell/observeMainOverflow.js +125 -0
  6. package/dist/Carousel/Carousel.d.ts +3 -1
  7. package/dist/Carousel/Carousel.d.ts.map +1 -1
  8. package/dist/Carousel/Carousel.js +12 -3
  9. package/dist/Popover/Popover.d.ts.map +1 -1
  10. package/dist/Popover/Popover.js +32 -12
  11. package/dist/Stepper/Step.d.ts.map +1 -1
  12. package/dist/Stepper/Step.js +26 -6
  13. package/dist/astryx.css +1 -0
  14. package/dist/hooks/scrollKeyboardDelegation.d.ts.map +1 -1
  15. package/dist/hooks/scrollKeyboardDelegation.js +7 -3
  16. package/dist/hooks/useScrollOverflow.d.ts +2 -1
  17. package/dist/hooks/useScrollOverflow.d.ts.map +1 -1
  18. package/dist/hooks/useScrollOverflow.js +56 -10
  19. package/dist/i18n/generated-locales/en.generated.d.ts.map +1 -1
  20. package/dist/i18n/generated-locales/en.generated.js +1 -0
  21. package/dist/i18n/generated-locales/pseudo.generated.d.ts.map +1 -1
  22. package/dist/i18n/generated-locales/pseudo.generated.js +1 -0
  23. package/dist/utils/sharedMutationObserver.d.ts +19 -11
  24. package/dist/utils/sharedMutationObserver.d.ts.map +1 -1
  25. package/dist/utils/sharedMutationObserver.js +66 -26
  26. package/locales/en.json +4 -0
  27. package/locales/pseudo.json +3 -0
  28. package/package.json +4 -4
  29. package/src/AppShell/AppShell.doc.mjs +1 -1
  30. package/src/AppShell/AppShell.spec.md +9 -12
  31. package/src/AppShell/AppShell.test.tsx +164 -2
  32. package/src/AppShell/AppShell.tsx +225 -5
  33. package/src/AppShell/__tests__/AppShellMainKeyboard.a11y.browser.spec.ts +534 -0
  34. package/src/AppShell/observeMainOverflow.test.ts +173 -0
  35. package/src/AppShell/observeMainOverflow.ts +143 -0
  36. package/src/Carousel/Carousel.doc.mjs +1 -1
  37. package/src/Carousel/Carousel.spec.md +9 -9
  38. package/src/Carousel/Carousel.test.tsx +124 -3
  39. package/src/Carousel/Carousel.tsx +10 -2
  40. package/src/Carousel/__tests__/Carousel.a11y.known-failures.ts +1 -16
  41. package/src/Popover/Popover.test.tsx +40 -0
  42. package/src/Popover/Popover.tsx +34 -14
  43. package/src/Popover/__tests__/PopoverSecondDocument.a11y.chromium.spec.ts +70 -8
  44. package/src/Stepper/Step.spec.md +36 -42
  45. package/src/Stepper/Step.tsx +33 -6
  46. package/src/Stepper/Stepper.doc.mjs +1 -1
  47. package/src/Stepper/Stepper.test.tsx +69 -7
  48. package/src/hooks/scrollKeyboardDelegation.test.ts +33 -0
  49. package/src/hooks/scrollKeyboardDelegation.ts +9 -3
  50. package/src/hooks/useScrollOverflow.doc.mjs +1 -1
  51. package/src/hooks/useScrollOverflow.test.tsx +102 -1
  52. package/src/hooks/useScrollOverflow.ts +59 -10
  53. package/src/i18n/generated-locales/en.generated.ts +1 -0
  54. package/src/i18n/generated-locales/pseudo.generated.ts +1 -0
  55. package/src/utils/sharedMutationObserver.test.ts +54 -1
  56. package/src/utils/sharedMutationObserver.ts +87 -29
@@ -1 +1 @@
1
- {"version":3,"file":"AppShell.d.ts","sourceRoot":"","sources":["../../src/AppShell/AppShell.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAQL,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAgCf,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,gBAAgB,CAAC;AAChD,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAQ5C,OAAO,EAEL,KAAK,mBAAmB,EACzB,MAAM,2BAA2B,CAAC;AACnC,OAAO,KAAK,EAAC,kBAAkB,EAAC,MAAM,SAAS,CAAC;AAsChD;;;GAGG;AACH,MAAM,MAAM,kBAAkB,GAAG,mBAAmB,GAAG,MAAM,CAAC;AAE9D;;;;;;;GAOG;AAEH;;GAEG;AACH,MAAM,MAAM,eAAe,GAAG,MAAM,kBAAkB,CAAC;AAEvD;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B;;;;OAIG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IAEjB;;OAEG;IACH,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IAEzC;;;;OAIG;IACH,OAAO,CAAC,EAAE,SAAS,CAAC;IAEpB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,kBAAkB,CAAC;IAEhC;;;;;;;;;;OAUG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,MAAM,WAAW,aAAc,SAAQ,SAAS,CAAC,cAAc,CAAC;IAC9D,wCAAwC;IACxC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAChC;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,eAAe,CAAC;IAE1B;;;OAGG;IACH,MAAM,CAAC,EAAE,SAAS,CAAC;IAEnB;;OAEG;IACH,QAAQ,EAAE,SAAS,CAAC;IAEpB;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,WAAW,CAAC;IAE7B;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAEzB;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,SAAS,CAAC,EAAE,KAAK,GAAG,eAAe,GAAG,SAAS,CAAC;IAEhD;;;;;;;;;;;;;;;;;;;;;;;;;OAyBG;IACH,OAAO,CAAC,EAAE,SAAS,CAAC;IAEpB;;;OAGG;IACH,MAAM,CAAC,EAAE,SAAS,CAAC;CACpB;AAqND,wBAAgB,QAAQ,CAAC,EACvB,OAAoB,EACpB,MAAM,EACN,QAAQ,EACR,cAAc,EACd,aAAa,EAAE,UAAU,EACzB,MAAe,EACf,SAAS,EACT,OAAO,EACP,MAAM,EACN,MAAM,EACN,SAAS,EACT,KAAK,EACL,GAAG,EACH,GAAG,IAAI,EACR,EAAE,aAAa,qBAykBf;yBAxlBe,QAAQ"}
1
+ {"version":3,"file":"AppShell.d.ts","sourceRoot":"","sources":["../../src/AppShell/AppShell.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAQL,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAiCf,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,gBAAgB,CAAC;AAChD,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAW5C,OAAO,EAEL,KAAK,mBAAmB,EACzB,MAAM,2BAA2B,CAAC;AACnC,OAAO,KAAK,EAAC,kBAAkB,EAAC,MAAM,SAAS,CAAC;AAwChD;;;GAGG;AACH,MAAM,MAAM,kBAAkB,GAAG,mBAAmB,GAAG,MAAM,CAAC;AAE9D;;;;;;;GAOG;AAEH;;GAEG;AACH,MAAM,MAAM,eAAe,GAAG,MAAM,kBAAkB,CAAC;AAEvD;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B;;;;OAIG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IAEjB;;OAEG;IACH,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IAEzC;;;;OAIG;IACH,OAAO,CAAC,EAAE,SAAS,CAAC;IAEpB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,kBAAkB,CAAC;IAEhC;;;;;;;;;;OAUG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,MAAM,WAAW,aAAc,SAAQ,SAAS,CAAC,cAAc,CAAC;IAC9D,wCAAwC;IACxC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAChC;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,eAAe,CAAC;IAE1B;;;OAGG;IACH,MAAM,CAAC,EAAE,SAAS,CAAC;IAEnB;;OAEG;IACH,QAAQ,EAAE,SAAS,CAAC;IAEpB;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,WAAW,CAAC;IAE7B;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAEzB;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,SAAS,CAAC,EAAE,KAAK,GAAG,eAAe,GAAG,SAAS,CAAC;IAEhD;;;;;;;;;;;;;;;;;;;;;;;;;OAyBG;IACH,OAAO,CAAC,EAAE,SAAS,CAAC;IAEpB;;;OAGG;IACH,MAAM,CAAC,EAAE,SAAS,CAAC;CACpB;AAuOD,wBAAgB,QAAQ,CAAC,EACvB,OAAoB,EACpB,MAAM,EACN,QAAQ,EACR,cAAc,EACd,aAAa,EAAE,UAAU,EACzB,MAAe,EACf,SAAS,EACT,OAAO,EACP,MAAM,EACN,MAAM,EACN,SAAS,EACT,KAAK,EACL,GAAG,EACH,GAAG,IAAI,EACR,EAAE,aAAa,qBA6wBf;yBA5xBe,QAAQ"}
@@ -23,7 +23,7 @@ import React from 'react';
23
23
  import { isValidElement, useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
24
24
  import * as stylex from '@stylexjs/stylex';
25
25
  import "../theme/tokens.stylex.js";
26
- import { colorVars, fontWeightVars, radiusVars, spacingVars, typeScaleVars } from "../theme/tokens.stylex.js";
26
+ import { colorVars, focusVars, fontWeightVars, radiusVars, spacingVars, typeScaleVars } from "../theme/tokens.stylex.js";
27
27
  import { Layout } from "../Layout/Layout.js";
28
28
  import { LayoutHeader } from "../Layout/LayoutHeader.js";
29
29
  import { LayoutPanel } from "../Layout/LayoutPanel.js";
@@ -38,7 +38,7 @@ import { TopNavRenderContext } from "../TopNav/TopNavRenderContext.js";
38
38
  import { TopNavMobileContentContext } from "../TopNav/TopNavMobileContentContext.js";
39
39
  import { AppShellMobileContext } from "./AppShellMobileContext.js";
40
40
  import { mergeProps, isRenderable } from "../utils/index.js";
41
- import { focusOutlineProps } from "../utils/focusOutline.stylex.js";
41
+ import { focusOutlineProps, focusOutlineStyles } from "../utils/focusOutline.stylex.js";
42
42
  import { useMediaQuery } from "../hooks/useMediaQuery.js";
43
43
  import { observeResize } from "../utils/sharedResizeObserver.js";
44
44
  import { themeProps } from "../utils/themeProps.js";
@@ -46,6 +46,8 @@ import { useTranslator } from "../i18n/index.js";
46
46
  import { useThemeDefinition } from "../theme/useTheme.js";
47
47
  import { DEFAULT_WIDTH_BREAKPOINTS } from "../theme/themeAdaptations.js";
48
48
  import { useMergedRefs } from "../hooks/useMergedRefs.js";
49
+ import { attachScrollKeyboardDelegation } from "../hooks/scrollKeyboardDelegation.js";
50
+ import { observeMainOverflow } from "./observeMainOverflow.js";
49
51
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
50
52
  const HasActivity = typeof React.Activity !== 'undefined';
51
53
  const ActivityWrapper = HasActivity ? ({
@@ -168,6 +170,14 @@ const styles = {
168
170
  kI3sdo: "x1uvtmcs",
169
171
  $$css: true
170
172
  },
173
+ mainElevatedCorner: {
174
+ krdFHd: "x9a4xmf",
175
+ $$css: true
176
+ },
177
+ mainScrollRegionInset: {
178
+ kInvED: "x1wfwxd8 x1nw7mhf",
179
+ $$css: true
180
+ },
171
181
  contentBgSurface: {
172
182
  kWkggS: "x10xzikg",
173
183
  $$css: true
@@ -332,9 +342,87 @@ export function AppShell({
332
342
  const handleSkipLinkClick = useCallback(() => {
333
343
  document.getElementById(MAIN_CONTENT_ID)?.focus();
334
344
  }, []);
345
+
346
+ // Main draws its focus ring only for focus a Tab press moved onto it, and
347
+ // keeps it for as long as that focus stays, even once the content fits.
348
+ // Programmatic focus (the skip link, a script's focus() on a route change)
349
+ // draws none (FR4), even after keyboard input, where :focus-visible alone
350
+ // would match. See the effect below for how a Tab move is told apart.
351
+ const [isTabFocus, setIsTabFocus] = useState(false);
352
+ // A window or tab switch blurs main without moving focus off it: main stays
353
+ // the document's active element, and focus comes back to it with no Tab
354
+ // press. The ring holds across that round trip; any real focus move ends it.
355
+ const isMainAwayRef = useRef(false);
356
+ const handleMainBlur = useCallback(event => {
357
+ if (event.target !== event.currentTarget) {
358
+ return;
359
+ }
360
+ if (document.activeElement === event.currentTarget) {
361
+ isMainAwayRef.current = true;
362
+ return;
363
+ }
364
+ isMainAwayRef.current = false;
365
+ setIsTabFocus(false);
366
+ }, []);
335
367
  const isFill = height === 'fill';
336
368
  const isAuto = height === 'auto';
337
369
 
370
+ // With `fill`, main is the page's scroller. While its content overflows it
371
+ // must be reachable from the keyboard even when nothing inside it is
372
+ // focusable (AST-025 FR12): it joins the tab order, and Tab entry moves on
373
+ // to its first link or button when it has one. Only `fill` observes main
374
+ // (PR1); LayoutContent keeps owning main's overflow and styles.
375
+ const [isMainOverflowing, setIsMainOverflowing] = useState(false);
376
+ const isMainScrollRegion = isFill && isMainOverflowing;
377
+ // Main's node is owned through a ref, never React state. Under `fill` its
378
+ // overflow is observed while the node is attached, and while main is a
379
+ // scroll region the keyboard delegation is attached to it. The ref callback
380
+ // changes with `fill`, so switching modes detaches and re-attaches main in
381
+ // the same commit; a detached or replaced node releases both before the new
382
+ // node takes over.
383
+ const mainNodeRef = useRef(null);
384
+ const mainOverflowRef = useRef(null);
385
+ const mainDelegationRef = useRef(null);
386
+ const isMainScrollRegionRef = useRef(isMainScrollRegion);
387
+ const syncMainDelegation = useCallback(() => {
388
+ const node = isMainScrollRegionRef.current ? mainNodeRef.current : null;
389
+ const current = mainDelegationRef.current;
390
+ if (current?.node === node) {
391
+ return;
392
+ }
393
+ current?.release();
394
+ mainDelegationRef.current = node == null ? null : {
395
+ node,
396
+ release: attachScrollKeyboardDelegation(node, node)
397
+ };
398
+ }, []);
399
+ const setMainNode = useCallback(node => {
400
+ mainNodeRef.current = node;
401
+ const observed = isFill ? node : null;
402
+ const current = mainOverflowRef.current;
403
+ if (current?.node !== observed) {
404
+ current?.stop();
405
+ mainOverflowRef.current = observed == null ? null : {
406
+ node: observed,
407
+ stop: observeMainOverflow(observed, setIsMainOverflowing)
408
+ };
409
+ if (observed == null) {
410
+ setIsMainOverflowing(false);
411
+ }
412
+ }
413
+ syncMainDelegation();
414
+ }, [isFill, syncMainDelegation]);
415
+ useEffect(() => {
416
+ isMainScrollRegionRef.current = isMainScrollRegion;
417
+ syncMainDelegation();
418
+ }, [isMainScrollRegion, syncMainDelegation]);
419
+ useEffect(() => () => {
420
+ mainOverflowRef.current?.stop();
421
+ mainOverflowRef.current = null;
422
+ mainDelegationRef.current?.release();
423
+ mainDelegationRef.current = null;
424
+ }, []);
425
+
338
426
  // Nav style derived values
339
427
  const hasBanner = isRenderable(banner);
340
428
  const hasTopNav = isRenderable(topNav);
@@ -459,6 +547,9 @@ export function AppShell({
459
547
  // (`component:SideNav` FR6).
460
548
  const railRef = useRef(null);
461
549
  const mainRef = useRef(null);
550
+ // Main's node goes to the glide above and to the overflow observation
551
+ // that, under `fill`, makes main a keyboard stop.
552
+ const mainRefs = useMergedRefs(mainRef, setMainNode);
462
553
  const backdropRef = useRef(null);
463
554
  const contentLayout = useAnimatedLayout({
464
555
  containerRef: shellRef
@@ -617,17 +708,104 @@ export function AppShell({
617
708
  // Build main content
618
709
  // =========================================================================
619
710
  const shouldElevateWithCorner = isElevated && hasTopNav && showSideNavInline;
711
+ // While main is a keyboard stop, a Tab keydown anywhere in the document
712
+ // (capture, so Shift+Tab from the page start counts too) marks the next
713
+ // focus move, and so does the window's focus event for keyboard focus that
714
+ // enters the document from outside (Tab or Shift+Tab from the page
715
+ // embedding this one), where the browser's own `:focus-visible` then tells
716
+ // a key from a pointer. A mark counts only once its event's dispatch has
717
+ // finished uncancelled (`eventPhase === 0`): the browser's own focus move
718
+ // follows that dispatch in the same task, while a focus() a page's handler
719
+ // calls during it is programmatic and draws no ring (FR4). The marks expire
720
+ // on a zero-delay timer, so a cancelled Tab, a Tab out of the document or a
721
+ // window focus with nothing focused leaves nothing for a later focus().
722
+ useEffect(() => {
723
+ if (!isMainScrollRegion) {
724
+ return;
725
+ }
726
+ let tabKeydown = null;
727
+ let windowFocus = null;
728
+ let expiry;
729
+ const expireMarks = () => {
730
+ clearTimeout(expiry);
731
+ expiry = setTimeout(() => {
732
+ tabKeydown = null;
733
+ windowFocus = null;
734
+ }, 0);
735
+ };
736
+ const onKeyDown = event => {
737
+ tabKeydown = event.key === 'Tab' ? event : null;
738
+ windowFocus = null;
739
+ expireMarks();
740
+ };
741
+ const onPointerDown = () => {
742
+ tabKeydown = null;
743
+ windowFocus = null;
744
+ };
745
+ const onWindowFocus = event => {
746
+ windowFocus = event;
747
+ expireMarks();
748
+ };
749
+ const onFocusIn = event => {
750
+ // Still dispatching: this focus move is a handler's focus(), so it
751
+ // reads no mark and leaves the mark for the browser's own move.
752
+ const tabDone = tabKeydown != null && tabKeydown.eventPhase === 0;
753
+ const focusDone = windowFocus != null && windowFocus.eventPhase === 0;
754
+ const arrivedByTab = tabDone && tabKeydown?.defaultPrevented === false;
755
+ const enteredDocument = focusDone;
756
+ if (tabDone) {
757
+ tabKeydown = null;
758
+ }
759
+ if (focusDone) {
760
+ windowFocus = null;
761
+ }
762
+ const main = mainNodeRef.current;
763
+ const isMain = event.target != null && event.target === main;
764
+ if (isMainAwayRef.current) {
765
+ // Focus comes back after a window or tab switch (main keeps the ring
766
+ // it had), or lands elsewhere (main's ring is over).
767
+ isMainAwayRef.current = false;
768
+ if (isMain && !arrivedByTab) {
769
+ return;
770
+ }
771
+ if (!isMain) {
772
+ setIsTabFocus(false);
773
+ return;
774
+ }
775
+ }
776
+ if (isMain) {
777
+ setIsTabFocus(arrivedByTab || enteredDocument && main.matches(':focus-visible'));
778
+ }
779
+ };
780
+ document.addEventListener('keydown', onKeyDown, true);
781
+ document.addEventListener('pointerdown', onPointerDown, true);
782
+ document.addEventListener('focusin', onFocusIn, true);
783
+ window.addEventListener('focus', onWindowFocus);
784
+ return () => {
785
+ document.removeEventListener('keydown', onKeyDown, true);
786
+ document.removeEventListener('pointerdown', onPointerDown, true);
787
+ document.removeEventListener('focusin', onFocusIn, true);
788
+ window.removeEventListener('focus', onWindowFocus);
789
+ clearTimeout(expiry);
790
+ };
791
+ }, [isMainScrollRegion]);
792
+
793
+ // Tab focus on main draws the ring until main loses focus; programmatic
794
+ // focus never does (FR4).
795
+ const drawsMainFocusRing = isTabFocus;
620
796
  const mainInner = /*#__PURE__*/_jsx(LayoutContent, {
621
- ref: mainRef,
797
+ ref: mainRefs,
622
798
  padding: contentPadding ?? 0,
623
799
  role: "main",
624
800
  id: MAIN_CONTENT_ID
625
801
  // Focusable skip-link target (WCAG 2.4.1) — without tabIndex, several
626
802
  // browsers won't move focus to the div when the skip link is activated.
803
+ // A keyboard stop only while it scrolls overflowing content.
627
804
  ,
628
- tabIndex: -1,
805
+ tabIndex: isMainScrollRegion ? 0 : -1,
629
806
  isScrollable: isFill,
630
- xstyle: [contentAreaStyle, styles.mainFocusTarget],
807
+ onBlur: handleMainBlur,
808
+ xstyle: [contentAreaStyle, drawsMainFocusRing && shouldElevateWithCorner && styles.mainElevatedCorner, drawsMainFocusRing ? [focusOutlineStyles.focusVisible, styles.mainScrollRegionInset] : styles.mainFocusTarget],
631
809
  children: children
632
810
  });
633
811
 
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Calls `onChange` with whether `main`'s content overflows it on either axis:
3
+ * once with the first measurement, then whenever the answer changes. Signals
4
+ * coalesce into one measurement per frame. A hidden, disconnected or
5
+ * zero-size main is unknown rather than fitting: the last answer stands until
6
+ * a later signal measures it.
7
+ */
8
+ export declare function observeMainOverflow(main: HTMLElement, onChange: (overflows: boolean) => void): () => void;
9
+ //# sourceMappingURL=observeMainOverflow.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"observeMainOverflow.d.ts","sourceRoot":"","sources":["../../src/AppShell/observeMainOverflow.ts"],"names":[],"mappings":"AAyCA;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,WAAW,EACjB,QAAQ,EAAE,CAAC,SAAS,EAAE,OAAO,KAAK,IAAI,GACrC,MAAM,IAAI,CA2FZ"}
@@ -0,0 +1,125 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ /**
6
+ * @file observeMainOverflow.ts
7
+ * @input AppShell's main region under `height="fill"`: its own scroll
8
+ * container, holding its content as direct children with no wrapper
9
+ * @output observeMainOverflow(main, onChange): reports whether main's content
10
+ * overflows it on either axis, once at the start and on every change, and
11
+ * returns the function that stops observing
12
+ * @position Internal to AppShell and never exported. AppShell makes main a
13
+ * keyboard stop only while it overflows (`spec:AST-025` FR12,
14
+ * `component:AppShell` FR4 and PR1). Main has no inner content box, so its
15
+ * content grows inside it without resizing it: its direct children's sizes
16
+ * are watched instead.
17
+ *
18
+ * SYNC: When modified, update:
19
+ * - /packages/core/src/AppShell/AppShell.tsx
20
+ * - /packages/core/src/AppShell/observeMainOverflow.test.ts
21
+ */
22
+ import { getLogicalAxisMapping, measureLogicalScrollAxis } from "../hooks/scrollGeometry.js";
23
+ import { observeMutations } from "../utils/sharedMutationObserver.js";
24
+ import { observeResize } from "../utils/sharedResizeObserver.js";
25
+
26
+ /**
27
+ * Main's content: layout attributes, text and children, all the way down.
28
+ * One shared observer serves every registration with these options.
29
+ */
30
+ const CONTENT_MUTATIONS = {
31
+ attributes: true,
32
+ attributeFilter: ['class', 'dir', 'hidden', 'style'],
33
+ characterData: true,
34
+ childList: true,
35
+ subtree: true
36
+ };
37
+
38
+ /**
39
+ * Calls `onChange` with whether `main`'s content overflows it on either axis:
40
+ * once with the first measurement, then whenever the answer changes. Signals
41
+ * coalesce into one measurement per frame. A hidden, disconnected or
42
+ * zero-size main is unknown rather than fitting: the last answer stands until
43
+ * a later signal measures it.
44
+ */
45
+ export function observeMainOverflow(main, onChange) {
46
+ let isActive = true;
47
+ let frame = null;
48
+ let overflows = null;
49
+ const measure = () => {
50
+ frame = null;
51
+ if (!isActive) {
52
+ return;
53
+ }
54
+ const computedStyle = getComputedStyle(main);
55
+ const mapping = getLogicalAxisMapping(computedStyle.writingMode, computedStyle.direction);
56
+ const inline = measureLogicalScrollAxis(main, 'inline', mapping);
57
+ const block = measureLogicalScrollAxis(main, 'block', mapping);
58
+ if (inline == null || block == null) {
59
+ return;
60
+ }
61
+ const next = inline.isScrollable || block.isScrollable;
62
+ if (next !== overflows) {
63
+ overflows = next;
64
+ onChange(next);
65
+ }
66
+ };
67
+ const scheduleMeasure = () => {
68
+ if (isActive && frame == null) {
69
+ frame = window.requestAnimationFrame(measure);
70
+ }
71
+ };
72
+
73
+ // Main keeps its size while its content grows; its children do not. Each
74
+ // direct child's size is watched, following the children as they change.
75
+ const childObservers = new Map();
76
+ const syncChildObservers = () => {
77
+ const children = new Set(Array.from(main.children));
78
+ for (const [child, stop] of childObservers) {
79
+ if (!children.has(child)) {
80
+ stop();
81
+ childObservers.delete(child);
82
+ }
83
+ }
84
+ for (const child of children) {
85
+ if (!childObservers.has(child)) {
86
+ childObservers.set(child, observeResize(child, scheduleMeasure));
87
+ }
88
+ }
89
+ };
90
+ const onContentChange = () => {
91
+ syncChildObservers();
92
+ scheduleMeasure();
93
+ };
94
+ measure();
95
+ syncChildObservers();
96
+ const stopObservingMain = observeResize(main, scheduleMeasure);
97
+ const stopObservingContent = observeMutations(main, CONTENT_MUTATIONS, onContentChange);
98
+ window.addEventListener('resize', scheduleMeasure);
99
+ window.visualViewport?.addEventListener('resize', scheduleMeasure);
100
+ main.addEventListener('load', scheduleMeasure, true);
101
+ main.addEventListener('transitionend', scheduleMeasure);
102
+ main.addEventListener('animationend', scheduleMeasure);
103
+ const fonts = document.fonts;
104
+ fonts?.addEventListener?.('loadingdone', scheduleMeasure);
105
+ void fonts?.ready.then(scheduleMeasure);
106
+ return () => {
107
+ isActive = false;
108
+ if (frame != null) {
109
+ window.cancelAnimationFrame(frame);
110
+ frame = null;
111
+ }
112
+ stopObservingMain();
113
+ stopObservingContent();
114
+ for (const stop of childObservers.values()) {
115
+ stop();
116
+ }
117
+ childObservers.clear();
118
+ window.removeEventListener('resize', scheduleMeasure);
119
+ window.visualViewport?.removeEventListener('resize', scheduleMeasure);
120
+ main.removeEventListener('load', scheduleMeasure, true);
121
+ main.removeEventListener('transitionend', scheduleMeasure);
122
+ main.removeEventListener('animationend', scheduleMeasure);
123
+ fonts?.removeEventListener?.('loadingdone', scheduleMeasure);
124
+ };
125
+ }
@@ -10,7 +10,9 @@
10
10
  * scroll control. Exposes APG
11
11
  * carousel semantics: the root is a labelled region with
12
12
  * aria-roledescription="carousel" and each item wrapper is a group with
13
- * aria-roledescription="slide" named "Slide N of M".
13
+ * aria-roledescription="slide" named "Slide N of M". The scroll area is a
14
+ * group named like the carousel and a tab stop only while its content
15
+ * overflows (AST-025 FR12).
14
16
  *
15
17
  * SYNC: When modified, update:
16
18
  * - /packages/core/src/Carousel/index.ts (exports)
@@ -1 +1 @@
1
- {"version":3,"file":"Carousel.d.ts","sourceRoot":"","sources":["../../src/Carousel/Carousel.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EACL,KAAK,SAAS,EAOf,MAAM,OAAO,CAAC;AAef,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAE5C,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,gBAAgB,CAAC;AAKhD;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,UAAU,IAAI,IAAI,CAAC;IACnB;;;OAGG;IACH,UAAU,IAAI,IAAI,CAAC;IACnB;;;;OAIG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B;;;;OAIG;IACH,aAAa,IAAI,OAAO,CAAC;IACzB;;;OAGG;IACH,aAAa,IAAI,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,aAAc,SAAQ,SAAS,CAAC,cAAc,CAAC;IAC9D,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAChC;;;OAGG;IACH,SAAS,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IACtC,kEAAkE;IAClE,QAAQ,EAAE,SAAS,CAAC;IACpB;;;OAGG;IACH,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,GAAG,CAAC,GAAG,GAAG,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACpC;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;;;;;;OAQG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;;;;;OAQG;IACH,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AA+JD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,QAAQ,CAAC,EACvB,GAAG,EACH,SAAS,EACT,QAAQ,EACR,GAAO,EACP,UAAiB,EACjB,WAAkB,EAClB,OAAe,EACf,OAAe,EACf,OAAO,EACP,YAAY,EAAE,kBAAkB,EAChC,MAAM,EACN,SAAS,EACT,KAAK,EACL,aAAa,EAAE,MAAM,EACrB,GAAG,SAAS,EACb,EAAE,aAAa,+BAyXf;yBAzYe,QAAQ"}
1
+ {"version":3,"file":"Carousel.d.ts","sourceRoot":"","sources":["../../src/Carousel/Carousel.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EACL,KAAK,SAAS,EAOf,MAAM,OAAO,CAAC;AAef,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAE5C,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,gBAAgB,CAAC;AAKhD;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,UAAU,IAAI,IAAI,CAAC;IACnB;;;OAGG;IACH,UAAU,IAAI,IAAI,CAAC;IACnB;;;;OAIG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B;;;;OAIG;IACH,aAAa,IAAI,OAAO,CAAC;IACzB;;;OAGG;IACH,aAAa,IAAI,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,aAAc,SAAQ,SAAS,CAAC,cAAc,CAAC;IAC9D,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAChC;;;OAGG;IACH,SAAS,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IACtC,kEAAkE;IAClE,QAAQ,EAAE,SAAS,CAAC;IACpB;;;OAGG;IACH,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,GAAG,CAAC,GAAG,GAAG,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACpC;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;;;;;;OAQG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;;;;;OAQG;IACH,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AA+JD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,QAAQ,CAAC,EACvB,GAAG,EACH,SAAS,EACT,QAAQ,EACR,GAAO,EACP,UAAiB,EACjB,WAAkB,EAClB,OAAe,EACf,OAAe,EACf,OAAO,EACP,YAAY,EAAE,kBAAkB,EAChC,MAAM,EACN,SAAS,EACT,KAAK,EACL,aAAa,EAAE,MAAM,EACrB,GAAG,SAAS,EACb,EAAE,aAAa,+BA+Xf;yBA/Ye,QAAQ"}
@@ -14,7 +14,9 @@
14
14
  * scroll control. Exposes APG
15
15
  * carousel semantics: the root is a labelled region with
16
16
  * aria-roledescription="carousel" and each item wrapper is a group with
17
- * aria-roledescription="slide" named "Slide N of M".
17
+ * aria-roledescription="slide" named "Slide N of M". The scroll area is a
18
+ * group named like the carousel and a tab stop only while its content
19
+ * overflows (AST-025 FR12).
18
20
  *
19
21
  * SYNC: When modified, update:
20
22
  * - /packages/core/src/Carousel/index.ts (exports)
@@ -447,8 +449,15 @@ export function Carousel({
447
449
  "aria-label": ariaLabel,
448
450
  "aria-roledescription": "carousel",
449
451
  children: [/*#__PURE__*/_jsx("div", {
450
- ref: composedRef,
451
- tabIndex: 0,
452
+ ref: composedRef
453
+ // The scroll area takes keyboard scrolling (spec:AST-025 FR12): a
454
+ // group named like the carousel, in the tab order only while its
455
+ // content overflows. While it fits it still takes focus
456
+ // programmatically, for the edge buttons' hand-off (FR2).
457
+ ,
458
+ role: "group",
459
+ "aria-label": ariaLabel,
460
+ tabIndex: hasOverflow ? 0 : -1,
452
461
  onWheel: handleWheel,
453
462
  ...mergeProps(themeProps('carousel-scroller', {
454
463
  gap,
@@ -1 +1 @@
1
- {"version":3,"file":"Popover.d.ts","sourceRoot":"","sources":["../../src/Popover/Popover.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"Popover.d.ts","sourceRoot":"","sources":["../../src/Popover/Popover.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,EAOZ,KAAK,YAAY,EACjB,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAEf,OAAO,KAAK,EACV,oBAAoB,EAErB,MAAM,kCAAkC,CAAC;AAQ1C,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAG5C,OAAO,KAAK,EAAC,cAAc,EAAE,cAAc,EAAC,MAAM,mBAAmB,CAAC;AAMtE,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,gBAAgB,CAAC;AAsFhD;;;;GAIG;AACH,MAAM,MAAM,mBAAmB,GAAG,oBAAoB,CAAC;AAEvD;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACxC,2EAA2E;IAC3E,GAAG,EAAE,CAAC,EAAE,EAAE,WAAW,GAAG,IAAI,KAAK,IAAI,CAAC;IACtC,sHAAsH;IACtH,OAAO,EAAE,CAAC,KAAK,CAAC,EAAE;QAAC,MAAM,EAAE,MAAM,CAAA;KAAC,KAAK,IAAI,CAAC;IAC5C,0EAA0E;IAC1E,eAAe,EAAE,QAAQ,CAAC;IAC1B,6DAA6D;IAC7D,eAAe,EAAE,OAAO,CAAC;IACzB,4DAA4D;IAC5D,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,YAAa,SAAQ,IAAI,CACxC,SAAS,EACT,QAAQ,GAAG,WAAW,GAAG,OAAO,CACjC;IACC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA6BG;IACH,QAAQ,CAAC,EAAE,SAAS,GAAG,CAAC,CAAC,KAAK,EAAE,yBAAyB,KAAK,SAAS,CAAC,CAAC;IAEzE;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC;IAEzC;;OAEG;IACH,OAAO,EAAE,SAAS,CAAC;IAEnB;;;;OAIG;IACH,SAAS,CAAC,EAAE,cAAc,CAAC;IAE3B;;;OAGG;IACH,SAAS,CAAC,EAAE,cAAc,CAAC;IAE3B;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IAEjB;;OAEG;IACH,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IAEzC;;;;OAIG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAExB;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,EAAE,WAAW,CAAC;IAEtB;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;;;;;;;OAQG;IACH,IAAI,CAAC,EAAE,QAAQ,GAAG,MAAM,CAAC;IAEzB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IAEzB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAE1B;;;;;;;;OAQG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IAEvB;;;;;OAKG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAE1B;;;;;;;OAOG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAE3B;;;;;;;;;;;;;;;;;;;OAmBG;IACH,YAAY,CAAC,EAAE,mBAAmB,CAAC;IAEnC;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AA6FD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,wBAAgB,OAAO,CAAC,EACtB,QAAQ,EACR,SAAS,EACT,OAAO,EACP,SAAmB,EACnB,SAAmB,EACnB,MAAM,EACN,YAAY,EACZ,SAAgB,EAChB,KAAK,EACL,OAAW,EACX,KAAK,EACL,IAAe,EACf,OAAO,EACP,cAAc,EACd,gBAAgB,EAChB,YAAY,EACZ,eAAsB,EACtB,gBAAuB,EACvB,YAAwB,EACxB,MAAM,EACN,SAAS,EACT,KAAK,EACL,aAAa,EAAE,MAAM,GACtB,EAAE,YAAY,GAAG,YAAY,CAgkB7B;yBAxlBe,OAAO"}
@@ -145,6 +145,15 @@ const styles = {
145
145
  }
146
146
  };
147
147
 
148
+ /**
149
+ * The window that holds `element`: the page's own, or a second document's (a
150
+ * same-origin iframe, a Document Picture-in-Picture window) when the Popover
151
+ * renders there.
152
+ */
153
+ function windowOf(element) {
154
+ return element?.ownerDocument.defaultView ?? window;
155
+ }
156
+
148
157
  // =============================================================================
149
158
  // Component
150
159
  // =============================================================================
@@ -374,11 +383,16 @@ export function Popover({
374
383
  if (measurementFrameRef.current != null) {
375
384
  return;
376
385
  }
377
- measurementFrameRef.current = window.requestAnimationFrame(() => {
378
- measurementFrameRef.current = null;
379
- measureOverflow();
380
- });
381
- }, [measureOverflow]);
386
+ // The surface's own window paints it, so its frame runs the measurement.
387
+ const view = windowOf(popover.contentRef.current);
388
+ measurementFrameRef.current = {
389
+ view,
390
+ id: view.requestAnimationFrame(() => {
391
+ measurementFrameRef.current = null;
392
+ measureOverflow();
393
+ })
394
+ };
395
+ }, [measureOverflow, popover.contentRef]);
382
396
  useIsomorphicLayoutEffect(() => {
383
397
  if (!popover.isOpen) {
384
398
  return;
@@ -387,8 +401,13 @@ export function Popover({
387
401
  if (!surface) {
388
402
  return;
389
403
  }
404
+ // Observe from the surface's own window. A Popover rendered into a second
405
+ // document resizes with that document's window, and a ResizeObserver the
406
+ // page created checks a box in a Picture-in-Picture window only while the
407
+ // page itself renders frames.
408
+ const view = windowOf(surface);
390
409
  measureOverflow();
391
- const resizeObserver = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(scheduleOverflowMeasurement);
410
+ const resizeObserver = typeof view.ResizeObserver === 'undefined' ? null : new view.ResizeObserver(scheduleOverflowMeasurement);
392
411
  const mutationObserver = typeof MutationObserver === 'undefined' ? null : new MutationObserver(scheduleOverflowMeasurement);
393
412
  resizeObserver?.observe(surface);
394
413
  mutationObserver?.observe(surface, {
@@ -397,16 +416,17 @@ export function Popover({
397
416
  subtree: true
398
417
  });
399
418
  surface.addEventListener('load', scheduleOverflowMeasurement, true);
400
- window.addEventListener('resize', scheduleOverflowMeasurement);
401
- window.visualViewport?.addEventListener('resize', scheduleOverflowMeasurement);
419
+ view.addEventListener('resize', scheduleOverflowMeasurement);
420
+ view.visualViewport?.addEventListener('resize', scheduleOverflowMeasurement);
402
421
  return () => {
403
422
  resizeObserver?.disconnect();
404
423
  mutationObserver?.disconnect();
405
424
  surface.removeEventListener('load', scheduleOverflowMeasurement, true);
406
- window.removeEventListener('resize', scheduleOverflowMeasurement);
407
- window.visualViewport?.removeEventListener('resize', scheduleOverflowMeasurement);
408
- if (measurementFrameRef.current != null) {
409
- window.cancelAnimationFrame(measurementFrameRef.current);
425
+ view.removeEventListener('resize', scheduleOverflowMeasurement);
426
+ view.visualViewport?.removeEventListener('resize', scheduleOverflowMeasurement);
427
+ const frame = measurementFrameRef.current;
428
+ if (frame != null) {
429
+ frame.view.cancelAnimationFrame(frame.id);
410
430
  measurementFrameRef.current = null;
411
431
  }
412
432
  };
@@ -1 +1 @@
1
- {"version":3,"file":"Step.d.ts","sourceRoot":"","sources":["../../src/Stepper/Step.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,OAAO,EAAkB,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAsBtD,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAM5C,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,cAAc,CAAC;AAE7C;;;;;;;GAOG;AACH,MAAM,MAAM,mBAAmB,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAC;AAC7D,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,UAAU,GAAG,UAAU,CAAC;AAE9D,MAAM,WAAW,SAAU,SAAQ,SAAS,CAAC,aAAa,CAAC;IACzD,wCAAwC;IACxC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;IAC/B;;;OAGG;IACH,IAAI,EAAE,MAAM,CAAC;IACb;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,SAAS,CAAC;IAErB;;;;;;;;;;;;;OAaG;IACH,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;OAGG;IACH,UAAU,CAAC,EAAE,SAAS,CAAC;IACvB;;;;;;;;OAQG;IACH,SAAS,CAAC,EAAE,mBAAmB,GAAG,SAAS,CAAC;IAC5C;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,WAAW,CAAC;CACvB;AAozBD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,IAAI,CAAC,EACnB,IAAI,EACJ,KAAK,EACL,WAAW,EACX,QAAQ,EACR,MAAM,EACN,UAAkB,EAClB,UAAkB,EAClB,UAAU,EACV,SAAS,EAAE,aAAa,EACxB,OAAO,EAAE,WAAW,EACpB,MAAM,EACN,SAAS,EACT,KAAK,EACL,GAAG,EACH,aAAa,EAAE,UAAU,EACzB,GAAG,IAAI,EACR,EAAE,SAAS,+BAq1BX;yBAt2Be,IAAI"}
1
+ {"version":3,"file":"Step.d.ts","sourceRoot":"","sources":["../../src/Stepper/Step.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,OAAO,EAAyB,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAsB7D,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAM5C,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,cAAc,CAAC;AAE7C;;;;;;;GAOG;AACH,MAAM,MAAM,mBAAmB,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAC;AAC7D,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,UAAU,GAAG,UAAU,CAAC;AAE9D,MAAM,WAAW,SAAU,SAAQ,SAAS,CAAC,aAAa,CAAC;IACzD,wCAAwC;IACxC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;IAC/B;;;OAGG;IACH,IAAI,EAAE,MAAM,CAAC;IACb;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,SAAS,CAAC;IAErB;;;;;;;;;;;;;OAaG;IACH,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;OAGG;IACH,UAAU,CAAC,EAAE,SAAS,CAAC;IACvB;;;;;;;;OAQG;IACH,SAAS,CAAC,EAAE,mBAAmB,GAAG,SAAS,CAAC;IAC5C;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,WAAW,CAAC;CACvB;AAozBD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,IAAI,CAAC,EACnB,IAAI,EACJ,KAAK,EACL,WAAW,EACX,QAAQ,EACR,MAAM,EACN,UAAkB,EAClB,UAAkB,EAClB,UAAU,EACV,SAAS,EAAE,aAAa,EACxB,OAAO,EAAE,WAAW,EACpB,MAAM,EACN,SAAS,EACT,KAAK,EACL,GAAG,EACH,aAAa,EAAE,UAAU,EACzB,GAAG,IAAI,EACR,EAAE,SAAS,+BAg3BX;yBAj4Be,IAAI"}
@@ -37,7 +37,7 @@
37
37
  * - /packages/cli/assets/templates/blocks/components/Stepper/ (showcase blocks)
38
38
  * - /packages/cli/assets/templates/blocks/components/Step/ (showcase blocks)
39
39
  */
40
- import { useLayoutEffect } from 'react';
40
+ import { useId, useLayoutEffect } from 'react';
41
41
  import { createPortal } from 'react-dom';
42
42
  import * as stylex from '@stylexjs/stylex';
43
43
  import "../theme/tokens.stylex.js";
@@ -719,7 +719,12 @@ export function Step({
719
719
  // A compact horizontal stepper moves navigation to the named controls in its
720
720
  // summary row. Its track — including the on-track indicators — stays purely
721
721
  // presentational so it does not expose a second, denser set of click targets.
722
- const isClickable = !isDisabled && onStepClick != null && !isCompact;
722
+ const hasButton = onStepClick != null && !isCompact;
723
+ // A disabled step keeps its button, focusable and aria-disabled, so it
724
+ // keeps its tab stop, is announced as unavailable, and keeps focus when it
725
+ // becomes disabled while focused (`spec:AST-085` FR1, FR4, FR5); only
726
+ // activation is withheld.
727
+ const isClickable = hasButton && !isDisabled;
723
728
  const handleClick = () => {
724
729
  if (isClickable && onStepClick) {
725
730
  onStepClick(step);
@@ -893,7 +898,13 @@ export function Step({
893
898
  })]
894
899
  }), endContent]
895
900
  });
901
+
902
+ // A clickable step's name is its number, label and status word; the
903
+ // description describes it, so a disabled step's reason reaches assistive
904
+ // technology too (spec:AST-085 FR5).
905
+ const descriptionID = useId();
896
906
  const descriptionNode = isRenderable(description) ? /*#__PURE__*/_jsx("div", {
907
+ id: descriptionID,
897
908
  ...{
898
909
  0: {
899
910
  className: "x18gyask"
@@ -1027,6 +1038,7 @@ export function Step({
1027
1038
  }), endContent]
1028
1039
  });
1029
1040
  const otDescriptionNode = isRenderable(description) ? /*#__PURE__*/_jsx("span", {
1041
+ id: descriptionID,
1030
1042
  ...mergeProps(descriptionThemeProps, {
1031
1043
  className: "x1lliihq x141an7d x1ojctns xv1l7n4"
1032
1044
  }),
@@ -1080,10 +1092,12 @@ export function Step({
1080
1092
  "aria-current": isActive ? 'step' : undefined,
1081
1093
  "data-testid": dataTestId,
1082
1094
  ...rest,
1083
- children: [isClickable ? /*#__PURE__*/_jsx("button", {
1095
+ children: [hasButton ? /*#__PURE__*/_jsx("button", {
1084
1096
  type: "button",
1085
1097
  onClick: handleClick,
1086
1098
  "aria-label": stepAriaLabel,
1099
+ "aria-describedby": isRenderable(description) ? descriptionID : undefined,
1100
+ "aria-disabled": isDisabled || undefined,
1087
1101
  ...pressable,
1088
1102
  ...stylex.props(styles.otInteractive, interactionOverlayStyles.backgroundColor, styles.otRowWrap, styles.otRowPadV(densitySpace), focusOutlineStyles.focusVisible),
1089
1103
  children: inner
@@ -1118,10 +1132,12 @@ export function Step({
1118
1132
  "aria-current": isActive ? 'step' : undefined,
1119
1133
  "data-testid": dataTestId,
1120
1134
  ...rest,
1121
- children: [isClickable ? /*#__PURE__*/_jsx("button", {
1135
+ children: [hasButton ? /*#__PURE__*/_jsx("button", {
1122
1136
  type: "button",
1123
1137
  onClick: handleClick,
1124
1138
  "aria-label": stepAriaLabel,
1139
+ "aria-describedby": isRenderable(description) ? descriptionID : undefined,
1140
+ "aria-disabled": isDisabled || undefined,
1125
1141
  ...pressable,
1126
1142
  ...stylex.props(styles.otInteractive, interactionOverlayStyles.backgroundColor, styles.otColWrap, styles.otPadBlock(densitySpace), focusOutlineStyles.focusVisible),
1127
1143
  children: innerH
@@ -1147,10 +1163,12 @@ export function Step({
1147
1163
  ...{
1148
1164
  className: "x78zum5 xdt5ytf x98rzlu"
1149
1165
  },
1150
- children: [isClickable ? /*#__PURE__*/_jsxs("button", {
1166
+ children: [hasButton ? /*#__PURE__*/_jsxs("button", {
1151
1167
  type: "button",
1152
1168
  onClick: handleClick,
1153
1169
  "aria-label": stepAriaLabel,
1170
+ "aria-describedby": isRenderable(description) ? descriptionID : undefined,
1171
+ "aria-disabled": isDisabled || undefined,
1154
1172
  ...pressable,
1155
1173
  ...stylex.props(styles.buttonReset, interactionOverlayStyles.backgroundColor, focusOutlineStyles.focusVisible, density === 'compact' && styles.densityCompact, density === 'balanced' && styles.densityBalanced, density === 'spacious' && styles.densitySpacious, styles.densityInline(separatedDensityInline)),
1156
1174
  children: [iconLabelNode, descriptionNode]
@@ -1172,10 +1190,12 @@ export function Step({
1172
1190
  children: [/*#__PURE__*/_jsx("div", {
1173
1191
  ...mergeProps(themeProps('step-bar'), stylex.props(styles.horizontalBar, styles.connectorTrack, isBarFilled ? styles.connectorFillH : styles.connectorEmptyH, barTiming)),
1174
1192
  "aria-hidden": "true"
1175
- }), isCompact ? compactNameNode : isClickable ? /*#__PURE__*/_jsxs("button", {
1193
+ }), isCompact ? compactNameNode : hasButton ? /*#__PURE__*/_jsxs("button", {
1176
1194
  type: "button",
1177
1195
  onClick: handleClick,
1178
1196
  "aria-label": stepAriaLabel,
1197
+ "aria-describedby": isRenderable(description) ? descriptionID : undefined,
1198
+ "aria-disabled": isDisabled || undefined,
1179
1199
  ...stylex.props(styles.buttonReset, interactionOverlayStyles.backgroundColor, focusOutlineStyles.focusVisible, density === 'compact' && styles.densityCompact, density === 'balanced' && styles.densityBalanced, density === 'spacious' && styles.densitySpacious, styles.densityInline(separatedDensityInline)),
1180
1200
  children: [iconLabelNode, descriptionNode]
1181
1201
  }) : /*#__PURE__*/_jsxs("div", {
package/dist/astryx.css CHANGED
@@ -1525,6 +1525,7 @@
1525
1525
  .x1wxx7ce.x1wxx7ce:where(.astryx-input-group > [data-astryx-input-group-item]):has(~ *:not([popover]):not(template):not(dialog)):not(#\#):not(#\#):not(#\#){border-start-end-radius:0}
1526
1526
  .x149bkfe:not(:has(~ *:not([popover]):not(template):not(dialog))):not(#\#):not(#\#):not(#\#){border-start-end-radius:var(--radius-element)}
1527
1527
  .x132ell6.x132ell6:where(.astryx-input-group > [data-astryx-input-group-item]):not(:first-child):not(#\#):not(#\#):not(#\#){border-start-start-radius:0}
1528
+ .x9a4xmf:focus-visible:not(#\#):not(#\#):not(#\#){border-start-start-radius:var(--radius-page)}
1528
1529
  .xuihb5h:focus-within:not(#\#):not(#\#):not(#\#){box-shadow:inset 0 0 0 2px var(--color-accent-muted)}
1529
1530
  .x15ysqaf:focus-within:not(#\#):not(#\#):not(#\#){box-shadow:none}
1530
1531
  .xyavpck:focus-within:not(#\#):not(#\#):not(#\#){box-shadow:var(--shadow-med)}