@astryxdesign/core 0.7.9-canary.9bd11b3 → 0.7.9-canary.cb2a34a

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 (49) hide show
  1. package/dist/AppShell/observeMainOverflow.d.ts.map +1 -1
  2. package/dist/AppShell/observeMainOverflow.js +9 -6
  3. package/dist/InputGroup/InputGroup.d.ts +4 -3
  4. package/dist/InputGroup/InputGroup.d.ts.map +1 -1
  5. package/dist/InputGroup/InputGroup.js +2 -2
  6. package/dist/InputGroup/groupStyles.d.ts +1 -1
  7. package/dist/InputGroup/groupStyles.d.ts.map +1 -1
  8. package/dist/InputGroup/groupStyles.js +1 -1
  9. package/dist/Layout/LayoutContent.d.ts +3 -1
  10. package/dist/Layout/LayoutContent.d.ts.map +1 -1
  11. package/dist/Layout/LayoutContent.js +12 -4
  12. package/dist/Layout/LayoutPanel.d.ts +4 -1
  13. package/dist/Layout/LayoutPanel.d.ts.map +1 -1
  14. package/dist/Layout/LayoutPanel.js +13 -4
  15. package/dist/Stack/StackItem.d.ts +8 -4
  16. package/dist/Stack/StackItem.d.ts.map +1 -1
  17. package/dist/Stack/StackItem.js +17 -4
  18. package/dist/astryx.css +1 -1
  19. package/dist/hooks/StackItemScrollRegion.d.ts +31 -0
  20. package/dist/hooks/StackItemScrollRegion.d.ts.map +1 -0
  21. package/dist/hooks/StackItemScrollRegion.js +40 -0
  22. package/dist/hooks/useScrollRegionProps.d.ts +35 -0
  23. package/dist/hooks/useScrollRegionProps.d.ts.map +1 -0
  24. package/dist/hooks/useScrollRegionProps.js +51 -0
  25. package/dist/i18n/generated-english/hooks/useScrollRegionProps.generated.d.ts +8 -0
  26. package/dist/i18n/generated-english/hooks/useScrollRegionProps.generated.d.ts.map +1 -0
  27. package/dist/i18n/generated-english/hooks/useScrollRegionProps.generated.js +12 -0
  28. package/dist/i18n/generated-locales/en.generated.d.ts.map +1 -1
  29. package/dist/i18n/generated-locales/en.generated.js +1 -0
  30. package/dist/i18n/generated-locales/pseudo.generated.d.ts.map +1 -1
  31. package/dist/i18n/generated-locales/pseudo.generated.js +1 -0
  32. package/locales/en.json +4 -0
  33. package/locales/pseudo.json +3 -0
  34. package/package.json +4 -4
  35. package/src/AppShell/observeMainOverflow.ts +9 -6
  36. package/src/InputGroup/InputGroup.doc.mjs +4 -3
  37. package/src/InputGroup/InputGroup.tsx +4 -3
  38. package/src/InputGroup/groupStyles.ts +3 -1
  39. package/src/Layout/LayoutContent.doc.mjs +6 -3
  40. package/src/Layout/LayoutContent.tsx +10 -4
  41. package/src/Layout/LayoutPanel.doc.mjs +6 -3
  42. package/src/Layout/LayoutPanel.tsx +11 -4
  43. package/src/Stack/Stack.doc.mjs +1 -1
  44. package/src/Stack/StackItem.tsx +27 -13
  45. package/src/hooks/StackItemScrollRegion.tsx +57 -0
  46. package/src/hooks/useScrollRegionProps.ts +76 -0
  47. package/src/i18n/generated-english/hooks/useScrollRegionProps.generated.ts +13 -0
  48. package/src/i18n/generated-locales/en.generated.ts +1 -0
  49. package/src/i18n/generated-locales/pseudo.generated.ts +1 -0
@@ -1 +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"}
1
+ {"version":3,"file":"observeMainOverflow.d.ts","sourceRoot":"","sources":["../../src/AppShell/observeMainOverflow.ts"],"names":[],"mappings":"AA4CA;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,WAAW,EACjB,QAAQ,EAAE,CAAC,SAAS,EAAE,OAAO,KAAK,IAAI,GACrC,MAAM,IAAI,CA2FZ"}
@@ -4,20 +4,23 @@
4
4
 
5
5
  /**
6
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
7
+ * @input A scroll container that holds its content as direct children with
8
+ * no wrapper: AppShell's main region under `height="fill"`, or a scrolling
9
+ * LayoutContent, LayoutPanel or StackItem
9
10
  * @output observeMainOverflow(main, onChange): reports whether main's content
10
11
  * overflows it on either axis, once at the start and on every change, and
11
12
  * returns the function that stops observing
12
- * @position Internal to AppShell and never exported. AppShell makes main a
13
+ * @position Internal and never exported. AppShell makes main a
13
14
  * 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.
15
+ * `component:AppShell` FR4 and PR1). useScrollRegionProps names a
16
+ * scrolling region only while it overflows. Main has no inner content
17
+ * box, so its content grows inside it without resizing it: its direct
18
+ * children's sizes are watched instead.
17
19
  *
18
20
  * SYNC: When modified, update:
19
21
  * - /packages/core/src/AppShell/AppShell.tsx
20
22
  * - /packages/core/src/AppShell/observeMainOverflow.test.ts
23
+ * - /packages/core/src/hooks/useScrollRegionProps.ts
21
24
  */
22
25
  import { getLogicalAxisMapping, measureLogicalScrollAxis } from "../hooks/scrollGeometry.js";
23
26
  import { observeMutations } from "../utils/sharedMutationObserver.js";
@@ -59,7 +59,8 @@ export interface InputGroupProps extends Omit<BaseProps<HTMLDivElement>, 'childr
59
59
  */
60
60
  size?: InputGroupSize;
61
61
  /**
62
- * Status indicator applied to the group border.
62
+ * Validation status. Its message shows below the group. The group border
63
+ * does not change; a theme can style the row from `data-status`.
63
64
  */
64
65
  status?: InputStatus;
