@astryxdesign/core 0.7.6-canary.7958bd2 → 0.7.6-canary.8d0a847

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.
@@ -1 +1 @@
1
- {"version":3,"file":"Item.d.ts","sourceRoot":"","sources":["../../src/Item/Item.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"Item.d.ts","sourceRoot":"","sources":["../../src/Item/Item.tsx"],"names":[],"mappings":"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"}
package/dist/Item/Item.js CHANGED
@@ -34,6 +34,7 @@ import "../theme/tokens.stylex.js";
34
34
  import { colorVars, radiusVars, spacingVars, durationVars, easeVars, typeScaleVars } from "../theme/tokens.stylex.js";
35
35
  import { isRenderable } from "../utils/isRenderable.js";
36
36
  import { mergeProps } from "../utils/mergeProps.js";
37
+ import { useFocusAcrossSwap } from "../utils/useFocusAcrossSwap.js";
37
38
  import { ItemDescriptionContext } from "./ItemDescriptionContext.js";
38
39
  import { ItemMarkerColumnsContext } from "./ItemMarkerColumnsContext.js";
39
40
  import { useMergedRefs } from "../hooks/useMergedRefs.js";
@@ -274,6 +275,13 @@ export function Item({
274
275
  const pressable = usePressFeedback();
275
276
  const columnMarker = use(ItemMarkerColumnsContext);
276
277
  const LinkComponent = useLinkComponent();
278
+ // A disabled row's anchor has no destination, so no activation path (a
279
+ // screen reader's click, Enter, a link context menu) can navigate
280
+ // (spec:AST-085 FR2). Like a disabled Link, it keeps the link role and skips
281
+ // the router link component, which may require a live href. The element type
282
+ // then changes with `isDisabled`, so focus follows the swap (FR4).
283
+ const RowAnchor = isDisabled ? 'a' : LinkComponent;
284
+ const anchorRef = useFocusAcrossSwap();
277
285
 
278
286
  // Delegation mode: the row is an enlarged click/tap target for a nested
279
287
  // control (e.g. a checkbox) that owns the keyboard access and action. The
@@ -399,12 +407,17 @@ export function Item({
399
407
  }
400
408
  }[!!isInline << 1 | !!isDisabled << 0],
401
409
  children: labelAndDescription
402
- }) : href != null && !isLinkRoot ? /*#__PURE__*/_jsx(LinkComponent, {
403
- href: href,
404
- target: target,
405
- rel: rel,
406
- "aria-disabled": isDisabled || undefined,
407
- tabIndex: isDisabled ? -1 : undefined,
410
+ }) : href != null && !isLinkRoot ? /*#__PURE__*/_jsx(RowAnchor, {
411
+ ref: anchorRef,
412
+ ...(isDisabled ? {
413
+ role: 'link',
414
+ 'aria-disabled': true,
415
+ tabIndex: -1
416
+ } : {
417
+ href,
418
+ target,
419
+ rel
420
+ }),
408
421
  ...{
409
422
  0: {
410
423
  className: "xmper1u x16khyan x1heor9g x78zum5 xdt5ytf x98rzlu xeuugli x1yc453h x1hl2dhg x1a2a7pz"
@@ -1 +1 @@
1
- {"version":3,"file":"NavHeadingMenuItem.d.ts","sourceRoot":"","sources":["../../src/NavMenu/NavHeadingMenuItem.tsx"],"names":[],"mappings":"AAIA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,EAAsB,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAEjE,OAAO,EAAiB,KAAK,QAAQ,EAAC,MAAM,SAAS,CAAC;AAUtD,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AA+D5C,MAAM,WAAW,uBAAwB,SAAQ,IAAI,CACnD,SAAS,CAAC,WAAW,CAAC,EACtB,SAAS,CACV;IACC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IAC7B,wCAAwC;IACxC,IAAI,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAC;IAC5B,0BAA0B;IAC1B,KAAK,EAAE,SAAS,CAAC;IACjB,4DAA4D;IAC5D,WAAW,CAAC,EAAE,SAAS,CAAC;IACxB,sEAAsE;IACtE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,0CAA0C;IAC1C,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,mDAAmD;IACnD,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,GAAG,EACH,IAAI,EACJ,KAAK,EACL,WAAW,EACX,IAAI,EACJ,OAAO,EACP,UAAkB,EAClB,MAAM,EACN,SAAS,EACT,KAAK,EACL,aAAa,EAAE,MAAM,EACrB,GAAG,SAAS,EACb,EAAE,uBAAuB,qBAwGzB;yBArHe,kBAAkB"}
1
+ {"version":3,"file":"NavHeadingMenuItem.d.ts","sourceRoot":"","sources":["../../src/NavMenu/NavHeadingMenuItem.tsx"],"names":[],"mappings":"AAIA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,EAAc,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAEzD,OAAO,EAAiB,KAAK,QAAQ,EAAC,MAAM,SAAS,CAAC;AAWtD,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AA+D5C,MAAM,WAAW,uBAAwB,SAAQ,IAAI,CACnD,SAAS,CAAC,WAAW,CAAC,EACtB,SAAS,CACV;IACC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IAC7B,wCAAwC;IACxC,IAAI,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAC;IAC5B,0BAA0B;IAC1B,KAAK,EAAE,SAAS,CAAC;IACjB,4DAA4D;IAC5D,WAAW,CAAC,EAAE,SAAS,CAAC;IACxB,sEAAsE;IACtE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,0CAA0C;IAC1C,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,mDAAmD;IACnD,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,GAAG,EACH,IAAI,EACJ,KAAK,EACL,WAAW,EACX,IAAI,EACJ,OAAO,EACP,UAAkB,EAClB,MAAM,EACN,SAAS,EACT,KAAK,EACL,aAAa,EAAE,MAAM,EACrB,GAAG,SAAS,EACb,EAAE,uBAAuB,qBAqEzB;yBAlFe,kBAAkB"}
@@ -11,13 +11,14 @@
11
11
  * SYNC: When modified, update:
12
12
  * - /packages/core/src/NavMenu/index.ts
13
13
  */
14
- import React, { useCallback, useRef } from 'react';
14
+ import React, { useCallback } from 'react';
15
15
  import * as stylex from '@stylexjs/stylex';
16
16
  import { renderIconSlot } from "../Icon/index.js";
17
17
  import { Text } from "../Text/index.js";
18
18
  import "../theme/tokens.stylex.js";
19
19
  import { colorVars, spacingVars, typographyVars, typeScaleVars, radiusVars } from "../theme/tokens.stylex.js";
20
- import { mergeProps, mergeRefs } from "../utils/index.js";
20
+ import { mergeProps } from "../utils/mergeProps.js";
21
+ import { useFocusAcrossSwap } from "../utils/useFocusAcrossSwap.js";
21
22
  import { useNavHeadingMenuContext } from "./NavMenuContext.js";
22
23
  import { useLinkComponent } from "../Link/useLinkComponent.js";
23
24
  import { themeProps } from "../utils/themeProps.js";
@@ -114,33 +115,8 @@ export function NavHeadingMenuItem({
114
115
  const Element = href ? isDisabled ? 'a' : LinkComponent : 'div';
115
116
 
116
117
  // The element type above changes when `isDisabled` flips, so React replaces
117
- // the row. Carry focus over to the replacement when the outgoing node held it
118
- // (spec:AST-085 FR4). The ref cleanup runs before the old node is removed, so
119
- // `activeElement` is still readable there. Only a different node replacing a
120
- // removed one takes focus, and only if focus has fallen to the page: React
121
- // also detaches and re-attaches the same node (for example when an <Activity>
122
- // boundary hides and reveals the row), and neither case may pull focus back
123
- // after the person has moved elsewhere.
124
- const focusedNodeRef = useRef(null);
125
- const itemRef = useCallback(node => {
126
- if (node === null) {
127
- return;
128
- }
129
- const outgoing = focusedNodeRef.current;
130
- focusedNodeRef.current = null;
131
- // Focus only follows the swap when it has fallen to the page. If the
132
- // person moved focus elsewhere in the meantime (for example while an
133
- // <Activity> boundary kept the row hidden), leave it alone.
134
- const focusFell = document.activeElement === null || document.activeElement === document.body;
135
- if (outgoing !== null && outgoing !== node && !outgoing.isConnected && focusFell) {
136
- node.focus();
137
- }
138
- const release = mergeRefs(ref)(node);
139
- return () => {
140
- focusedNodeRef.current = document.activeElement === node ? node : null;
141
- release?.();
142
- };
143
- }, [ref]);
118
+ // the row; focus follows to the replacement (spec:AST-085 FR4).
119
+ const itemRef = useFocusAcrossSwap(ref);
144
120
  return /*#__PURE__*/_jsxs(Element, {
145
121
  ...restProps,
146
122
  ref: itemRef,
@@ -1 +1 @@
1
- {"version":3,"file":"TreeListItem.d.ts","sourceRoot":"","sources":["../../src/TreeList/TreeListItem.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;GAUG;AAEH,OAAO,EAAyB,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAC7D,OAAO,KAAK,MAAM,MAAM,kBAAkB,CAAC;AAoB3C,OAAO,KAAK,EAAC,eAAe,EAAE,eAAe,EAAC,MAAM,iBAAiB,CAAC;AA0RtE,MAAM,WAAW,yBAAyB;IACxC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,KAAK,CAAC,SAAS,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC/B,UAAU,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC7B,OAAO,CAAC,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC;IACxC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,mEAAmE;IACnE,MAAM,CAAC,EAAE,MAAM,CAAC,YAAY,CAAC;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;IAC5B,WAAW,EAAE,OAAO,CAAC;IACrB;;;;;;;;OAQG;IACH,kBAAkB,EAAE,OAAO,CAAC;IAC5B,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,OAAO,CAAC;IAChB,eAAe,EAAE,aAAa,CAAC,OAAO,CAAC,CAAC;IACxC,UAAU,EAAE,OAAO,CAAC;IACpB;;;;OAIG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,OAAO,EAAE,eAAe,CAAC;IACzB;;;;OAIG;IACH,OAAO,EAAE,eAAe,CAAC;IACzB,uEAAuE;IACvE,gBAAgB,CAAC,EAAE,SAAS,CAAC;IAC7B,wEAAwE;IACxE,QAAQ,EAAE,MAAM,CAAC;IACjB,uDAAuD;IACvD,OAAO,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,UAAU,EAAE,OAAO,CAAC;CACrB;AAMD,wBAAgB,YAAY,CAAC,EAC3B,EAAE,EACF,KAAK,EACL,WAAW,EACX,YAAY,EACZ,UAAU,EACV,MAAM,EACN,SAAS,EACT,KAAK,EACL,OAAO,EACP,IAAI,EACJ,MAAM,EACN,UAAkB,EAClB,UAAkB,EAClB,WAAW,EACX,kBAAkB,EAClB,WAAW,EACX,MAAM,EACN,eAAe,EACf,UAAU,EACV,SAAiB,EACjB,QAAQ,EACR,OAAO,EACP,OAAO,EACP,gBAAgB,EAChB,QAAQ,EACR,OAAO,EACP,UAAU,GACX,EAAE,yBAAyB,+BAqT3B;yBAjVe,YAAY"}
1
+ {"version":3,"file":"TreeListItem.d.ts","sourceRoot":"","sources":["../../src/TreeList/TreeListItem.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;GAUG;AAEH,OAAO,EAAyB,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAC7D,OAAO,KAAK,MAAM,MAAM,kBAAkB,CAAC;AAqB3C,OAAO,KAAK,EAAC,eAAe,EAAE,eAAe,EAAC,MAAM,iBAAiB,CAAC;AA0RtE,MAAM,WAAW,yBAAyB;IACxC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,KAAK,CAAC,SAAS,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC/B,UAAU,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC7B,OAAO,CAAC,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC;IACxC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,mEAAmE;IACnE,MAAM,CAAC,EAAE,MAAM,CAAC,YAAY,CAAC;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;IAC5B,WAAW,EAAE,OAAO,CAAC;IACrB;;;;;;;;OAQG;IACH,kBAAkB,EAAE,OAAO,CAAC;IAC5B,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,OAAO,CAAC;IAChB,eAAe,EAAE,aAAa,CAAC,OAAO,CAAC,CAAC;IACxC,UAAU,EAAE,OAAO,CAAC;IACpB;;;;OAIG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,OAAO,EAAE,eAAe,CAAC;IACzB;;;;OAIG;IACH,OAAO,EAAE,eAAe,CAAC;IACzB,uEAAuE;IACvE,gBAAgB,CAAC,EAAE,SAAS,CAAC;IAC7B,wEAAwE;IACxE,QAAQ,EAAE,MAAM,CAAC;IACjB,uDAAuD;IACvD,OAAO,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,UAAU,EAAE,OAAO,CAAC;CACrB;AASD,wBAAgB,YAAY,CAAC,EAC3B,EAAE,EACF,KAAK,EACL,WAAW,EACX,YAAY,EACZ,UAAU,EACV,MAAM,EACN,SAAS,EACT,KAAK,EACL,OAAO,EACP,IAAI,EACJ,MAAM,EACN,UAAkB,EAClB,UAAkB,EAClB,WAAW,EACX,kBAAkB,EAClB,WAAW,EACX,MAAM,EACN,eAAe,EACf,UAAU,EACV,SAAiB,EACjB,QAAQ,EACR,OAAO,EACP,OAAO,EACP,gBAAgB,EAChB,QAAQ,EACR,OAAO,EACP,UAAU,GACX,EAAE,yBAAyB,+BAuU3B;yBAnWe,YAAY"}
@@ -22,7 +22,8 @@ import { interactionOverlayStyles } from "../utils/interactionOverlay.stylex.js"
22
22
  import { usePressFeedback } from "../hooks/usePressFeedback.js";
23
23
  import { hasInteractiveAncestor, useClickableContainer } from "../hooks/useClickableContainer.js";
24
24
  import { Icon } from "../Icon/index.js";
25
- import { mergeProps } from "../utils/index.js";
25
+ import { mergeProps } from "../utils/mergeProps.js";
26
+ import { useFocusAcrossSwap } from "../utils/useFocusAcrossSwap.js";
26
27
  import { useLinkComponent } from "../Link/useLinkComponent.js";
27
28
  import { TreeListBranches } from "./TreeListBranches.js";
28
29
  import { themeProps } from "../utils/themeProps.js";
@@ -150,6 +151,8 @@ const descriptionSizeStyles = {
150
151
  // Component
151
152
  // =============================================================================
152
153
 
154
+ /** A plain anchor, for a disabled item whose link has no destination. */
155
+ const DisabledAnchor = 'a';
153
156
  export function TreeListItem({
154
157
  id,
155
158
  label,
@@ -185,6 +188,10 @@ export function TreeListItem({
185
188
  const descriptionId = useId();
186
189
  const LinkComponent = useLinkComponent();
187
190
  const linkRef = useRef(null);
191
+ // Disabling the item swaps the router link for a plain anchor, so React
192
+ // replaces it; focus on the inner link (after a pointer click) follows the
193
+ // swap instead of falling to the page (spec:AST-085 FR4).
194
+ const anchorRef = useFocusAcrossSwap(linkRef);
188
195
  const isInteractive = onClick != null || href != null;
189
196
  const handleToggle = useMemo(() => hasChildren && onToggle != null ? e => {
190
197
  e.stopPropagation();
@@ -352,11 +359,27 @@ export function TreeListItem({
352
359
  className: "x2lah0s x78zum5 x6s0dn4"
353
360
  },
354
361
  children: startContent
355
- }), href != null ? /*#__PURE__*/_jsx(LinkComponent, {
356
- ref: linkRef,
362
+ }), href != null && isDisabled ?
363
+ /*#__PURE__*/
364
+ // A disabled item's anchor has no destination, so neither the row's
365
+ // forwarded activation nor a screen reader's click can navigate
366
+ // (spec:AST-085 FR2). Like a disabled Link, it keeps the link role and
367
+ // skips the router link component.
368
+ _jsx(DisabledAnchor, {
369
+ ref: anchorRef,
370
+ role: "link",
371
+ "aria-disabled": true,
372
+ "aria-labelledby": labelId,
373
+ "aria-describedby": description != null ? descriptionId : undefined,
374
+ tabIndex: -1,
375
+ ...{
376
+ className: "xmper1u x16khyan xln7xf2 x1heor9g x78zum5 xdt5ytf x98rzlu xeuugli x1yc453h x1hl2dhg x1a2a7pz"
377
+ },
378
+ children: labelAndDescription
379
+ }) : href != null ? /*#__PURE__*/_jsx(LinkComponent, {
380
+ ref: anchorRef,
357
381
  href: href,
358
382
  target: target,
359
- "aria-disabled": isDisabled || undefined,
360
383
  "aria-labelledby": labelId,
361
384
  "aria-describedby": description != null ? descriptionId : undefined
362
385
  // Roving tabindex lives on the treeitem row; inner action is not a
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @file useFocusAcrossSwap.ts
3
+ * @input An optional forwarded ref
4
+ * @output A callback ref that carries focus to a replacement element
5
+ * @position Internal hook for a component whose element type changes with a
6
+ * prop (a link row that renders a plain anchor while disabled). React
7
+ * replaces the element, so focus would fall to the page; spec:AST-085 FR4
8
+ * keeps it on the row.
9
+ *
10
+ * The ref cleanup runs before the old node is removed, so `activeElement` is
11
+ * still readable there. Only a different node replacing a removed one takes
12
+ * focus, and only if focus has fallen to the page: React also detaches and
13
+ * re-attaches the same node (for example when an <Activity> boundary hides and
14
+ * reveals it), and neither case may pull focus back after the person has moved
15
+ * elsewhere.
16
+ */
17
+ import { type Ref } from 'react';
18
+ export declare function useFocusAcrossSwap<T extends HTMLElement>(ref?: Ref<T>): (node: T | null) => (() => void) | undefined;
19
+ //# sourceMappingURL=useFocusAcrossSwap.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useFocusAcrossSwap.d.ts","sourceRoot":"","sources":["../../src/utils/useFocusAcrossSwap.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAsB,KAAK,GAAG,EAAC,MAAM,OAAO,CAAC;AAGpD,wBAAgB,kBAAkB,CAAC,CAAC,SAAS,WAAW,EACtD,GAAG,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,GACX,CAAC,IAAI,EAAE,CAAC,GAAG,IAAI,KAAK,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,CA4B9C"}
@@ -0,0 +1,41 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ /**
6
+ * @file useFocusAcrossSwap.ts
7
+ * @input An optional forwarded ref
8
+ * @output A callback ref that carries focus to a replacement element
9
+ * @position Internal hook for a component whose element type changes with a
10
+ * prop (a link row that renders a plain anchor while disabled). React
11
+ * replaces the element, so focus would fall to the page; spec:AST-085 FR4
12
+ * keeps it on the row.
13
+ *
14
+ * The ref cleanup runs before the old node is removed, so `activeElement` is
15
+ * still readable there. Only a different node replacing a removed one takes
16
+ * focus, and only if focus has fallen to the page: React also detaches and
17
+ * re-attaches the same node (for example when an <Activity> boundary hides and
18
+ * reveals it), and neither case may pull focus back after the person has moved
19
+ * elsewhere.
20
+ */
21
+ import { useCallback, useRef } from 'react';
22
+ import { mergeRefs } from "./mergeRefs.js";
23
+ export function useFocusAcrossSwap(ref) {
24
+ const focusedNodeRef = useRef(null);
25
+ return useCallback(node => {
26
+ if (node === null) {
27
+ return undefined;
28
+ }
29
+ const outgoing = focusedNodeRef.current;
30
+ focusedNodeRef.current = null;
31
+ const focusFell = document.activeElement === null || document.activeElement === document.body;
32
+ if (outgoing !== null && outgoing !== node && !outgoing.isConnected && focusFell) {
33
+ node.focus();
34
+ }
35
+ const release = mergeRefs(ref)(node);
36
+ return () => {
37
+ focusedNodeRef.current = document.activeElement === node ? node : null;
38
+ release?.();
39
+ };
40
+ }, [ref]);
41
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/core",
3
- "version": "0.7.6-canary.7958bd2",
3
+ "version": "0.7.6-canary.8d0a847",
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",
@@ -847,9 +847,9 @@
847
847
  "react-dom": ">=19.0.0"
848
848
  },
849
849
  "devDependencies": {
850
- "@astryxdesign/a11y-spec": "0.7.6-canary.7958bd2",
851
- "@astryxdesign/cli": "0.7.6-canary.7958bd2",
852
- "@astryxdesign/interaction-harness": "0.7.6-canary.7958bd2",
850
+ "@astryxdesign/a11y-spec": "0.7.6-canary.8d0a847",
851
+ "@astryxdesign/cli": "0.7.6-canary.8d0a847",
852
+ "@astryxdesign/interaction-harness": "0.7.6-canary.8d0a847",
853
853
  "@babel/cli": "^7.29.7",
854
854
  "@babel/core": "^7.29.7",
855
855
  "@babel/preset-react": "^7.29.7",
@@ -1,13 +1,13 @@
1
1
  ---
2
2
  schema_version: 3
3
- template_version: 5
3
+ template_version: 7
4
4
  kind: component
5
5
  id: component:ChatComposer
6
- authority: draft
6
+ authority: current
7
7
  archive_reason: null
8
8
  superseded_by: null
9
- approved_by: null
10
- approved_at: null
9
+ approved_by: cixzhang
10
+ approved_at: 2026-10-31
11
11
  owners: [cixzhang]
12
12
  review_triggers: [public-api, behavior, layout, theming, accessibility]
13
13
  verified_by:
@@ -47,15 +47,15 @@ system_specs:
47
47
 
48
48
  ## Contract at a glance
49
49
 
50
- | Area | Contract |
51
- | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
52
- | Public contract | No new API shape or default. The exported context submits its supplied value; the default disabled editor exposes its disabled state; and an explicitly shown Stop action remains operable while message editing is disabled. |
53
- | Behavior | ChatComposer coordinates a controlled or uncontrolled draft, trims a submitted value, clears through the existing value channel, composes caller slots, and keeps the default send/stop action synchronized with context. |
54
- | End-user impact | Screen-reader users can identify the disabled editor, pointer and touch users can stop an interruptible response, and users of a custom editor send the draft that editor supplied instead of stale composer state. |
55
- | Builder impact | None. Existing props, defaults, exports, and slot shapes remain; custom inputs can rely on the published `onSubmit(value)` context operation. |
56
- | Compatibility/readiness | Patch-compatible corrections to released behavior. This observational draft is non-authoritative; body theming, BaseProps ownership, spacious density, and a stop state without `onStop` remain unresolved owner choices. |
57
- | Review checks | Reject ignored context submission arguments, a disabled state hidden from the default textbox, an ancestor that blocks an enabled Stop action, or any change that presents the open questions below as settled policy. |
58
- | Governing rules | `architecture:interaction-modality/INV4, INV7, INV9`; `architecture:public-component-api/INV5–INV6, INV9, INV12`; `architecture:component-theming-surface/INV4, INV6`; `spec:AST-020/FR1`; `spec:AST-029/DEC-4`. |
50
+ | Area | Contract |
51
+ | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
52
+ | Public contract | `ChatComposer` with a controlled or uncontrolled draft (`value`, `onChange`), `onSubmit`, `placeholder`, `isDisabled`, `isStopShown`, `onStop`, `density`, `elevation`, `status`, `statusPosition`, the drawer, header, input, footer, and send slots, and BaseProps; the exported composition context. |
53
+ | Behavior | ChatComposer coordinates a controlled or uncontrolled draft, trims a submitted value, clears through the existing value channel, composes caller slots, and keeps the default send/stop action synchronized with context. |
54
+ | End-user impact | Screen-reader users can identify the disabled editor, pointer and touch users can stop an interruptible response, and users of a custom editor send the draft that editor supplied instead of stale composer state. |
55
+ | Builder impact | None. |
56
+ | Compatibility/readiness | Records shipped behavior; no public change. Two implementation gaps are listed under Current-state impact; four owner questions stay open. Evidence still pending for parts of FR1, FR5–FR7, FR10, and PR1–PR2 (read from source; see the verification map). |
57
+ | Review checks | Reject ignored context submission arguments, a disabled state hidden from the default textbox, an ancestor that blocks an enabled Stop action, or a change that settles an open question without its owner. |
58
+ | Governing rules | `architecture:interaction-modality/INV4, INV7, INV9`; `architecture:public-component-api/INV5–INV6, INV9, INV12`; `architecture:component-theming-surface/INV4, INV6`; `spec:AST-020/FR1`; `spec:AST-029/DEC-4`. |
59
59
 
60
60
  This table is a review projection; the body below is authoritative.
61
61
 
@@ -69,13 +69,25 @@ streaming state, validation policy, and the content placed in each slot.
69
69
 
70
70
  ## Compatibility and migration
71
71
 
72
- - Released default preserved: yes for ordinary editing, submission, layout, and visual defaults.
73
- - Compatibility class: patch-compatible behavioral correction; the public context operation now uses the value its caller supplies, and disabled-plus-stop restores the already enabled Stop path.
74
- - Controlled/uncontrolled behavior: unchanged; a successful submission still clears through `onChange` in controlled mode and local state otherwise.
75
- - Migration decision: `spec:AST-029/DEC-4`; each behavior correction was reproduced by a failing test before implementation changed.
72
+ - Released default preserved: `yes`
73
+ - Compatibility class: records shipped behavior; no public change
74
+ - Controlled/uncontrolled behavior: a successful submission clears through
75
+ `onChange` in controlled mode and local state otherwise
76
+ - Migration decision: none
76
77
 
77
78
  Consumer migration instructions belong in consumer docs and release notes.
78
79
 
80
+ ## Current-state impact
81
+
82
+ - **The public target does not reach the painted body.** The `chat-composer`
83
+ target sits on the outer frame, so a theme cannot select the body's elevation
84
+ and border. `architecture:component-theming-surface` INV4 puts each themable
85
+ part's paint on its target (FR7, OQ1).
86
+ - **Styling inputs split across two elements.** `ref`, DOM props and events,
87
+ `className`, and `style` reach the outer frame while `xstyle` reaches the
88
+ painted body. `architecture:public-component-api` INV6 has styling inputs
89
+ compose on one element (FR10, OQ2).
90
+
79
91
  ## Ownership boundary
80
92
 
81
93
  **Owns**
@@ -101,31 +113,28 @@ Consumer migration instructions belong in consumer docs and release notes.
101
113
  | Submission | input submit; default send; custom context `onSubmit(value)` | Delivers one trimmed, non-empty supplied value and clears the draft through the current channel. | Enabled composer with a non-empty supplied value | no submission | `component:ChatComposer` | stable | Empty values and submissions while disabled are ignored. |
102
114
  | Input composition | default ChatComposerInput; custom React node | Supplies the editing surface and optional registered focus control. | Every ChatComposer | ChatComposerInput | `component:ChatComposer` | stable | A custom input without a registered control gets textarea focus fallback. |
103
115
  | Editing availability | enabled; disabled | Controls whether the draft can be edited or submitted. | Every ChatComposer | enabled | `component:ChatComposer` | stable | Disabled submission is ignored. |
104
- | Primary action | send; stop | Sends the current draft or requests interruption. | Default ChatSendButton or a custom sendButton | send | `component:ChatComposer` | stable | A shown Stop action without `onStop` is currently rendered but inert. |
116
+ | Primary action | send; stop | Sends the current draft or requests interruption. | Default ChatSendButton or a custom sendButton | send | `component:ChatComposer` | stable | A shown Stop action without `onStop` renders but does nothing (OQ4). |
105
117
  | Density | compact; balanced; spacious | Selects composer spacing. | Every ChatComposer | balanced | `component:ChatComposer` | stable | Unknown values are rejected by TypeScript. |
106
118
  | Elevation | low; none | Selects a raised body or a flat bordered body. | Every ChatComposer | low | `component:ChatComposer` | stable | Unknown values are rejected by TypeScript. |
107
119
  | Status | absent; error; warning; top; bottom | Adds announced feedback before or after the body. | Optional status object | absent; bottom when present | `component:ChatComposer` | stable | An omitted message renders the bar and announces nothing. |
108
120
  | Named composition slots | drawer; header actions/context; input; footer/send actions | Lets callers compose supporting controls and information around the draft. | Optional, independently supplied | omitted | `component:ChatComposer` | stable | Omitted slot groups emit no empty header; footer remains for the send action. |
109
- | BaseProps surface | ref; DOM props/events; className/style; xstyle | Extends the component with DOM identity, behavior, and styling. | Every ChatComposer | omitted | `architecture:public-component-api` | stable | These surfaces currently have split root/body ownership; see OQ2. |
121
+ | BaseProps surface | ref; DOM props/events; className/style; xstyle | Extends the component with DOM identity, behavior, and styling. | Every ChatComposer | omitted | `architecture:public-component-api` | stable | Split between the frame and the body (FR10, OQ2). |
110
122
 
111
123
  ## Behavioral and layout contract
112
124
 
113
- Draft requirements identify their basis so observed code is not mistaken for an
114
- intentional decision. A `current` contract contains no unresolved rows.
115
-
116
- | ID | Candidate invariant | Basis | Draft review state |
117
- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ------------------ |
118
- | FR1 | Controlled mode MUST report changes through `onChange`; uncontrolled mode MUST retain them locally. Every successful submit MUST trim and deliver the value supplied to that operation, then clear through the same value channel. | public types and docs; `architecture:public-component-api/INV3, INV9, INV12`; focused tests | settled |
119
- | FR2 | The default editor MUST serialize its current text/tokens, submit Enter without Shift, preserve Shift+Enter, defer during IME composition, and allow a caller key handler to prevent the default operation. | implementation, docs, and focused tests | verify |
120
- | FR3 | `isDisabled` MUST prevent draft edits and submissions and MUST expose `aria-disabled="true"` on the default textbox; removing the prop MUST remove that state. | `spec:AST-020/FR1` (WCAG 2.2 4.1.2); `architecture:interaction-modality/INV5`; red/green focused test | settled |
121
- | FR4 | When `isStopShown` is true, the default primary action MUST remain an enabled, named Stop button and call `onStop`; if editing is also disabled, ancestor styling MUST NOT block its pointer, touch, or keyboard activation. | `architecture:interaction-modality/INV4, INV7, INV9`; red/green focused and browser-story tests | settled |
122
- | FR5 | Omitted header slots MUST omit the header row; the drawer precedes the body; input fills the body between header and footer; footer actions precede send actions and the primary action; status follows `statusPosition`. | implementation, props, stories, and blocks | verify |
123
- | FR6 | Compact currently reduces body padding and gap. Balanced and spacious currently use the same body spacing. Whether spacious needs distinct output MUST remain unresolved until a component/design owner decides it. | implementation and published union; no current owner selects distinct spacious geometry | human decision |
124
- | FR7 | `low` currently applies low rest and medium hover/focus-within elevation; `none` applies no shadow and a border. Public theme reachability MUST cover the painting body or the public target/model MUST change through an approved compatibility path. | implementation; `architecture:component-theming-surface/INV4, INV6`; existing public issue | human decision |
125
- | FR8 | Keyboard focus entering the registered/default editor MUST add the shared focus indication to the body; pointer focus MUST not add that extra ring. Clicking non-interactive body space MUST focus the registered control, then a textarea fallback. | implementation; `architecture:interaction-modality/INV1–INV3, INV9`; focused tests | verify |
126
- | FR9 | Error status is announced assertively and warning status politely through the shared announcement channel, once per occasion: one state transition, which a rerender or an update to the same value does not repeat (`spec:AST-086` FR6); the status bar itself carries no live role. Both pair a semantic icon with color and can render above or below the body. | implementation, docs, and stories; `spec:AST-020/FR1`; `spec:AST-086/FR2, FR5, FR6` | verify |
127
- | FR10 | BaseProps currently split across elements: ref, DOM props/events, `className`, and `style` reach the outer frame while `xstyle` reaches the painted body. The owning element MUST remain unresolved pending compatibility review. | implementation; `architecture:public-component-api/INV5–INV6, INV8` | human decision |
128
- | FR11 | The exported context MUST expose the current value, change and submit operations, placeholder, disabled/stop/send state, stop callback, and mutable input focus control to custom composition. | public export, types, docs, stories, and focused tests; `architecture:public-component-api/INV12` | verify |
125
+ | ID | Invariant | Basis |
126
+ | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- |
127
+ | FR1 | Controlled mode MUST report changes through `onChange`; uncontrolled mode MUST retain them locally. Every successful submit MUST trim and deliver the value supplied to that operation, then clear through the same value channel. | public types and docs; `architecture:public-component-api/INV3, INV9, INV12`; focused tests |
128
+ | FR2 | The default editor MUST serialize its current text/tokens, submit Enter without Shift, preserve Shift+Enter, defer during IME composition, and allow a caller key handler to prevent the default operation. | implementation, docs, and focused tests |
129
+ | FR3 | `isDisabled` MUST prevent draft edits and submissions and MUST expose `aria-disabled="true"` on the default textbox; removing the prop MUST remove that state. | `spec:AST-020/FR1` (WCAG 2.2 4.1.2); `architecture:interaction-modality/INV5`; red/green focused test |
130
+ | FR4 | When `isStopShown` is true, the default primary action MUST remain an enabled, named Stop button and call `onStop`; if editing is also disabled, ancestor styling MUST NOT block its pointer, touch, or keyboard activation. | `architecture:interaction-modality/INV4, INV7, INV9`; red/green focused and browser-story tests |
131
+ | FR5 | Omitted header slots MUST omit the header row; the drawer precedes the body; input fills the body between header and footer; footer actions precede send actions and the primary action; status follows `statusPosition`. | implementation, props, stories, and blocks |
132
+ | FR6 | `compact` reduces body padding and gap; `balanced` and `spacious` produce the same body spacing. Whether `spacious` should differ is open (OQ3). | implementation and published union; no current owner selects distinct spacious geometry |
133
+ | FR7 | `low` applies low rest and medium hover and focus-within elevation; `none` applies no shadow and a border. Implementation gap: the public target does not reach the painted body (Current-state impact; OQ1). | implementation; `architecture:component-theming-surface/INV4, INV6`; the `chat-composer` target sits on the frame (Current-state impact) |
134
+ | FR8 | Keyboard focus entering the registered/default editor MUST add the shared focus indication to the body; pointer focus MUST not add that extra ring. Clicking non-interactive body space MUST focus the registered control, then a textarea fallback. | implementation; `architecture:interaction-modality/INV1–INV3, INV9`; focused tests |
135
+ | FR9 | Error status is announced assertively and warning status politely through the shared announcement channel, once per occasion: one state transition, which a rerender or an update to the same value does not repeat (`spec:AST-086` FR6); the status bar itself carries no live role. Both pair a semantic icon with color and can render above or below the body. | implementation, docs, and stories; `spec:AST-020/FR1`; `spec:AST-086/FR2, FR5, FR6` |
136
+ | FR10 | `ref`, DOM props and events, `className`, and `style` reach the outer frame; `xstyle` reaches the painted body. Implementation gap: the styling inputs split across two elements (Current-state impact; OQ2). | implementation; `architecture:public-component-api/INV5–INV6, INV8` |
137
+ | FR11 | The exported context MUST expose the current value, change and submit operations, placeholder, disabled/stop/send state, stop callback, and mutable input focus control to custom composition. | public export, types, docs, stories, and focused tests; `architecture:public-component-api/INV12` |
129
138
 
130
139
  ### Allowed variation
131
140
 
@@ -157,7 +166,7 @@ intentional decision. A `current` contract contains no unresolved rows.
157
166
  ### Performance and resources
158
167
 
159
168
  - **PR1 — Context stability.** The exported context value SHOULD retain referential stability while none of its public members changes.
160
- - **PR2 — Focus coordination.** The component MUST NOT add document listeners or observers to implement body-click focus or modality-specific focus indication.
169
+ - **PR2 — Focus coordination.** The component adds no document listener or observer of its own for body-click focus or focus indication; it mounts the shared interaction-modality tracker, which adds document-level `pointerdown` and `keydown` listeners (`architecture:interaction-modality`).
161
170
 
162
171
  ## Accessibility contract
163
172
 
@@ -171,19 +180,18 @@ intentional decision. A `current` contract contains no unresolved rows.
171
180
 
172
181
  | Anatomy or state | Design requirement | Representation authority | Hierarchy role | Component contract |
173
182
  | ------------------- | ------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | -------------- | ------------------ |
174
- | Composer frame | Groups body and optional status into one compositional shell. | observed implementation | container | FR5 |
183
+ | Composer frame | Groups body and optional status into one compositional shell. | Current source | container | FR5 |
175
184
  | Composer body | Provides the painted rounded input/action surface and elevation. | elevation is shipped; public theme reachability is unsettled | prominent | FR7 |
176
185
  | Drawer | Places attachment/context content before the body. | delegated to `component:ChatComposerDrawer` | supporting | FR5 |
177
- | Header | Places caller actions at start and caller context at end. | observed implementation | supporting | FR5 |
186
+ | Header | Places caller actions at start and caller context at end. | Current source | supporting | FR5 |
178
187
  | Input | Owns or hosts the editing surface and its focus control. | delegated to `component:ChatComposerInput` or caller custom input | prominent | FR1–FR3, FR8, FR11 |
179
- | Footer | Places caller actions and the primary action on the final body row. | observed implementation | supporting | FR4–FR5 |
188
+ | Footer | Places caller actions and the primary action on the final body row. | Current source | supporting | FR4–FR5 |
180
189
  | Send or stop action | Presents the current primary operation. | delegated to `component:ChatSendButton` or caller custom send button | prominent | FR1, FR4 |
181
190
  | Status message | Adds error or warning feedback before or after the body. | shared-channel announcement and color are shipped; separate public theme reachability is not asserted here | supporting | FR5, FR9, AR5 |
182
191
 
183
- The aggregate Chat record currently owns the public `chat-composer` target. That
184
- target sits on the frame and cannot select the body's non-inherited elevation and
185
- border properties. This draft records the reachability gap without opting into a
186
- machine theming map, selecting a new target name, or moving the released target.
192
+ The public `chat-composer` target sits on the frame and cannot select the body's
193
+ elevation and border (Current-state impact). No anatomy-theming map is recorded
194
+ yet; the block is optional during migration, so the map is deferred.
187
195
 
188
196
  ## Family and system relationships
189
197
 
@@ -193,29 +201,37 @@ machine theming map, selecting a new target name, or moving the released target.
193
201
  - `architecture:component-theming-surface` owns the anatomy-to-target disposition and makes the current body reachability mismatch blocking without deciding its repair shape.
194
202
  - `architecture:component-test-sufficiency` owns public-state partitions, negative assertions, observable output, and the exact-head browser boundary.
195
203
  - `architecture:react-component-runtime` owns render-derived state and browser/server boundaries.
196
- - `architecture:knowledge-contracts` keeps consumer syntax in `ChatComposer.doc.mjs` and observed system behavior in this draft.
204
+ - `architecture:knowledge-contracts` keeps consumer syntax in `ChatComposer.doc.mjs` and system behavior in this record.
197
205
  - `spec:AST-020/FR1` carries the current WCAG 2.2 accessibility baseline.
198
- - `spec:AST-029/DEC-4` owns red-before-green behavior remediation; the broader Night Watch contract owns this observational backfill, finite evidence inventory, and fail-closed readiness report.
206
+ - `spec:AST-029` owns the audit evidence inventory this record's verification map follows.
199
207
 
200
208
  ## Verification map
201
209
 
202
- | Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
203
- | ------------ | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------- | ----------------------------------- |
204
- | FR1, FR11 | `ChatComposer.test.tsx`; `ChatSendButton.test.tsx`; custom-input stories | controlled/uncontrolled, context-supplied value, default/custom send | Ignoring the context argument submits stale state; default Send passes no draft; clearing skips the active channel. | `audit:ChatComposer/api-behavior` |
205
- | FR2 | `ChatComposerInput.test.tsx` | Enter, Shift+Enter, composing, cancelled keydown, text and token serialization | A key path sends during IME, loses newline behavior, bypasses caller cancellation, or emits the wrong string. | `audit:ChatComposer/input-behavior` |
206
- | FR3 | red/green disabled-state assertion; focused input tests; exact-head axe | disabled and re-enabled default textbox | The textbox loses its ARIA state, remains editable, or accepts a disabled submit. | `audit:ChatComposer/accessibility` |
207
- | FR4 | red/green pointer assertion; disabled-streaming Storybook play; exact-head test-ui | enabled send, shown Stop, disabled-plus-shown Stop | An ancestor blocks pointer hit-testing, Stop becomes native-disabled, or `onStop` does not fire. | `audit:ChatComposer/modality` |
208
- | FR5, FR9 | ChatComposer stories, CLI block examples, production Storybook build | omitted/full slots, attachments, top/bottom status, error/warning | Slot order changes, an empty header appears, or status semantics and placement disagree. | `audit:ChatComposer/composition` |
209
- | FR6 | source and future owner-selected density comparison | compact, balanced, spacious | The audit invents distinct spacious styling without authority or silently changes one public density. | `audit:ChatComposer/design-gap` |
210
- | FR7 | elevation focused test; flat story; current theming guards; owner review pending | low rest/hover/focus, none, public target | Elevation output disappears or the reachability gap is represented as solved without an approved target mapping. | `audit:ChatComposer/theming` |
211
- | FR8, AR4 | `ChatComposer.test.tsx`; exact-head browser/visual gates | keyboard, pointer, programmatic focus, internal action, body click | Pointer focus gains the keyboard ring, editor keyboard focus loses it, or body click bypasses the registered control. | `audit:ChatComposer/focus` |
212
- | FR10 | source/DOM ownership review; owner decision pending | ref, DOM props, className/style, xstyle | A compatibility-affecting root/body move is made without deciding one public owner. | `audit:ChatComposer/api-gap` |
213
- | narrow state | disabled-streaming mobile Storybook fixture and exact-head test-ui | 320px viewport with default input and Stop | The composer widens its viewport or the primary action becomes unreachable. | `audit:ChatComposer/responsive` |
210
+ | Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
211
+ | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | ----------------------------------- |
212
+ | FR1, FR11 | `ChatComposer.test.tsx` input composition tests | context-supplied value, custom input reading value, change, and disabled | A context submission that ignores its argument, or a context member a custom input cannot read, fails. | `audit:ChatComposer/api-behavior` |
213
+ | FR2, AR3 | `ChatComposerInput.test.tsx` | Enter, Shift+Enter, composing, cancelled keydown | A key path that sends during composition or loses a newline fails. | `audit:ChatComposer/input-behavior` |
214
+ | FR3, AR1 | `ChatComposer.test.tsx` "exposes disabled state on the default textbox and clears it when enabled" | disabled and re-enabled default textbox | A textbox that loses or keeps its disabled state wrongly fails. | `audit:ChatComposer/accessibility` |
215
+ | FR4, AR2 | `ChatComposer.test.tsx` "keeps the Stop action pointer-operable while editing is disabled"; the `DisabledStreamingNarrow` story play in `ChatComposer.stories.tsx` | disabled editing with Stop shown; a narrow viewport with no horizontal overflow | A Stop action an ancestor blocks, or one that does not call `onStop`, fails. | `audit:ChatComposer/modality` |
216
+ | FR7 | `ChatComposer.test.tsx` elevation tests | `low` default, `none` | Levels that share a body class, or a default other than `low`, fail. | `audit:ChatComposer/theming` |
217
+ | FR8, AR4 | `ChatComposer.test.tsx` focus indication tests and custom-input focus tests | keyboard, pointer, programmatic after each, internal button, body click | A ring on pointer focus, a missing ring on keyboard focus, or a body click that skips the control fails. | `audit:ChatComposer/focus` |
218
+ | FR9, AR5 | `ChatComposer.test.tsx` status announcement tests | error, warning, StrictMode mount, repeat after clear, no message, page load | A live role on the bar, a repeated or missing announcement, or the wrong urgency fails. | `audit:ChatComposer/accessibility` |
219
+
220
+ Read from source, with no focused test yet:
221
+
222
+ - FR1: controlled and uncontrolled draft storage, trimming, rejecting an empty
223
+ or disabled submission, and clearing through the active channel.
224
+ - FR5: slot order, the omitted header row, and status placement above or below
225
+ the body.
226
+ - FR6: the density spacing.
227
+ - FR7: the shadow and border values for each elevation.
228
+ - FR10: where each BaseProps input lands.
229
+ - PR1–PR2: context stability, and no document listener or observer of the
230
+ component's own.
214
231
 
215
232
  ## Decision log
216
233
 
217
- None. This draft records shipped behavior plus corrections already required by
218
- current shared authority. It does not approve new ChatComposer policy.
234
+ No component-local decision is recorded.
219
235
 
220
236
  ## Open questions
221
237