@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.
- package/dist/Item/Item.d.ts.map +1 -1
- package/dist/Item/Item.js +19 -6
- package/dist/NavMenu/NavHeadingMenuItem.d.ts.map +1 -1
- package/dist/NavMenu/NavHeadingMenuItem.js +5 -29
- package/dist/TreeList/TreeListItem.d.ts.map +1 -1
- package/dist/TreeList/TreeListItem.js +27 -4
- package/dist/utils/useFocusAcrossSwap.d.ts +19 -0
- package/dist/utils/useFocusAcrossSwap.d.ts.map +1 -0
- package/dist/utils/useFocusAcrossSwap.js +41 -0
- package/package.json +4 -4
- package/src/Chat/ChatComposer.spec.md +75 -59
- package/src/Item/Item.test.tsx +60 -0
- package/src/Item/Item.tsx +14 -7
- package/src/NavMenu/NavHeadingMenuItem.tsx +5 -39
- package/src/Resizable/Resizable.spec.md +144 -124
- package/src/TreeList/TreeList.test.tsx +26 -0
- package/src/TreeList/TreeListItem.tsx +26 -4
- package/src/utils/useFocusAcrossSwap.ts +55 -0
package/dist/Item/Item.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Item.d.ts","sourceRoot":"","sources":["../../src/Item/Item.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,EAOL,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAUf,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;
|
|
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(
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
118
|
-
|
|
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;
|
|
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/
|
|
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
|
|
356
|
-
|
|
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.
|
|
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.
|
|
851
|
-
"@astryxdesign/cli": "0.7.6-canary.
|
|
852
|
-
"@astryxdesign/interaction-harness": "0.7.6-canary.
|
|
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:
|
|
3
|
+
template_version: 7
|
|
4
4
|
kind: component
|
|
5
5
|
id: component:ChatComposer
|
|
6
|
-
authority:
|
|
6
|
+
authority: current
|
|
7
7
|
archive_reason: null
|
|
8
8
|
superseded_by: null
|
|
9
|
-
approved_by:
|
|
10
|
-
approved_at:
|
|
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 |
|
|
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 |
|
|
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
|
|
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
|
|
73
|
-
- Compatibility class:
|
|
74
|
-
- Controlled/uncontrolled behavior:
|
|
75
|
-
|
|
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`
|
|
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 |
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
|
117
|
-
|
|
|
118
|
-
|
|
|
119
|
-
|
|
|
120
|
-
|
|
|
121
|
-
|
|
|
122
|
-
|
|
|
123
|
-
|
|
|
124
|
-
|
|
|
125
|
-
|
|
|
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
|
|
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. |
|
|
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. |
|
|
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. |
|
|
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
|
|
184
|
-
|
|
185
|
-
|
|
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
|
|
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
|
|
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
|
|
203
|
-
|
|
|
204
|
-
| FR1, FR11
|
|
205
|
-
| FR2
|
|
206
|
-
| FR3
|
|
207
|
-
| FR4
|
|
208
|
-
|
|
|
209
|
-
|
|
|
210
|
-
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
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
|
|