65
66
  /**
@@ -72,8 +73,8 @@ export interface InputGroupProps extends Omit<BaseProps<HTMLDivElement>, 'childr
72
73
  'data-testid'?: string;
73
74
  }
74
75
  /**
75
- * Groups an input with prefix/suffix addons in a visually connected
76
- * container with shared border and focus ring.
76
+ * Groups an input with prefix and suffix addons in one connected row. A
77
+ * focused member stays above its neighbours, so its focus ring shows in full.
77
78
  *
78
79
  * @example
79
80
  * ```
@@ -1 +1 @@
1
- {"version":3,"file":"InputGroup.d.ts","sourceRoot":"","sources":["../../src/InputGroup/InputGroup.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAsB,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAG1D,OAAO,EAAQ,KAAK,WAAW,EAAC,MAAM,UAAU,CAAC;AAGjD,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAK5C,MAAM,MAAM,cAAc,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AA0BhD,MAAM,WAAW,eAAgB,SAAQ,IAAI,CAC3C,SAAS,CAAC,cAAc,CAAC,EACzB,UAAU,CACX;IACC,mDAAmD;IACnD,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAEhC;;OAEG;IACH,QAAQ,EAAE,SAAS,CAAC;IAEpB;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IAExB;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;;OAGG;IACH,IAAI,CAAC,EAAE,cAAc,CAAC;IAEtB;;OAEG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;IAErB;;OAEG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CAAC,EACzB,QAAQ,EACR,KAAK,EACL,aAAqB,EACrB,WAAW,EACX,UAAU,EAAE,cAAsB,EAClC,UAAkB,EAClB,UAAkB,EAClB,IAAI,EAAE,QAAQ,EACd,MAAM,EACN,YAAY,EACZ,MAAM,EACN,SAAS,EACT,KAAK,EACL,GAAG,EACH,aAAa,EAAE,MAAM,EACrB,GAAG,IAAI,EACR,EAAE,eAAe,+BA8EjB;yBA/Fe,UAAU"}
1
+ {"version":3,"file":"InputGroup.d.ts","sourceRoot":"","sources":["../../src/InputGroup/InputGroup.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAsB,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAG1D,OAAO,EAAQ,KAAK,WAAW,EAAC,MAAM,UAAU,CAAC;AAGjD,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAK5C,MAAM,MAAM,cAAc,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AA0BhD,MAAM,WAAW,eAAgB,SAAQ,IAAI,CAC3C,SAAS,CAAC,cAAc,CAAC,EACzB,UAAU,CACX;IACC,mDAAmD;IACnD,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAEhC;;OAEG;IACH,QAAQ,EAAE,SAAS,CAAC;IAEpB;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IAExB;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;;OAGG;IACH,IAAI,CAAC,EAAE,cAAc,CAAC;IAEtB;;;OAGG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;IAErB;;OAEG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CAAC,EACzB,QAAQ,EACR,KAAK,EACL,aAAqB,EACrB,WAAW,EACX,UAAU,EAAE,cAAsB,EAClC,UAAkB,EAClB,UAAkB,EAClB,IAAI,EAAE,QAAQ,EACd,MAAM,EACN,YAAY,EACZ,MAAM,EACN,SAAS,EACT,KAAK,EACL,GAAG,EACH,aAAa,EAAE,MAAM,EACrB,GAAG,IAAI,EACR,EAAE,eAAe,+BA8EjB;yBA/Fe,UAAU"}
@@ -57,8 +57,8 @@ const sizeStyles = {
57
57
  }
58
58
  };
59
59
  /**
60
- * Groups an input with prefix/suffix addons in a visually connected
61
- * container with shared border and focus ring.
60
+ * Groups an input with prefix and suffix addons in one connected row. A
61
+ * focused member stays above its neighbours, so its focus ring shows in full.
62
62
  *
63
63
  * @example
64
64
  * ```
@@ -33,7 +33,7 @@ export declare const groupStyles: Readonly<{
33
33
  readonly borderStartEndRadius: stylex.StyleXClassNameFor<"borderStartEndRadius", 0 | "8px">;
34
34
  readonly borderEndEndRadius: stylex.StyleXClassNameFor<"borderEndEndRadius", 0 | "8px">;
35
35
  readonly ':focus-within': stylex.StyleXClassNameFor<":focus-within", {
36
- readonly zIndex: 1;
36
+ readonly zIndex: 2;
37
37
  }>;
38
38
  }>;
39
39
  }>;
@@ -1 +1 @@
1
- {"version":3,"file":"groupStyles.d.ts","sourceRoot":"","sources":["../../src/InputGroup/groupStyles.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AAEH,OAAO,KAAK,MAAM,MAAM,kBAAkB,CAAC;AAG3C;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB;;CAEtB,CAAC;AA0BX,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;EAwDtB,CAAC"}
1
+ {"version":3,"file":"groupStyles.d.ts","sourceRoot":"","sources":["../../src/InputGroup/groupStyles.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AAEH,OAAO,KAAK,MAAM,MAAM,kBAAkB,CAAC;AAG3C;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB;;CAEtB,CAAC;AA0BX,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;EA0DtB,CAAC"}
@@ -62,7 +62,7 @@ export const groupStyles = {
62
62
  kVL7Gh: "xbiv7yw x1xrp5p4",
63
63
  kfmiAY: "x1ga7v0g x1ursdma",
64
64
  kT0f0o: "x16uus16 x1qa286i",
65
- kXnDq0: "x1rm9qnc",
65
+ kXnDq0: "x1pc0aet",
66
66
  $$css: true
67
67
  }
68
68
  };
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @file LayoutContent.tsx
3
- * @input Uses React StyleX, LayoutSlotsContext
3
+ * @input Uses React StyleX, LayoutSlotsContext, useScrollRegionProps
4
4
  * @output Exports LayoutContent component and LayoutContentProps
5
5
  * @position Scrollable main content area for Layout. Wraps the primary body content
6
6
  * with automatic scroll containment and context-aware padding.
@@ -35,6 +35,8 @@ export interface LayoutContentProps extends BaseProps<HTMLDivElement> {
35
35
  /**
36
36
  * Accessible label for the landmark.
37
37
  * Required when role is set and multiple landmarks of the same type exist.
38
+ * While the content scrolls, it also names the scrolling region, which
39
+ * otherwise takes a translated "Content".
38
40
  */
39
41
  label?: string;
