@astryxdesign/core 0.7.6-canary.8d0a847 → 0.7.6-canary.a23abec
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 +6 -19
- package/dist/NavMenu/NavHeadingMenuItem.d.ts.map +1 -1
- package/dist/NavMenu/NavHeadingMenuItem.js +29 -5
- package/dist/TreeList/TreeListItem.d.ts.map +1 -1
- package/dist/TreeList/TreeListItem.js +4 -27
- package/package.json +4 -4
- package/src/Chat/ChatComposer.spec.md +59 -75
- package/src/Item/Item.test.tsx +0 -60
- package/src/Item/Item.tsx +7 -14
- package/src/NavMenu/NavHeadingMenuItem.tsx +39 -5
- package/src/Resizable/Resizable.spec.md +124 -144
- package/src/TreeList/TreeList.test.tsx +0 -26
- package/src/TreeList/TreeListItem.tsx +4 -26
- package/dist/utils/useFocusAcrossSwap.d.ts +0 -19
- package/dist/utils/useFocusAcrossSwap.d.ts.map +0 -1
- package/dist/utils/useFocusAcrossSwap.js +0 -41
- package/src/utils/useFocusAcrossSwap.ts +0 -55
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,7 +34,6 @@ 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";
|
|
38
37
|
import { ItemDescriptionContext } from "./ItemDescriptionContext.js";
|
|
39
38
|
import { ItemMarkerColumnsContext } from "./ItemMarkerColumnsContext.js";
|
|
40
39
|
import { useMergedRefs } from "../hooks/useMergedRefs.js";
|
|
@@ -275,13 +274,6 @@ export function Item({
|
|
|
275
274
|
const pressable = usePressFeedback();
|
|
276
275
|
const columnMarker = use(ItemMarkerColumnsContext);
|
|
277
276
|
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();
|
|
285
277
|
|
|
286
278
|
// Delegation mode: the row is an enlarged click/tap target for a nested
|
|
287
279
|
// control (e.g. a checkbox) that owns the keyboard access and action. The
|
|
@@ -407,17 +399,12 @@ export function Item({
|
|
|
407
399
|
}
|
|
408
400
|
}[!!isInline << 1 | !!isDisabled << 0],
|
|
409
401
|
children: labelAndDescription
|
|
410
|
-
}) : href != null && !isLinkRoot ? /*#__PURE__*/_jsx(
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
} : {
|
|
417
|
-
href,
|
|
418
|
-
target,
|
|
419
|
-
rel
|
|
420
|
-
}),
|
|
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,
|
|
421
408
|
...{
|
|
422
409
|
0: {
|
|
423
410
|
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,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"}
|
|
@@ -11,14 +11,13 @@
|
|
|
11
11
|
* SYNC: When modified, update:
|
|
12
12
|
* - /packages/core/src/NavMenu/index.ts
|
|
13
13
|
*/
|
|
14
|
-
import React, { useCallback } from 'react';
|
|
14
|
+
import React, { useCallback, useRef } 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 } from "../utils/
|
|
21
|
-
import { useFocusAcrossSwap } from "../utils/useFocusAcrossSwap.js";
|
|
20
|
+
import { mergeProps, mergeRefs } from "../utils/index.js";
|
|
22
21
|
import { useNavHeadingMenuContext } from "./NavMenuContext.js";
|
|
23
22
|
import { useLinkComponent } from "../Link/useLinkComponent.js";
|
|
24
23
|
import { themeProps } from "../utils/themeProps.js";
|
|
@@ -115,8 +114,33 @@ export function NavHeadingMenuItem({
|
|
|
115
114
|
const Element = href ? isDisabled ? 'a' : LinkComponent : 'div';
|
|
116
115
|
|
|
117
116
|
// The element type above changes when `isDisabled` flips, so React replaces
|
|
118
|
-
// the row
|
|
119
|
-
|
|
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]);
|
|
120
144
|
return /*#__PURE__*/_jsxs(Element, {
|
|
121
145
|
...restProps,
|
|
122
146
|
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;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"}
|
|
@@ -22,8 +22,7 @@ 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/
|
|
26
|
-
import { useFocusAcrossSwap } from "../utils/useFocusAcrossSwap.js";
|
|
25
|
+
import { mergeProps } from "../utils/index.js";
|
|
27
26
|
import { useLinkComponent } from "../Link/useLinkComponent.js";
|
|
28
27
|
import { TreeListBranches } from "./TreeListBranches.js";
|
|
29
28
|
import { themeProps } from "../utils/themeProps.js";
|
|
@@ -151,8 +150,6 @@ const descriptionSizeStyles = {
|
|
|
151
150
|
// Component
|
|
152
151
|
// =============================================================================
|
|
153
152
|
|
|
154
|
-
/** A plain anchor, for a disabled item whose link has no destination. */
|
|
155
|
-
const DisabledAnchor = 'a';
|
|
156
153
|
export function TreeListItem({
|
|
157
154
|
id,
|
|
158
155
|
label,
|
|
@@ -188,10 +185,6 @@ export function TreeListItem({
|
|
|
188
185
|
const descriptionId = useId();
|
|
189
186
|
const LinkComponent = useLinkComponent();
|
|
190
187
|
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);
|
|
195
188
|
const isInteractive = onClick != null || href != null;
|
|
196
189
|
const handleToggle = useMemo(() => hasChildren && onToggle != null ? e => {
|
|
197
190
|
e.stopPropagation();
|
|
@@ -359,27 +352,11 @@ export function TreeListItem({
|
|
|
359
352
|
className: "x2lah0s x78zum5 x6s0dn4"
|
|
360
353
|
},
|
|
361
354
|
children: startContent
|
|
362
|
-
}), href != null
|
|
363
|
-
|
|
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,
|
|
355
|
+
}), href != null ? /*#__PURE__*/_jsx(LinkComponent, {
|
|
356
|
+
ref: linkRef,
|
|
381
357
|
href: href,
|
|
382
358
|
target: target,
|
|
359
|
+
"aria-disabled": isDisabled || undefined,
|
|
383
360
|
"aria-labelledby": labelId,
|
|
384
361
|
"aria-describedby": description != null ? descriptionId : undefined
|
|
385
362
|
// Roving tabindex lives on the treeitem row; inner action is not a
|
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.a23abec",
|
|
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.a23abec",
|
|
851
|
+
"@astryxdesign/cli": "0.7.6-canary.a23abec",
|
|
852
|
+
"@astryxdesign/interaction-harness": "0.7.6-canary.a23abec",
|
|
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: 5
|
|
4
4
|
kind: component
|
|
5
5
|
id: component:ChatComposer
|
|
6
|
-
authority:
|
|
6
|
+
authority: draft
|
|
7
7
|
archive_reason: null
|
|
8
8
|
superseded_by: null
|
|
9
|
-
approved_by:
|
|
10
|
-
approved_at:
|
|
9
|
+
approved_by: null
|
|
10
|
+
approved_at: null
|
|
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 | 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`. |
|
|
59
59
|
|
|
60
60
|
This table is a review projection; the body below is authoritative.
|
|
61
61
|
|
|
@@ -69,25 +69,13 @@ streaming state, validation policy, and the content placed in each slot.
|
|
|
69
69
|
|
|
70
70
|
## Compatibility and migration
|
|
71
71
|
|
|
72
|
-
- Released default preserved:
|
|
73
|
-
- Compatibility class:
|
|
74
|
-
- Controlled/uncontrolled behavior: a successful submission clears through
|
|
75
|
-
|
|
76
|
-
- Migration decision: none
|
|
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.
|
|
77
76
|
|
|
78
77
|
Consumer migration instructions belong in consumer docs and release notes.
|
|
79
78
|
|
|
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
|
-
|
|
91
79
|
## Ownership boundary
|
|
92
80
|
|
|
93
81
|
**Owns**
|
|
@@ -113,28 +101,31 @@ Consumer migration instructions belong in consumer docs and release notes.
|
|
|
113
101
|
| 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. |
|
|
114
102
|
| 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. |
|
|
115
103
|
| Editing availability | enabled; disabled | Controls whether the draft can be edited or submitted. | Every ChatComposer | enabled | `component:ChatComposer` | stable | Disabled submission is ignored. |
|
|
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`
|
|
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. |
|
|
117
105
|
| Density | compact; balanced; spacious | Selects composer spacing. | Every ChatComposer | balanced | `component:ChatComposer` | stable | Unknown values are rejected by TypeScript. |
|
|
118
106
|
| Elevation | low; none | Selects a raised body or a flat bordered body. | Every ChatComposer | low | `component:ChatComposer` | stable | Unknown values are rejected by TypeScript. |
|
|
119
107
|
| 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. |
|
|
120
108
|
| 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. |
|
|
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 |
|
|
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. |
|
|
122
110
|
|
|
123
111
|
## Behavioral and layout contract
|
|
124
112
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
|
129
|
-
|
|
|
130
|
-
|
|
|
131
|
-
|
|
|
132
|
-
|
|
|
133
|
-
|
|
|
134
|
-
|
|
|
135
|
-
|
|
|
136
|
-
|
|
|
137
|
-
|
|
|
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 |
|
|
138
129
|
|
|
139
130
|
### Allowed variation
|
|
140
131
|
|
|
@@ -166,7 +157,7 @@ Consumer migration instructions belong in consumer docs and release notes.
|
|
|
166
157
|
### Performance and resources
|
|
167
158
|
|
|
168
159
|
- **PR1 — Context stability.** The exported context value SHOULD retain referential stability while none of its public members changes.
|
|
169
|
-
- **PR2 — Focus coordination.** The component
|
|
160
|
+
- **PR2 — Focus coordination.** The component MUST NOT add document listeners or observers to implement body-click focus or modality-specific focus indication.
|
|
170
161
|
|
|
171
162
|
## Accessibility contract
|
|
172
163
|
|
|
@@ -180,18 +171,19 @@ Consumer migration instructions belong in consumer docs and release notes.
|
|
|
180
171
|
|
|
181
172
|
| Anatomy or state | Design requirement | Representation authority | Hierarchy role | Component contract |
|
|
182
173
|
| ------------------- | ------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | -------------- | ------------------ |
|
|
183
|
-
| Composer frame | Groups body and optional status into one compositional shell. |
|
|
174
|
+
| Composer frame | Groups body and optional status into one compositional shell. | observed implementation | container | FR5 |
|
|
184
175
|
| Composer body | Provides the painted rounded input/action surface and elevation. | elevation is shipped; public theme reachability is unsettled | prominent | FR7 |
|
|
185
176
|
| Drawer | Places attachment/context content before the body. | delegated to `component:ChatComposerDrawer` | supporting | FR5 |
|
|
186
|
-
| Header | Places caller actions at start and caller context at end. |
|
|
177
|
+
| Header | Places caller actions at start and caller context at end. | observed implementation | supporting | FR5 |
|
|
187
178
|
| Input | Owns or hosts the editing surface and its focus control. | delegated to `component:ChatComposerInput` or caller custom input | prominent | FR1–FR3, FR8, FR11 |
|
|
188
|
-
| Footer | Places caller actions and the primary action on the final body row. |
|
|
179
|
+
| Footer | Places caller actions and the primary action on the final body row. | observed implementation | supporting | FR4–FR5 |
|
|
189
180
|
| Send or stop action | Presents the current primary operation. | delegated to `component:ChatSendButton` or caller custom send button | prominent | FR1, FR4 |
|
|
190
181
|
| 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 |
|
|
191
182
|
|
|
192
|
-
The public `chat-composer` target
|
|
193
|
-
|
|
194
|
-
|
|
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.
|
|
195
187
|
|
|
196
188
|
## Family and system relationships
|
|
197
189
|
|
|
@@ -201,37 +193,29 @@ yet; the block is optional during migration, so the map is deferred.
|
|
|
201
193
|
- `architecture:component-theming-surface` owns the anatomy-to-target disposition and makes the current body reachability mismatch blocking without deciding its repair shape.
|
|
202
194
|
- `architecture:component-test-sufficiency` owns public-state partitions, negative assertions, observable output, and the exact-head browser boundary.
|
|
203
195
|
- `architecture:react-component-runtime` owns render-derived state and browser/server boundaries.
|
|
204
|
-
- `architecture:knowledge-contracts` keeps consumer syntax in `ChatComposer.doc.mjs` and system behavior in this
|
|
196
|
+
- `architecture:knowledge-contracts` keeps consumer syntax in `ChatComposer.doc.mjs` and observed system behavior in this draft.
|
|
205
197
|
- `spec:AST-020/FR1` carries the current WCAG 2.2 accessibility baseline.
|
|
206
|
-
- `spec:AST-029` owns the
|
|
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.
|
|
207
199
|
|
|
208
200
|
## Verification map
|
|
209
201
|
|
|
210
|
-
| Contract
|
|
211
|
-
|
|
|
212
|
-
| FR1, FR11
|
|
213
|
-
| FR2
|
|
214
|
-
| FR3
|
|
215
|
-
| FR4
|
|
216
|
-
|
|
|
217
|
-
|
|
|
218
|
-
|
|
|
219
|
-
|
|
220
|
-
|
|
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.
|
|
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` |
|
|
231
214
|
|
|
232
215
|
## Decision log
|
|
233
216
|
|
|
234
|
-
|
|
217
|
+
None. This draft records shipped behavior plus corrections already required by
|
|
218
|
+
current shared authority. It does not approve new ChatComposer policy.
|
|
235
219
|
|
|
236
220
|
## Open questions
|
|
237
221
|
|
package/src/Item/Item.test.tsx
CHANGED
|
@@ -29,7 +29,6 @@ import {
|
|
|
29
29
|
} from '@testing-library/react';
|
|
30
30
|
import {rulesDeclaredFor} from '../__tests__/pressState';
|
|
31
31
|
import userEvent from '@testing-library/user-event';
|
|
32
|
-
import {LinkProvider} from '../Link/LinkProvider';
|
|
33
32
|
import {Item} from './Item';
|
|
34
33
|
import {ItemDescriptionContext} from './ItemDescriptionContext';
|
|
35
34
|
import {List, ListItem} from '../List';
|
|
@@ -384,65 +383,6 @@ describe('Item', () => {
|
|
|
384
383
|
expect(screen.getByTestId('item')).toHaveAttribute('aria-disabled', 'true');
|
|
385
384
|
});
|
|
386
385
|
|
|
387
|
-
it("gives a disabled row's link no destination (spec:AST-085 FR2)", () => {
|
|
388
|
-
function RouterLink({ref, ...props}: React.ComponentPropsWithRef<'a'>) {
|
|
389
|
-
return <a ref={ref} data-router-link {...props} />;
|
|
390
|
-
}
|
|
391
|
-
render(
|
|
392
|
-
<LinkProvider component={RouterLink}>
|
|
393
|
-
<Item
|
|
394
|
-
label="Billing"
|
|
395
|
-
href="/billing"
|
|
396
|
-
target="_blank"
|
|
397
|
-
rel="noopener"
|
|
398
|
-
isDisabled
|
|
399
|
-
/>
|
|
400
|
-
<Item label="Profile" href="/profile" />
|
|
401
|
-
</LinkProvider>,
|
|
402
|
-
);
|
|
403
|
-
// No href, target or rel, so a screen reader's click, Enter or a link
|
|
404
|
-
// menu cannot navigate; it keeps the link role and skips the router link.
|
|
405
|
-
const disabled = screen.getByRole('link', {name: 'Billing'});
|
|
406
|
-
expect(disabled).toHaveAttribute('aria-disabled', 'true');
|
|
407
|
-
for (const attribute of ['href', 'target', 'rel', 'data-router-link']) {
|
|
408
|
-
expect(disabled).not.toHaveAttribute(attribute);
|
|
409
|
-
}
|
|
410
|
-
const enabled = screen.getByRole('link', {name: 'Profile'});
|
|
411
|
-
expect(enabled).toHaveAttribute('href', '/profile');
|
|
412
|
-
expect(enabled).toHaveAttribute('data-router-link');
|
|
413
|
-
expect(enabled).not.toHaveAttribute('aria-disabled');
|
|
414
|
-
});
|
|
415
|
-
|
|
416
|
-
it('keeps focus on a focused link row that becomes disabled (spec:AST-085 FR4)', () => {
|
|
417
|
-
const {rerender} = render(<Item label="Billing" href="/billing" />);
|
|
418
|
-
screen.getByRole('link', {name: 'Billing'}).focus();
|
|
419
|
-
rerender(<Item label="Billing" href="/billing" isDisabled />);
|
|
420
|
-
// The disabled row renders a plain anchor in place of the link component;
|
|
421
|
-
// focus moves to it instead of falling to the page.
|
|
422
|
-
const disabled = screen.getByRole('link', {name: 'Billing'});
|
|
423
|
-
expect(disabled).not.toHaveAttribute('href');
|
|
424
|
-
expect(disabled).toHaveFocus();
|
|
425
|
-
rerender(<Item label="Billing" href="/billing" />);
|
|
426
|
-
expect(screen.getByRole('link', {name: 'Billing'})).toHaveFocus();
|
|
427
|
-
});
|
|
428
|
-
|
|
429
|
-
it('does not pull focus back to a row that changes while focus is elsewhere', () => {
|
|
430
|
-
const {rerender} = render(
|
|
431
|
-
<>
|
|
432
|
-
<Item label="Billing" href="/billing" />
|
|
433
|
-
<button type="button">Elsewhere</button>
|
|
434
|
-
</>,
|
|
435
|
-
);
|
|
436
|
-
screen.getByRole('button', {name: 'Elsewhere'}).focus();
|
|
437
|
-
rerender(
|
|
438
|
-
<>
|
|
439
|
-
<Item label="Billing" href="/billing" isDisabled />
|
|
440
|
-
<button type="button">Elsewhere</button>
|
|
441
|
-
</>,
|
|
442
|
-
);
|
|
443
|
-
expect(screen.getByRole('button', {name: 'Elsewhere'})).toHaveFocus();
|
|
444
|
-
});
|
|
445
|
-
|
|
446
386
|
it('disables the invisible button when isDisabled', () => {
|
|
447
387
|
const {container} = render(
|
|
448
388
|
<Item label="Disabled" onClick={() => {}} isDisabled />,
|