40
42
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"LayoutContent.d.ts","sourceRoot":"","sources":["../../src/Layout/LayoutContent.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAC,QAAQ,EAAE,SAAS,EAAC,MAAM,OAAO,CAAC;AAC/C,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAM5C,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,gBAAgB,CAAC;AAqFhD,MAAM,WAAW,kBAAmB,SAAQ,SAAS,CAAC,cAAc,CAAC;IACnE,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAChC;;OAEG;IACH,QAAQ,CAAC,EAAE,SAAS,CAAC;IAErB;;;;OAIG;IACH,OAAO,CAAC,EAAE,WAAW,CAAC;IAEtB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IAEvB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;;OAGG;IACH,IAAI,CAAC,EAAE,QAAQ,CAAC;CACjB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,wBAAgB,aAAa,CAAC,EAC5B,QAAQ,EACR,YAAmB,EACnB,OAAO,EACP,KAAK,EACL,IAAI,EACJ,MAAM,EACN,SAAS,EACT,KAAK,EACL,GAAG,EACH,GAAG,KAAK,EACT,EAAE,kBAAkB,+BAkDpB;yBA7De,aAAa"}
1
+ {"version":3,"file":"LayoutContent.d.ts","sourceRoot":"","sources":["../../src/Layout/LayoutContent.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAC,QAAQ,EAAE,SAAS,EAAC,MAAM,OAAO,CAAC;AAC/C,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAQ5C,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,gBAAgB,CAAC;AAqFhD,MAAM,WAAW,kBAAmB,SAAQ,SAAS,CAAC,cAAc,CAAC;IACnE,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAChC;;OAEG;IACH,QAAQ,CAAC,EAAE,SAAS,CAAC;IAErB;;;;OAIG;IACH,OAAO,CAAC,EAAE,WAAW,CAAC;IAEtB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IAEvB;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;;OAGG;IACH,IAAI,CAAC,EAAE,QAAQ,CAAC;CACjB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,wBAAgB,aAAa,CAAC,EAC5B,QAAQ,EACR,YAAmB,EACnB,OAAO,EACP,KAAK,EACL,IAAI,EACJ,MAAM,EACN,SAAS,EACT,KAAK,EACL,GAAG,EACH,GAAG,KAAK,EACT,EAAE,kBAAkB,+BAoDpB;yBA/De,aAAa"}
@@ -4,7 +4,7 @@
4
4
 
5
5
  /**
6
6
  * @file LayoutContent.tsx
7
- * @input Uses React StyleX, LayoutSlotsContext
7
+ * @input Uses React StyleX, LayoutSlotsContext, useScrollRegionProps
8
8
  * @output Exports LayoutContent component and LayoutContentProps
9
9
  * @position Scrollable main content area for Layout. Wraps the primary body content
10
10
  * with automatic scroll containment and context-aware padding.
@@ -19,6 +19,8 @@ import * as stylex from '@stylexjs/stylex';
19
19
  import "../theme/tokens.stylex.js";
20
20
  import { spacingVars } from "../theme/tokens.stylex.js";
21
21
  import { LayoutSlotsContext } from "./LayoutSlotsContext.js";
22
+ import { useMergedRefs } from "../hooks/useMergedRefs.js";
23
+ import { useScrollRegionProps } from "../hooks/useScrollRegionProps.js";
22
24
  import { mergeProps } from "../utils/mergeProps.js";
23
25
  import { themeProps } from "../utils/themeProps.js";
24
26
  import { paddingStyles, containerPaddingInlineVarStyles, containerPaddingBlockStartVarStyles, containerPaddingBlockEndVarStyles } from "./padding.stylex.js";
@@ -146,10 +148,16 @@ export function LayoutContent({
146
148
  hasEnd
147
149
  } = use(LayoutSlotsContext);
148
150
  const isZeroPadding = padding === 0;
151
+ const region = useScrollRegionProps({
152
+ isScrollable,
153
+ role,
154
+ label
155
+ });
156
+ const mergedRef = useMergedRefs(ref, region.ref);
149
157
  return /*#__PURE__*/_jsx("div", {
150
- ref: ref,
151
- role: role,
152
- "aria-label": label,
158
+ ref: mergedRef,
159
+ role: region.role,
160
+ "aria-label": region['aria-label'],
153
161
  ...mergeProps(themeProps('layout-content'), stylex.props(styles.content,
154
162
  // Outer padding on container edges (unless content is full bleed)
155
163
  !hasStart && !isZeroPadding && padding == null && styles.noStart, !hasEnd && !isZeroPadding && padding == null && styles.noEnd, !hasHeader && !isZeroPadding && padding == null && styles.noHeader, !hasFooter && !isZeroPadding && padding == null && styles.noFooter, isScrollable && styles.scrollable, isZeroPadding && styles.fullBleed, padding != null && paddingStyles[padding], padding != null && containerPaddingInlineVarStyles[padding], padding != null && containerPaddingBlockStartVarStyles[padding], padding != null && containerPaddingBlockEndVarStyles[padding], !hasStart && !hasEnd && !isZeroPadding && styles.constrainedNoPanelsStart, !hasStart && !hasEnd && !isZeroPadding && styles.constrainedNoPanelsEnd, hasStart && !hasEnd && !isZeroPadding && styles.constrainedSingleStartPanel, !hasStart && hasEnd && !isZeroPadding && styles.constrainedSingleEndPanel, xstyle), className, style),
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * @file LayoutPanel.tsx
3
- * @input Uses React StyleX, LayoutAreaContext, LayoutSlotsContext
3
+ * @input Uses React StyleX, LayoutAreaContext, LayoutSlotsContext,
4
+ * useScrollRegionProps
4
5
  * @output Exports LayoutPanel component and LayoutPanelProps
5
6
  * @position Sidebar panel for Layout start/end slots. Use for navigation panels,
6
7
  * settings sidebars, detail panels, or any fixed-width side content.
@@ -47,6 +48,8 @@ export interface LayoutPanelProps extends BaseProps<HTMLDivElement> {
47
48
  /**
48
49
  * Accessible label for the landmark.
49
50
  * Required when role is set and multiple landmarks of the same type exist.
51
+ * While the panel scrolls, it also names the scrolling region, which
52
+ * otherwise takes a translated "Content".
50
53
  */
51
54
  label?: string;
52
55
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"LayoutPanel.d.ts","sourceRoot":"","sources":["../../src/Layout/LayoutPanel.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAC,QAAQ,EAAE,SAAS,EAAC,MAAM,OAAO,CAAC;AAC/C,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAO5C,OAAO,KAAK,EAAC,SAAS,EAAE,WAAW,EAAC,MAAM,gBAAgB,CAAC;AAC3D,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAmF9D,MAAM,WAAW,gBAAiB,SAAQ,SAAS,CAAC,cAAc,CAAC;IACjE,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAChC;;OAEG;IACH,QAAQ,CAAC,EAAE,SAAS,CAAC;IAErB;;;;;;;;;OASG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;;;OAIG;IACH,OAAO,CAAC,EAAE,WAAW,CAAC;IAEtB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IAEvB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;;OAGG;IACH,IAAI,CAAC,EAAE,QAAQ,CAAC;IAEhB;;;;OAIG;IACH,KAAK,CAAC,EAAE,SAAS,CAAC;IAElB;;;;;;;;;;;;;OAaG;IACH,SAAS,CAAC,EAAE,cAAc,CAAC;CAC5B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,WAAW,CAAC,EAC1B,QAAQ,EACR,UAAkB,EAClB,YAAmB,EACnB,KAAK,EACL,OAAO,EACP,IAAI,EACJ,KAAK,EACL,SAAS,EACT,MAAM,EACN,SAAS,EACT,KAAK,EACL,GAAG,EACH,GAAG,KAAK,EACT,EAAE,gBAAgB,+BAkElB;yBAhFe,WAAW"}
1
+ {"version":3,"file":"LayoutPanel.d.ts","sourceRoot":"","sources":["../../src/Layout/LayoutPanel.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAC,QAAQ,EAAE,SAAS,EAAC,MAAM,OAAO,CAAC;AAC/C,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAS5C,OAAO,KAAK,EAAC,SAAS,EAAE,WAAW,EAAC,MAAM,gBAAgB,CAAC;AAC3D,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAmF9D,MAAM,WAAW,gBAAiB,SAAQ,SAAS,CAAC,cAAc,CAAC;IACjE,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAChC;;OAEG;IACH,QAAQ,CAAC,EAAE,SAAS,CAAC;IAErB;;;;;;;;;OASG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;;;OAIG;IACH,OAAO,CAAC,EAAE,WAAW,CAAC;IAEtB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IAEvB;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;;OAGG;IACH,IAAI,CAAC,EAAE,QAAQ,CAAC;IAEhB;;;;OAIG;IACH,KAAK,CAAC,EAAE,SAAS,CAAC;IAElB;;;;;;;;;;;;;OAaG;IACH,SAAS,CAAC,EAAE,cAAc,CAAC;CAC5B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,WAAW,CAAC,EAC1B,QAAQ,EACR,UAAkB,EAClB,YAAmB,EACnB,KAAK,EACL,OAAO,EACP,IAAI,EACJ,KAAK,EACL,SAAS,EACT,MAAM,EACN,SAAS,EACT,KAAK,EACL,GAAG,EACH,GAAG,KAAK,EACT,EAAE,gBAAgB,+BAoElB;yBAlFe,WAAW"}
@@ -4,7 +4,8 @@
4
4
 
5
5
  /**
6
6
  * @file LayoutPanel.tsx
7
- * @input Uses React StyleX, LayoutAreaContext, LayoutSlotsContext
7
+ * @input Uses React StyleX, LayoutAreaContext, LayoutSlotsContext,
8
+ * useScrollRegionProps
8
9
  * @output Exports LayoutPanel component and LayoutPanelProps
9
10
  * @position Sidebar panel for Layout start/end slots. Use for navigation panels,
10
11
  * settings sidebars, detail panels, or any fixed-width side content.
@@ -20,6 +21,8 @@ import "../theme/tokens.stylex.js";
20
21
  import { colorVars, spacingVars } from "../theme/tokens.stylex.js";
21
22
  import { LayoutAreaContext } from "./LayoutAreaContext.js";
22
23
  import { LayoutSlotsContext } from "./LayoutSlotsContext.js";
24
+ import { useMergedRefs } from "../hooks/useMergedRefs.js";
25
+ import { useScrollRegionProps } from "../hooks/useScrollRegionProps.js";
23
26
  import { mergeProps } from "../utils/index.js";
24
27
  import { themeProps } from "../utils/themeProps.js";
25
28
  import { paddingStyles, containerPaddingInlineVarStyles, containerPaddingBlockStartVarStyles, containerPaddingBlockEndVarStyles } from "./padding.stylex.js";
@@ -160,6 +163,12 @@ export function LayoutPanel({
160
163
 
161
164
  // When no divider, collapse spacing for seamless visual flow
162
165
  const shouldCollapseSpacing = !hasDivider && !isZeroPadding && padding == null;
166
+ const region = useScrollRegionProps({
167
+ isScrollable,
168
+ role,
169
+ label
170
+ });
171
+ const mergedRef = useMergedRefs(ref, region.ref);
163
172
 
164
173
  // Select divider style based on position
165
174
  const dividerStyle = isStartPanel ? styles.dividerEnd : isEndPanel ? styles.dividerStart : null;
@@ -167,9 +176,9 @@ export function LayoutPanel({
167
176
  // Select collapse style based on position (collapse the side where divider would be)
168
177
  const collapseStyle = isStartPanel ? styles.collapseEnd : isEndPanel ? styles.collapseStart : null;
169
178
  return /*#__PURE__*/_jsx("div", {
170
- ref: ref,
171
- role: role,
172
- "aria-label": label,
179
+ ref: mergedRef,
180
+ role: region.role,
181
+ "aria-label": region['aria-label'],
173
182
  ...mergeProps(themeProps('layout-panel'), stylex.props(styles.panel, dynamicStyles.sizing(effectiveWidth ?? null),
174
183
  // Outer padding on container edges (unless component is full bleed)
175
184
  isStartPanel && !isZeroPadding && padding == null && styles.startPanel, isEndPanel && !isZeroPadding && padding == null && styles.endPanel, !hasHeader && !isZeroPadding && padding == null && styles.noHeader, !hasFooter && !isZeroPadding && padding == null && styles.noFooter, isScrollable && styles.scrollable, isZeroPadding && styles.fullBleed, padding != null && paddingStyles[padding], padding != null && containerPaddingInlineVarStyles[padding], padding != null && containerPaddingBlockStartVarStyles[padding], padding != null && containerPaddingBlockEndVarStyles[padding], hasDivider && dividerStyle, shouldCollapseSpacing && collapseStyle, xstyle), className, style),
@@ -1,8 +1,10 @@
1
1
  /**
2
2
  * @file StackItem.tsx
3
- * @input Uses React, ElementType, stackItem utility
3
+ * @input Uses React, ElementType, stackItem utility, StackItemScrollRegion
4
4
  * @output Exports StackItem polymorphic component and StackItemProps
5
- * @position Layout/Stack component; uses stackItem.stylex.ts
5
+ * @position Layout/Stack component; uses stackItem.stylex.ts. A scrollable
6
+ * `div` or `span` item renders through StackItemScrollRegion, which names
7
+ * it while it scrolls.
6
8
  *
7
9
  * SYNC: When modified, update these files to stay in sync:
8
10
  * - /packages/core/src/Stack/Stack.doc.mjs
@@ -34,7 +36,9 @@ export interface StackItemProps extends BaseProps<HTMLElement> {
34
36
  * reset, so `<StackItem size="fill" isScrollable>` is a complete scroll
35
37
  * region — it grows to fill the stack and scrolls its own overflow with
36
38
  * no extra style plumbing. Matches `isScrollable` on `LayoutContent`
37
- * and `LayoutPanel`.
39
+ * and `LayoutPanel`. While its content overflows, a `div` or `span` item
40
+ * is a group named by the caller's `aria-label`, or by a translated
41
+ * "Content". Another element keeps its own role.
38
42
  * @default false
39
43
  */
40
44
  isScrollable?: boolean;
@@ -62,7 +66,7 @@ export interface StackItemProps extends BaseProps<HTMLElement> {
62
66
  * </HStack>
63
67
  * ```
64
68
  */
65
- export declare function StackItem({ crossAlignSelf, size, isScrollable, as: element, xstyle, className, style, children, ref, ...props }: StackItemProps): import("react").ReactElement<any, string | import("react").JSXElementConstructor<any>>;
69
+ export declare function StackItem({ crossAlignSelf, size, isScrollable, as: element, xstyle, className, style, children, ref, ...props }: StackItemProps): import("react").ReactElement<any, string | import("react").JSXElementConstructor<any>> | import("react").FunctionComponentElement<import("../hooks/StackItemScrollRegion.js").StackItemScrollRegionProps>;
66
70
  export declare namespace StackItem {
67
71
  var displayName: string;
68
72
  }
@@ -1 +1 @@
1
- {"version":3,"file":"StackItem.d.ts","sourceRoot":"","sources":["../../src/Stack/StackItem.tsx"],"names":[],"mappings":"AAEA;;;;;;;;;GASG;AAEH,OAAO,EAAgB,KAAK,WAAW,EAAE,KAAK,SAAS,EAAW,MAAM,OAAO,CAAC;AAChF,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAE5C,OAAO,EAEL,KAAK,uBAAuB,EAC5B,KAAK,aAAa,EACnB,MAAM,oBAAoB,CAAC;AAU5B,MAAM,WAAW,cAAe,SAAQ,SAAS,CAAC,WAAW,CAAC;IAC5D,wCAAwC;IACxC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IAC7B;;;OAGG;IACH,cAAc,CAAC,EAAE,uBAAuB,CAAC;IAEzC;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,aAAa,CAAC;IAErB;;;;;;;;;OASG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IAEvB;;;OAGG;IACH,EAAE,CAAC,EAAE,WAAW,CAAC;IAEjB;;OAEG;IACH,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,SAAS,CAAC,EACxB,cAAc,EACd,IAAI,EACJ,YAAY,EACZ,EAAE,EAAE,OAAe,EACnB,MAAM,EACN,SAAS,EACT,KAAK,EACL,QAAQ,EACR,GAAG,EACH,GAAG,KAAK,EACT,EAAE,cAAc,0FAqBhB;yBAhCe,SAAS"}
1
+ {"version":3,"file":"StackItem.d.ts","sourceRoot":"","sources":["../../src/Stack/StackItem.tsx"],"names":[],"mappings":"AAEA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAgB,KAAK,WAAW,EAAE,KAAK,SAAS,EAAW,MAAM,OAAO,CAAC;AAChF,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAE5C,OAAO,EAEL,KAAK,uBAAuB,EAC5B,KAAK,aAAa,EACnB,MAAM,oBAAoB,CAAC;AAW5B,MAAM,WAAW,cAAe,SAAQ,SAAS,CAAC,WAAW,CAAC;IAC5D,wCAAwC;IACxC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IAC7B;;;OAGG;IACH,cAAc,CAAC,EAAE,uBAAuB,CAAC;IAEzC;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,aAAa,CAAC;IAErB;;;;;;;;;;;OAWG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IAEvB;;;OAGG;IACH,EAAE,CAAC,EAAE,WAAW,CAAC;IAEjB;;OAEG;IACH,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,SAAS,CAAC,EACxB,cAAc,EACd,IAAI,EACJ,YAAY,EACZ,EAAE,EAAE,OAAe,EACnB,MAAM,EACN,SAAS,EACT,KAAK,EACL,QAAQ,EACR,GAAG,EACH,GAAG,KAAK,EACT,EAAE,cAAc,0MA8BhB;yBAzCe,SAAS"}
@@ -2,9 +2,11 @@
2
2
 
3
3
  /**
4
4
  * @file StackItem.tsx
5
- * @input Uses React, ElementType, stackItem utility
5
+ * @input Uses React, ElementType, stackItem utility, StackItemScrollRegion
6
6
  * @output Exports StackItem polymorphic component and StackItemProps
7
- * @position Layout/Stack component; uses stackItem.stylex.ts
7
+ * @position Layout/Stack component; uses stackItem.stylex.ts. A scrollable
8
+ * `div` or `span` item renders through StackItemScrollRegion, which names
9
+ * it while it scrolls.
8
10
  *
9
11
  * SYNC: When modified, update these files to stay in sync:
10
12
  * - /packages/core/src/Stack/Stack.doc.mjs
@@ -16,6 +18,7 @@ import * as stylex from '@stylexjs/stylex';
16
18
  import { stackItem } from "./stackItem.stylex.js";
17
19
  import { mergeProps } from "../utils/mergeProps.js";
18
20
  import { themeProps } from "../utils/themeProps.js";
21
+ import { StackItemScrollRegion } from "../hooks/StackItemScrollRegion.js";
19
22
  const overflowStyles = {
20
23
  scrollable: {
21
24
  kVQacm: "xysyzu8",
@@ -52,12 +55,22 @@ export function StackItem({
52
55
  crossAlignSelf,
53
56
  size
54
57
  }), isScrollable && overflowStyles.scrollable, xstyle);
55
- return /*#__PURE__*/createElement(element, {
56
- ref: ref,
58
+ const elementProps = {
57
59
  ...mergeProps(themeProps('stack-item', {
58
60
  size
59
61
  }), stylexProps, className, style),
60
62
  ...props
63
+ };
64
+ if (isScrollable && (element === 'div' || element === 'span')) {
65
+ return /*#__PURE__*/createElement(StackItemScrollRegion, {
66
+ as: element,
67
+ ref,
68
+ elementProps
69
+ }, children);
70
+ }
71
+ return /*#__PURE__*/createElement(element, {
72
+ ref: ref,
73
+ ...elementProps
61
74
  }, children);
62
75
  }
63
76
  StackItem.displayName = 'StackItem';
package/dist/astryx.css CHANGED
@@ -1633,7 +1633,7 @@
1633
1633
  .xttggg:is([dir="rtl"] *):not(#\#):not(#\#):not(#\#){transform:translateX(100%)}
1634
1634
  .x1nyx83j:is([dir="rtl"] *):not(#\#):not(#\#):not(#\#){transform:translateX(50%)}
1635
1635
  .x9cjr8z:popover-open:not(#\#):not(#\#):not(#\#){transform:translateY(0)}
1636
- .x1rm9qnc:focus-within:not(#\#):not(#\#):not(#\#){z-index:1}
1636
+ .x1pc0aet:focus-within:not(#\#):not(#\#):not(#\#){z-index:2}
1637
1637
  .xf5goe6.xf5goe6:where(.astryx-input-group > [data-astryx-input-group-item]):focus-visible:not(#\#):not(#\#):not(#\#){z-index:2}
1638
1638
  .x151j4nw:has(input[aria-disabled="true"]:focus-visible):not(#\#):not(#\#):not(#\#){box-shadow:inset 0 0 0 2px var(--color-accent-muted)}
1639
1639
  .xwmge8v:has(> [aria-disabled="true"]:focus-visible):not(#\#):not(#\#):not(#\#){box-shadow:inset 0 0 0 2px var(--color-accent-muted)}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * @file StackItemScrollRegion.tsx
3
+ * @input A scrollable `div` or `span` StackItem's props, ref and children
4
+ * @output StackItemScrollRegion (internal; not re-exported from
5
+ * hooks/index.ts), which renders that element and names it as a group
6
+ * while its content overflows
7
+ * @position Internal client island for StackItem, which itself stays a
8
+ * static render. Only a scrollable `div` or `span` item measures its
9
+ * overflow, on the client; another element keeps its own role.
10
+ *
11
+ * SYNC: When modified, update:
12
+ * - /packages/core/src/Stack/StackItem.tsx
13
+ * - /packages/core/src/serverSafeComponents.test.ts
14
+ */
15
+ import { type AriaRole, type ReactNode, type Ref } from 'react';
16
+ export interface StackItemScrollRegionProps {
17
+ as: 'div' | 'span';
18
+ ref?: Ref<HTMLElement>;
19
+ elementProps: {
20
+ role?: AriaRole;
21
+ 'aria-label'?: string;
22
+ [prop: string]: unknown;
23
+ };
24
+ children?: ReactNode;
25
+ }
26
+ export declare function StackItemScrollRegion({ as: element, ref, elementProps, children, }: StackItemScrollRegionProps): import("react").DetailedReactHTMLElement<{
27
+ ref: import("react").RefCallback<HTMLElement>;
28
+ role: AriaRole | undefined;
29
+ 'aria-label': string | undefined;
30
+ }, HTMLElement>;
31
+ //# sourceMappingURL=StackItemScrollRegion.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"StackItemScrollRegion.d.ts","sourceRoot":"","sources":["../../src/hooks/StackItemScrollRegion.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAgB,KAAK,QAAQ,EAAE,KAAK,SAAS,EAAE,KAAK,GAAG,EAAC,MAAM,OAAO,CAAC;AAI7E,MAAM,WAAW,0BAA0B;IACzC,EAAE,EAAE,KAAK,GAAG,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,GAAG,CAAC,WAAW,CAAC,CAAC;IACvB,YAAY,EAAE;QACZ,IAAI,CAAC,EAAE,QAAQ,CAAC;QAChB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;KACzB,CAAC;IACF,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAED,wBAAgB,qBAAqB,CAAC,EACpC,EAAE,EAAE,OAAO,EACX,GAAG,EACH,YAAY,EACZ,QAAQ,GACT,EAAE,0BAA0B;;;;gBAiB5B"}
@@ -0,0 +1,40 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ /**
6
+ * @file StackItemScrollRegion.tsx
7
+ * @input A scrollable `div` or `span` StackItem's props, ref and children
8
+ * @output StackItemScrollRegion (internal; not re-exported from
9
+ * hooks/index.ts), which renders that element and names it as a group
10
+ * while its content overflows
11
+ * @position Internal client island for StackItem, which itself stays a
12
+ * static render. Only a scrollable `div` or `span` item measures its
13
+ * overflow, on the client; another element keeps its own role.
14
+ *
15
+ * SYNC: When modified, update:
16
+ * - /packages/core/src/Stack/StackItem.tsx
17
+ * - /packages/core/src/serverSafeComponents.test.ts
18
+ */
19
+ import { createElement } from 'react';
20
+ import { useMergedRefs } from "./useMergedRefs.js";
21
+ import { useScrollRegionProps } from "./useScrollRegionProps.js";
22
+ export function StackItemScrollRegion({
23
+ as: element,
24
+ ref,
25
+ elementProps,
26
+ children
27
+ }) {
28
+ const region = useScrollRegionProps({
29
+ isScrollable: true,
30
+ role: elementProps.role,
31
+ label: elementProps['aria-label']
32
+ });
33
+ const mergedRef = useMergedRefs(ref, region.ref);
34
+ return /*#__PURE__*/createElement(element, {
35
+ ...elementProps,
36
+ ref: mergedRef,
37
+ role: region.role,
38
+ 'aria-label': region['aria-label']
39
+ }, children);
40
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * @file useScrollRegionProps.ts
3
+ * @input Whether a region may scroll, and the caller's role and label
4
+ * @output useScrollRegionProps: a ref for the region, and the role and
5
+ * accessible name that the region carries while its content overflows
6
+ * @position Internal hook for LayoutContent, LayoutPanel and StackItem.
7
+ *
8
+ * SYNC: When modified, update:
9
+ * - /packages/core/src/hooks/useScrollRegionProps.test.tsx
10
+ * - /packages/core/src/AppShell/observeMainOverflow.ts
11
+ */
12
+ import { type AriaRole, type RefCallback } from 'react';
13
+ export interface ScrollRegionOptions {
14
+ /** Whether the region may scroll at all (`overflow: auto`). */
15
+ isScrollable: boolean;
16
+ /** The caller's role. It always wins, and takes no default name. */
17
+ role?: AriaRole;
18
+ /** The caller's accessible name. It always wins. */
19
+ label?: string;
20
+ }
21
+ export interface ScrollRegionProps {
22
+ ref: RefCallback<HTMLElement>;
23
+ role: AriaRole | undefined;
24
+ 'aria-label': string | undefined;
25
+ }
26
+ /**
27
+ * The region's ref, role and accessible name. While the region may scroll
28
+ * and its content overflows, it is a group named by the caller's `label`, or
29
+ * by the translated "Content". A caller's `role` is kept with only the
30
+ * caller's `label`: a single main landmark relies on its role alone. When
31
+ * the region does not scroll, the caller's own `role` and `label` pass
32
+ * through unchanged.
33
+ */
34
+ export declare function useScrollRegionProps({ isScrollable, role, label, }: ScrollRegionOptions): ScrollRegionProps;
35
+ //# sourceMappingURL=useScrollRegionProps.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useScrollRegionProps.d.ts","sourceRoot":"","sources":["../../src/hooks/useScrollRegionProps.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;GAUG;AAEH,OAAO,EAAwB,KAAK,QAAQ,EAAE,KAAK,WAAW,EAAC,MAAM,OAAO,CAAC;AAU7E,MAAM,WAAW,mBAAmB;IAClC,+DAA+D;IAC/D,YAAY,EAAE,OAAO,CAAC;IACtB,oEAAoE;IACpE,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB,oDAAoD;IACpD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,iBAAiB;IAChC,GAAG,EAAE,WAAW,CAAC,WAAW,CAAC,CAAC;IAC9B,IAAI,EAAE,QAAQ,GAAG,SAAS,CAAC;IAC3B,YAAY,EAAE,MAAM,GAAG,SAAS,CAAC;CAClC;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,EACnC,YAAY,EACZ,IAAI,EACJ,KAAK,GACN,EAAE,mBAAmB,GAAG,iBAAiB,CAsBzC"}
@@ -0,0 +1,51 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ /**
6
+ * @file useScrollRegionProps.ts
7
+ * @input Whether a region may scroll, and the caller's role and label
8
+ * @output useScrollRegionProps: a ref for the region, and the role and
9
+ * accessible name that the region carries while its content overflows
10
+ * @position Internal hook for LayoutContent, LayoutPanel and StackItem.
11
+ *
12
+ * SYNC: When modified, update:
13
+ * - /packages/core/src/hooks/useScrollRegionProps.test.tsx
14
+ * - /packages/core/src/AppShell/observeMainOverflow.ts
15
+ */
16
+ import { useCallback, useState } from 'react';
17
+ import { registerEnglishFallback, useComponentTranslator } from "../i18n/useComponentTranslator.js";
18
+ import english from "../i18n/generated-english/hooks/useScrollRegionProps.generated.js";
19
+ import { observeMainOverflow } from "../AppShell/observeMainOverflow.js";
20
+ registerEnglishFallback(english);
21
+ /**
22
+ * The region's ref, role and accessible name. While the region may scroll
23
+ * and its content overflows, it is a group named by the caller's `label`, or
24
+ * by the translated "Content". A caller's `role` is kept with only the
25
+ * caller's `label`: a single main landmark relies on its role alone. When
26
+ * the region does not scroll, the caller's own `role` and `label` pass
27
+ * through unchanged.
28
+ */
29
+ export function useScrollRegionProps({
30
+ isScrollable,
31
+ role,
32
+ label
33
+ }) {
34
+ const t = useComponentTranslator();
35
+ const [overflows, setOverflows] = useState(false);
36
+ // A caller's role takes no default name, so its region is not observed.
37
+ const hasRole = role != null;
38
+ const ref = useCallback(element => {
39
+ if (element == null || !isScrollable || hasRole) {
40
+ setOverflows(false);
41
+ return undefined;
42
+ }
43
+ return observeMainOverflow(element, setOverflows);
44
+ }, [hasRole, isScrollable]);
45
+ const isGroup = !hasRole && isScrollable && overflows;
46
+ return {
47
+ ref,
48
+ role: role ?? (isGroup ? 'group' : undefined),
49
+ 'aria-label': label ?? (isGroup ? t('@astryx.scrollRegion.label') : undefined)
50
+ };
51
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * @generated by scripts/generate-i18n-runtime.mjs from packages/core/locales/en.json,
3
+ * holding the English for the catalog keys named in packages/core/src/hooks/useScrollRegionProps.ts.
4
+ * Do not edit by hand. Run `pnpm generate:i18n-runtime` instead.
5
+ */
6
+ declare const english: Readonly<Record<string, string>>;
7
+ export default english;
8
+ //# sourceMappingURL=useScrollRegionProps.generated.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useScrollRegionProps.generated.d.ts","sourceRoot":"","sources":["../../../../src/i18n/generated-english/hooks/useScrollRegionProps.generated.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AAEH,QAAA,MAAM,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAE7C,CAAC;AAEF,eAAe,OAAO,CAAC"}
@@ -0,0 +1,12 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @generated by scripts/generate-i18n-runtime.mjs from packages/core/locales/en.json,
5
+ * holding the English for the catalog keys named in packages/core/src/hooks/useScrollRegionProps.ts.
6
+ * Do not edit by hand. Run `pnpm generate:i18n-runtime` instead.
7
+ */
8
+
9
+ const english = {
10
+ "@astryx.scrollRegion.label": "Content"
11
+ };
12
+ export default english;
@@ -1 +1 @@
1
- {"version":3,"file":"en.generated.d.ts","sourceRoot":"","sources":["../../../src/i18n/generated-locales/en.generated.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AAEH,QAAA,MAAM,cAAc,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAmqBpD,CAAC;AAEF,eAAe,cAAc,CAAC"}
1
+ {"version":3,"file":"en.generated.d.ts","sourceRoot":"","sources":["../../../src/i18n/generated-locales/en.generated.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AAEH,QAAA,MAAM,cAAc,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAoqBpD,CAAC;AAEF,eAAe,cAAc,CAAC"}
@@ -338,6 +338,7 @@ const runtimeCatalog = {
338
338
  '@astryx.banner.expand': 'Expand',
339
339
  '@astryx.button.loading': 'Loading',
340
340
  '@astryx.spinner.loading': 'Loading',
341
+ '@astryx.scrollRegion.label': 'Content',
341
342
  '@astryx.chatComposerDrawer.expand': 'Expand {label}',
342
343
  '@astryx.chatComposerDrawer.collapse': 'Collapse {label}',
343
344
  '@astryx.chatDictationButton.startDictation': 'Start dictation',
@@ -1 +1 @@
1
- {"version":3,"file":"pseudo.generated.d.ts","sourceRoot":"","sources":["../../../src/i18n/generated-locales/pseudo.generated.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AAEH,QAAA,MAAM,cAAc,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CA+qBpD,CAAC;AAEF,eAAe,cAAc,CAAC"}
1
+ {"version":3,"file":"pseudo.generated.d.ts","sourceRoot":"","sources":["../../../src/i18n/generated-locales/pseudo.generated.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AAEH,QAAA,MAAM,cAAc,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAgrBpD,CAAC;AAEF,eAAe,cAAc,CAAC"}
@@ -338,6 +338,7 @@ const runtimeCatalog = {
338
338
  '@astryx.banner.expand': '⟦Éẋþàñð⟧',
339
339
  '@astryx.button.loading': '⟦Łóàðíñĝ⟧',
340
340
  '@astryx.spinner.loading': '⟦Łóàðíñĝ⟧',
341
+ '@astryx.scrollRegion.label': '⟦Çóñţéñţ⟧',
341
342
  '@astryx.chatComposerDrawer.expand': '⟦Éẋþàñð {label}⟧',
342
343
  '@astryx.chatComposerDrawer.collapse': '⟦Çółłàþšé {label}⟧',
343
344
  '@astryx.chatDictationButton.startDictation': '⟦Šţàřţ ðíçţàţíóñ⟧',
package/locales/en.json CHANGED
@@ -1323,6 +1323,10 @@
1323
1323
  "defaultMessage": "Loading",
1324
1324
  "description": "Screen-reader-only default name for a standalone Spinner's role=\"status\" element, used when the consumer supplies neither `aria-label` nor a visible string label; also announced once when a busy input or selector starts loading, in place of its decorative Spinner. Present-progressive form. Kept separate from `button.loading` and `typeahead.loading` — translations may diverge."
1325
1325
  },
1326
+ "@astryx.scrollRegion.label": {
1327
+ "defaultMessage": "Content",
1328
+ "description": "Accessible name of a scrolling content region (LayoutContent, LayoutPanel or StackItem) when the caller gives none. A screen reader announces it with the region's group role while the region scrolls."
1329
+ },
1326
1330
  "@astryx.chatComposerDrawer.expand": {
1327
1331
  "defaultMessage": "Expand {label}",
1328
1332
  "description": "Screen-reader-only label on the ChatComposerDrawer toggle when the drawer is collapsed. `{label}` is the drawer's visible name — example: `Expand Attachments`. Pairs with `collapse`."
@@ -992,6 +992,9 @@
992
992
  "@astryx.spinner.loading": {
993
993
  "defaultMessage": "⟦Łóàðíñĝ⟧"
994
994
  },
995
+ "@astryx.scrollRegion.label": {
996
+ "defaultMessage": "⟦Çóñţéñţ⟧"
997
+ },
995
998
  "@astryx.chatComposerDrawer.expand": {
996
999
  "defaultMessage": "⟦Éẋþàñð {label}⟧"
997
1000
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/core",
3
- "version": "0.7.9-canary.9bd11b3",
3
+ "version": "0.7.9-canary.cb2a34a",
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",
@@ -870,9 +870,9 @@
870
870
  "react-dom": ">=19.0.0"
871
871
  },
872
872
  "devDependencies": {
873
- "@astryxdesign/a11y-spec": "0.7.9-canary.9bd11b3",
874
- "@astryxdesign/cli": "0.7.9-canary.9bd11b3",
875
- "@astryxdesign/interaction-harness": "0.7.9-canary.9bd11b3",
873
+ "@astryxdesign/a11y-spec": "0.7.9-canary.cb2a34a",
874
+ "@astryxdesign/cli": "0.7.9-canary.cb2a34a",
875
+ "@astryxdesign/interaction-harness": "0.7.9-canary.cb2a34a",
876
876
  "@babel/cli": "^7.29.7",
877
877
  "@babel/core": "^7.29.7",
878
878
  "@babel/preset-react": "^7.29.7",
@@ -4,20 +4,23 @@
4
4
 
5
5
  /**
6
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
7
+ * @input A scroll container that holds its content as direct children with
8
+ * no wrapper: AppShell's main region under `height="fill"`, or a scrolling
9
+ * LayoutContent, LayoutPanel or StackItem
9
10
  * @output observeMainOverflow(main, onChange): reports whether main's content
10
11
  * overflows it on either axis, once at the start and on every change, and
11
12
  * returns the function that stops observing
12
- * @position Internal to AppShell and never exported. AppShell makes main a
13
+ * @position Internal and never exported. AppShell makes main a
13
14
  * 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.
15
+ * `component:AppShell` FR4 and PR1). useScrollRegionProps names a
16
+ * scrolling region only while it overflows. Main has no inner content
17
+ * box, so its content grows inside it without resizing it: its direct
18
+ * children's sizes are watched instead.
17
19
  *
18
20
  * SYNC: When modified, update:
19
21
  * - /packages/core/src/AppShell/AppShell.tsx
20
22
  * - /packages/core/src/AppShell/observeMainOverflow.test.ts
23
+ * - /packages/core/src/hooks/useScrollRegionProps.ts
21
24
  */
22
25
 
23
26
  import {
@@ -24,7 +24,7 @@ export const docs = {
24
24
  ],
25
25
  },
26
26
  description:
27
- 'Groups an input with prefix/suffix addons in a visually connected container with shared border and focus ring.',
27
+ 'Groups an input with prefix and suffix addons in one connected row. A focused member stays above its neighbours, so its focus ring shows in full.',
28
28
  props: [
29
29
  {
30
30
  name: 'children',
@@ -59,7 +59,7 @@ export const docs = {
59
59
  {
60
60
  name: 'isOptional',
61
61
  type: 'boolean',
62
- description: 'Show "(optional)" indicator.',
62
+ description: 'Show the "Optional" indicator after the label.',
63
63
  default: 'false',
64
64
  },
65
65
  {
@@ -77,7 +77,8 @@ export const docs = {
77
77
  {
78
78
  name: 'status',
79
79
  type: "{type: 'warning' | 'error' | 'success', message?: string}",
80
- description: 'Status indicator applied to the group border.',
80
+ description:
81
+ 'Validation status. Its message shows below the group. The group border does not change; a theme can style the row from `data-status`.',
81
82
  },
82
83
  {
83
84
  name: 'labelTooltip',
@@ -109,7 +109,8 @@ export interface InputGroupProps extends Omit<
109
109
  size?: InputGroupSize;
110
110
 
111
111
  /**
112
- * Status indicator applied to the group border.
112
+ * Validation status. Its message shows below the group. The group border
113
+ * does not change; a theme can style the row from `data-status`.
113
114
  */
114
115
  status?: InputStatus;
115
116
 
@@ -125,8 +126,8 @@ export interface InputGroupProps extends Omit<
125
126
  }
126
127
 
127
128
  /**
128
- * Groups an input with prefix/suffix addons in a visually connected
129
- * container with shared border and focus ring.
129
+ * Groups an input with prefix and suffix addons in one connected row. A
130
+ * focused member stays above its neighbours, so its focus ring shows in full.
130
131
  *
131
132
  * @example
132
133
  * ```
@@ -96,8 +96,10 @@ export const groupStyles = stylex.create({
96
96
  default: 0,
97
97
  [IS_LAST_ITEM]: radiusVars['--radius-element'],
98
98
  },
99
+ // Input wrappers already sit at z-index 1. A focused member goes one
100
+ // higher, so an unfocused input beside it cannot cover its focus ring.
99
101
  ':focus-within': {
100
- zIndex: 1,
102
+ zIndex: 2,
101
103
  },
102
104
  },
103
105
  });
@@ -30,7 +30,8 @@ export const docs = {
30
30
  {
31
31
  name: 'label',
32
32
  type: 'string',
33
- description: 'Accessible label for the landmark element.',
33
+ description:
34
+ 'Accessible label for the landmark element. While the region scrolls, it also names the scrolling region, which otherwise takes a translated "Content".',
34
35
  },
35
36
  {
36
37
  name: 'role',
@@ -66,7 +67,8 @@ export const docsZh = {
66
67
  {
67
68
  name: 'label',
68
69
  type: 'string',
69
- description: '地标元素的无障碍标签。',
70
+ description:
71
+ '地标元素的无障碍标签。区域滚动时,它也为滚动区域命名;未提供时使用已翻译的“Content”(内容)。',
70
72
  },
71
73
  {
72
74
  name: 'role',
@@ -86,7 +88,8 @@ export const docsDense = {
86
88
  padding: 'Internal padding on spacing scale. Overrides layout container default.',
87
89
  isScrollable:
88
90
  'Enable scrollable overflow. With arithmetic contentWidth values, the scrollport reaches open sides while context-aware insets align children.',
89
- label: 'Accessible label for landmark element.',
91
+ label:
92
+ 'Accessible label for landmark element; also names the region while it scrolls (default "Content").',
90
93
  role: 'ARIA landmark role.',
91
94
  },
92
95
  };
@@ -4,7 +4,7 @@
4
4
 
5
5
  /**
6
6
  * @file LayoutContent.tsx
7
- * @input Uses React StyleX, LayoutSlotsContext
7
+ * @input Uses React StyleX, LayoutSlotsContext, useScrollRegionProps
8
8
  * @output Exports LayoutContent component and LayoutContentProps
9
9
  * @position Scrollable main content area for Layout. Wraps the primary body content
10
10
  * with automatic scroll containment and context-aware padding.
@@ -21,6 +21,8 @@ import {use} from 'react';
21
21
  import * as stylex from '@stylexjs/stylex';
22
22
  import {spacingVars} from '../theme/tokens.stylex';
23
23
  import {LayoutSlotsContext} from './LayoutSlotsContext';
24
+ import {useMergedRefs} from '../hooks/useMergedRefs';
25
+ import {useScrollRegionProps} from '../hooks/useScrollRegionProps';
24
26
  import {mergeProps} from '../utils/mergeProps';
25
27
  import type {SpacingStep} from '../utils/types';
26
28
  import {themeProps} from '../utils/themeProps';
@@ -132,6 +134,8 @@ export interface LayoutContentProps extends BaseProps<HTMLDivElement> {
132
134
  /**
133
135
  * Accessible label for the landmark.
134
136
  * Required when role is set and multiple landmarks of the same type exist.
137
+ * While the content scrolls, it also names the scrolling region, which
138
+ * otherwise takes a translated "Content".
135
139
  */
136
140
  label?: string;
137
141
 
@@ -192,12 +196,14 @@ export function LayoutContent({
192
196
  const {hasHeader, hasFooter, hasStart, hasEnd} = use(LayoutSlotsContext);
193
197
 
194
198
  const isZeroPadding = padding === 0;
199
+ const region = useScrollRegionProps({isScrollable, role, label});
200
+ const mergedRef = useMergedRefs(ref, region.ref);
195
201
 
196
202
  return (
197
203
  <div
198
- ref={ref}
199
- role={role}
200
- aria-label={label}
204
+ ref={mergedRef}
205
+ role={region.role}
206
+ aria-label={region['aria-label']}
201
207
  {...mergeProps(
202
208
  themeProps('layout-content'),
203
209
  stylex.props(
@@ -53,7 +53,8 @@ export const docs = {
53
53
  {
54
54
  name: 'label',
55
55
  type: 'string',
56
- description: 'Accessible label for the landmark element.',
56
+ description:
57
+ 'Accessible label for the landmark element. While the region scrolls, it also names the scrolling region, which otherwise takes a translated "Content".',
57
58
  },
58
59
  {
59
60
  name: 'role',
@@ -106,7 +107,8 @@ export const docsZh = {
106
107
  {
107
108
  name: 'label',
108
109
  type: 'string',
109
- description: '地标元素的无障碍标签。',
110
+ description:
111
+ '地标元素的无障碍标签。区域滚动时,它也为滚动区域命名;未提供时使用已翻译的“Content”(内容)。',
110
112
  },
111
113
  {
112
114
  name: 'role',
@@ -139,7 +141,8 @@ export const docsDense = {
139
141
  padding:
140
142
  'Internal padding on spacing scale. Overrides layout container default.',
141
143
  isScrollable: 'Enable scrollable overflow.',
142
- label: 'Accessible label for landmark element.',
144
+ label:
145
+ 'Accessible label for landmark element; also names the region while it scrolls (default "Content").',
143
146
  role: 'ARIA landmark role.',
144
147
  width:
145
148
  'Panel width. Numbers = pixels, strings as-is. Ignored when resizable provided; hook controls width.',
@@ -4,7 +4,8 @@
4
4
 
5
5
  /**
6
6
  * @file LayoutPanel.tsx
7
- * @input Uses React StyleX, LayoutAreaContext, LayoutSlotsContext
7
+ * @input Uses React StyleX, LayoutAreaContext, LayoutSlotsContext,
8
+ * useScrollRegionProps
8
9
  * @output Exports LayoutPanel component and LayoutPanelProps
9
10
  * @position Sidebar panel for Layout start/end slots. Use for navigation panels,
10
11
  * settings sidebars, detail panels, or any fixed-width side content.
@@ -22,6 +23,8 @@ import * as stylex from '@stylexjs/stylex';
22
23
  import {colorVars, spacingVars} from '../theme/tokens.stylex';
23
24
  import {LayoutAreaContext} from './LayoutAreaContext';
24
25
  import {LayoutSlotsContext} from './LayoutSlotsContext';
26
+ import {useMergedRefs} from '../hooks/useMergedRefs';
27
+ import {useScrollRegionProps} from '../hooks/useScrollRegionProps';
25
28
  import {mergeProps} from '../utils';
26
29
  import type {SizeValue, SpacingStep} from '../utils/types';
27
30
  import type {ResizableProps} from '../Resizable/useResizable';
@@ -144,6 +147,8 @@ export interface LayoutPanelProps extends BaseProps<HTMLDivElement> {
144
147
  /**
145
148
  * Accessible label for the landmark.
146
149
  * Required when role is set and multiple landmarks of the same type exist.
150
+ * While the panel scrolls, it also names the scrolling region, which
151
+ * otherwise takes a translated "Content".
147
152
  */
148
153
  label?: string;
149
154
 
@@ -235,6 +240,8 @@ export function LayoutPanel({
235
240
  // When no divider, collapse spacing for seamless visual flow
236
241
  const shouldCollapseSpacing =
237
242
  !hasDivider && !isZeroPadding && padding == null;
243
+ const region = useScrollRegionProps({isScrollable, role, label});
244
+ const mergedRef = useMergedRefs(ref, region.ref);
238
245
 
239
246
  // Select divider style based on position
240
247
  const dividerStyle = isStartPanel
@@ -252,9 +259,9 @@ export function LayoutPanel({
252
259
 
253
260
  return (
254
261
  <div
255
- ref={ref}
256
- role={role}
257
- aria-label={label}
262
+ ref={mergedRef}
263
+ role={region.role}
264
+ aria-label={region['aria-label']}
258
265
  {...mergeProps(
259
266
  themeProps('layout-panel'),
260
267
  stylex.props(
@@ -524,7 +524,7 @@ export const docs = {
524
524
  name: 'isScrollable',
525
525
  type: 'boolean',
526
526
  description:
527
- 'Enables scrollable overflow (overflow: auto). StackItem already applies the flex min-height/min-width reset, so <StackItem size="fill" isScrollable> is a complete scroll region. Matches isScrollable on LayoutContent and LayoutPanel.',
527
+ 'Enables scrollable overflow (overflow: auto). StackItem already applies the flex min-height/min-width reset, so <StackItem size="fill" isScrollable> is a complete scroll region. Matches isScrollable on LayoutContent and LayoutPanel. While its content overflows, a div or span item is a group named by aria-label, or by a translated "Content"; another element keeps its own role.',
528
528
  default: 'false',
529
529
  },
530
530
  {
@@ -2,9 +2,11 @@
2
2
 
3
3
  /**
4
4
  * @file StackItem.tsx
5
- * @input Uses React, ElementType, stackItem utility
5
+ * @input Uses React, ElementType, stackItem utility, StackItemScrollRegion
6
6
  * @output Exports StackItem polymorphic component and StackItemProps
7
- * @position Layout/Stack component; uses stackItem.stylex.ts
7
+ * @position Layout/Stack component; uses stackItem.stylex.ts. A scrollable
8
+ * `div` or `span` item renders through StackItemScrollRegion, which names
9
+ * it while it scrolls.
8
10
  *
9
11
  * SYNC: When modified, update these files to stay in sync:
10
12
  * - /packages/core/src/Stack/Stack.doc.mjs
@@ -21,6 +23,7 @@ import {
21
23
  } from './stackItem.stylex';
22
24
  import {mergeProps} from '../utils/mergeProps';
23
25
  import {themeProps} from '../utils/themeProps';
26
+ import {StackItemScrollRegion} from '../hooks/StackItemScrollRegion';
24
27
 
25
28
  const overflowStyles = stylex.create({
26
29
  scrollable: {
@@ -53,7 +56,9 @@ export interface StackItemProps extends BaseProps<HTMLElement> {
53
56
  * reset, so `<StackItem size="fill" isScrollable>` is a complete scroll
54
57
  * region — it grows to fill the stack and scrolls its own overflow with
55
58
  * no extra style plumbing. Matches `isScrollable` on `LayoutContent`
56
- * and `LayoutPanel`.
59
+ * and `LayoutPanel`. While its content overflows, a `div` or `span` item
60
+ * is a group named by the caller's `aria-label`, or by a translated
61
+ * "Content". Another element keeps its own role.
57
62
  * @default false
58
63
  */
59
64
  isScrollable?: boolean;
@@ -102,18 +107,27 @@ export function StackItem({
102
107
  xstyle,
103
108
  );
104
109
 
110
+ const elementProps = {
111
+ ...mergeProps(
112
+ themeProps('stack-item', {size}),
113
+ stylexProps,
114
+ className,
115
+ style,
116
+ ),
117
+ ...props,
118
+ };
119
+
120
+ if (isScrollable && (element === 'div' || element === 'span')) {
121
+ return createElement(
122
+ StackItemScrollRegion,
123
+ {as: element, ref, elementProps},
124
+ children,
125
+ );
126
+ }
127
+
105
128
  return createElement(
106
129
  element,
107
- {
108
- ref: ref as Ref<Element>,
109
- ...mergeProps(
110
- themeProps('stack-item', {size}),
111
- stylexProps,
112
- className,
113
- style,
114
- ),
115
- ...props,
116
- },
130
+ {ref: ref as Ref<Element>, ...elementProps},
117
131
  children,
118
132
  );
119
133
  }
@@ -0,0 +1,57 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ /**
6
+ * @file StackItemScrollRegion.tsx
7
+ * @input A scrollable `div` or `span` StackItem's props, ref and children
8
+ * @output StackItemScrollRegion (internal; not re-exported from
9
+ * hooks/index.ts), which renders that element and names it as a group
10
+ * while its content overflows
11
+ * @position Internal client island for StackItem, which itself stays a
12
+ * static render. Only a scrollable `div` or `span` item measures its
13
+ * overflow, on the client; another element keeps its own role.
14
+ *
15
+ * SYNC: When modified, update:
16
+ * - /packages/core/src/Stack/StackItem.tsx
17
+ * - /packages/core/src/serverSafeComponents.test.ts
18
+ */
19
+
20
+ import {createElement, type AriaRole, type ReactNode, type Ref} from 'react';
21
+ import {useMergedRefs} from './useMergedRefs';
22
+ import {useScrollRegionProps} from './useScrollRegionProps';
23
+
24
+ export interface StackItemScrollRegionProps {
25
+ as: 'div' | 'span';
26
+ ref?: Ref<HTMLElement>;
27
+ elementProps: {
28
+ role?: AriaRole;
29
+ 'aria-label'?: string;
30
+ [prop: string]: unknown;
31
+ };
32
+ children?: ReactNode;
33
+ }
34
+
35
+ export function StackItemScrollRegion({
36
+ as: element,
37
+ ref,
38
+ elementProps,
39
+ children,
40
+ }: StackItemScrollRegionProps) {
41
+ const region = useScrollRegionProps({
42
+ isScrollable: true,
43
+ role: elementProps.role,
44
+ label: elementProps['aria-label'],
45
+ });
46
+ const mergedRef = useMergedRefs(ref, region.ref);
47
+ return createElement(
48
+ element,
49
+ {
50
+ ...elementProps,
51
+ ref: mergedRef,
52
+ role: region.role,
53
+ 'aria-label': region['aria-label'],
54
+ },
55
+ children,
56
+ );
57
+ }
@@ -0,0 +1,76 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ /**
6
+ * @file useScrollRegionProps.ts
7
+ * @input Whether a region may scroll, and the caller's role and label
8
+ * @output useScrollRegionProps: a ref for the region, and the role and
9
+ * accessible name that the region carries while its content overflows
10
+ * @position Internal hook for LayoutContent, LayoutPanel and StackItem.
11
+ *
12
+ * SYNC: When modified, update:
13
+ * - /packages/core/src/hooks/useScrollRegionProps.test.tsx
14
+ * - /packages/core/src/AppShell/observeMainOverflow.ts
15
+ */
16
+
17
+ import {useCallback, useState, type AriaRole, type RefCallback} from 'react';
18
+ import {
19
+ registerEnglishFallback,
20
+ useComponentTranslator,
21
+ } from '../i18n/useComponentTranslator';
22
+ import english from '../i18n/generated-english/hooks/useScrollRegionProps.generated';
23
+ import {observeMainOverflow} from '../AppShell/observeMainOverflow';
24
+
25
+ registerEnglishFallback(english);
26
+
27
+ export interface ScrollRegionOptions {
28
+ /** Whether the region may scroll at all (`overflow: auto`). */
29
+ isScrollable: boolean;
30
+ /** The caller's role. It always wins, and takes no default name. */
31
+ role?: AriaRole;
32
+ /** The caller's accessible name. It always wins. */
33
+ label?: string;
34
+ }
35
+
36
+ export interface ScrollRegionProps {
37
+ ref: RefCallback<HTMLElement>;
38
+ role: AriaRole | undefined;
39
+ 'aria-label': string | undefined;
40
+ }
41
+
42
+ /**
43
+ * The region's ref, role and accessible name. While the region may scroll
44
+ * and its content overflows, it is a group named by the caller's `label`, or
45
+ * by the translated "Content". A caller's `role` is kept with only the
46
+ * caller's `label`: a single main landmark relies on its role alone. When
47
+ * the region does not scroll, the caller's own `role` and `label` pass
48
+ * through unchanged.
49
+ */
50
+ export function useScrollRegionProps({
51
+ isScrollable,
52
+ role,
53
+ label,
54
+ }: ScrollRegionOptions): ScrollRegionProps {
55
+ const t = useComponentTranslator();
56
+ const [overflows, setOverflows] = useState(false);
57
+ // A caller's role takes no default name, so its region is not observed.
58
+ const hasRole = role != null;
59
+ const ref = useCallback(
60
+ (element: HTMLElement | null) => {
61
+ if (element == null || !isScrollable || hasRole) {
62
+ setOverflows(false);
63
+ return undefined;
64
+ }
65
+ return observeMainOverflow(element, setOverflows);
66
+ },
67
+ [hasRole, isScrollable],
68
+ );
69
+ const isGroup = !hasRole && isScrollable && overflows;
70
+ return {
71
+ ref,
72
+ role: role ?? (isGroup ? 'group' : undefined),
73
+ 'aria-label':
74
+ label ?? (isGroup ? t('@astryx.scrollRegion.label') : undefined),
75
+ };
76
+ }
@@ -0,0 +1,13 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @generated by scripts/generate-i18n-runtime.mjs from packages/core/locales/en.json,
5
+ * holding the English for the catalog keys named in packages/core/src/hooks/useScrollRegionProps.ts.
6
+ * Do not edit by hand. Run `pnpm generate:i18n-runtime` instead.
7
+ */
8
+
9
+ const english: Readonly<Record<string, string>> = {
10
+ "@astryx.scrollRegion.label": "Content"
11
+ };
12
+
13
+ export default english;
@@ -375,6 +375,7 @@ const runtimeCatalog: Readonly<Record<string, string>> = {
375
375
  '@astryx.banner.expand': 'Expand',
376
376
  '@astryx.button.loading': 'Loading',
377
377
  '@astryx.spinner.loading': 'Loading',
378
+ '@astryx.scrollRegion.label': 'Content',
378
379
  '@astryx.chatComposerDrawer.expand': 'Expand {label}',
379
380
  '@astryx.chatComposerDrawer.collapse': 'Collapse {label}',
380
381
  '@astryx.chatDictationButton.startDictation': 'Start dictation',
@@ -383,6 +383,7 @@ const runtimeCatalog: Readonly<Record<string, string>> = {
383
383
  '@astryx.banner.expand': '⟦Éẋþàñð⟧',
384
384
  '@astryx.button.loading': '⟦Łóàðíñĝ⟧',
385
385
  '@astryx.spinner.loading': '⟦Łóàðíñĝ⟧',
386
+ '@astryx.scrollRegion.label': '⟦Çóñţéñţ⟧',
386
387
  '@astryx.chatComposerDrawer.expand': '⟦Éẋþàñð {label}⟧',
387
388
  '@astryx.chatComposerDrawer.collapse': '⟦Çółłàþšé {label}⟧',
388
389
  '@astryx.chatDictationButton.startDictation': '⟦Šţàřţ ðíçţàţíóñ⟧',