@astryxdesign/core 0.7.4 → 0.7.5-canary.1093026
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/Tokenizer/Tokenizer.d.ts.map +1 -1
- package/dist/Tokenizer/Tokenizer.js +44 -40
- package/dist/Tokenizer/useEndLaneReserve.d.ts +3 -2
- package/dist/Tokenizer/useEndLaneReserve.d.ts.map +1 -1
- package/dist/Tokenizer/useEndLaneReserve.js +3 -2
- package/dist/astryx.css +6 -13
- package/package.json +4 -4
- package/src/Badge/Badge.doc.mjs +2 -1
- package/src/Icon/Icon.spec.md +40 -29
- package/src/Tokenizer/Tokenizer.test.tsx +26 -8
- package/src/Tokenizer/Tokenizer.tsx +65 -72
- package/src/Tokenizer/__tests__/TokenizerEndLane.a11y.chromium.spec.ts +471 -0
- package/src/Tokenizer/useEndLaneReserve.ts +3 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Tokenizer.d.ts","sourceRoot":"","sources":["../../src/Tokenizer/Tokenizer.tsx"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"Tokenizer.d.ts","sourceRoot":"","sources":["../../src/Tokenizer/Tokenizer.tsx"],"names":[],"mappings":"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"}
|
|
@@ -67,11 +67,8 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
67
67
|
// because the input's reserve is derived from the same value.
|
|
68
68
|
const END_LANE_INSET = spacingVars['--spacing-2'];
|
|
69
69
|
const TOKEN_ROW_INSET = `calc(${spacingVars['--spacing-1']} - 1px)`;
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
const INPUT_CLEAR_COARSE_POINTER_OUTSET = '2px';
|
|
73
|
-
// Kept in sync with useEndLaneReserve: inputCompact reads the custom property
|
|
74
|
-
// directly so its empty-query pseudo-class can suppress the inner reserve.
|
|
70
|
+
// Kept in sync with useEndLaneReserve: wrapperWithTokens reads the custom
|
|
71
|
+
// property directly, to keep token rows and the input short of the lane.
|
|
75
72
|
const END_LANE_RESERVE_VAR = '--_tokenizer-end-lane-reserve';
|
|
76
73
|
const styles = {
|
|
77
74
|
wrapper: {
|
|
@@ -85,12 +82,14 @@ const styles = {
|
|
|
85
82
|
wrapperWithTokens: {
|
|
86
83
|
k8WAf4: "x1x7cmac",
|
|
87
84
|
kg3NbH: "xx2oswg",
|
|
85
|
+
kwRFfy: "xx0flbe",
|
|
88
86
|
khm7nJ: "xajebq8",
|
|
89
87
|
$$css: true
|
|
90
88
|
},
|
|
91
89
|
token: {
|
|
92
90
|
k1xSpc: "x78zum5",
|
|
93
91
|
kmuXW: "x2lah0s",
|
|
92
|
+
ks0D6T: "x193iq5w",
|
|
94
93
|
kVAEAm: "x1n2onr6",
|
|
95
94
|
kY2c9j: "x1vjfegm",
|
|
96
95
|
$$css: true
|
|
@@ -105,6 +104,11 @@ const styles = {
|
|
|
105
104
|
kY2c9j: "x1vjfegm",
|
|
106
105
|
$$css: true
|
|
107
106
|
},
|
|
107
|
+
idleSpinner: {
|
|
108
|
+
k33iCy: "xlshs6z",
|
|
109
|
+
kCygrm: "xdps06",
|
|
110
|
+
$$css: true
|
|
111
|
+
},
|
|
108
112
|
inputAtMax: {
|
|
109
113
|
kzqmXN: "xnalus7",
|
|
110
114
|
k7Eaqz: "xeuugli",
|
|
@@ -115,16 +119,15 @@ const styles = {
|
|
|
115
119
|
$$css: true
|
|
116
120
|
},
|
|
117
121
|
inputCompact: {
|
|
118
|
-
kVAEAm: "x1n2onr6
|
|
122
|
+
kVAEAm: "x1n2onr6",
|
|
119
123
|
kB7OPa: "x9f619",
|
|
120
|
-
kLqNvP: "xhi6v0a x1cs7wtd",
|
|
121
124
|
kY2c9j: "x1ja2u2z",
|
|
122
|
-
k7Eaqz: "x1fns5xo
|
|
123
|
-
kUk6DE: "xfoc8du
|
|
124
|
-
kzqmXN: "xnalus7
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
125
|
+
k7Eaqz: "x1fns5xo x14dcwvw",
|
|
126
|
+
kUk6DE: "xfoc8du x1q9bidn",
|
|
127
|
+
kzqmXN: "xnalus7",
|
|
128
|
+
keTefX: "x1lziwak x1rq9t4t",
|
|
129
|
+
kZCmMZ: "x2a0j69 x1jwxvv4",
|
|
130
|
+
kwRFfy: "xyri2b",
|
|
128
131
|
$$css: true
|
|
129
132
|
},
|
|
130
133
|
truncatedWrapper: {
|
|
@@ -168,23 +171,6 @@ const endSectionSizeStyles = {
|
|
|
168
171
|
$$css: true
|
|
169
172
|
}
|
|
170
173
|
};
|
|
171
|
-
const compactInputSizeStyles = {
|
|
172
|
-
sm: {
|
|
173
|
-
k87sOh: "x80663w x1a21zuh",
|
|
174
|
-
k3aq6I: "x1c071of xv6p7n0",
|
|
175
|
-
$$css: true
|
|
176
|
-
},
|
|
177
|
-
md: {
|
|
178
|
-
k87sOh: "x80663w x811pv0",
|
|
179
|
-
k3aq6I: "x1c071of xv6p7n0",
|
|
180
|
-
$$css: true
|
|
181
|
-
},
|
|
182
|
-
lg: {
|
|
183
|
-
k87sOh: "x80663w x1o13q2w",
|
|
184
|
-
k3aq6I: "x1c071of xv6p7n0",
|
|
185
|
-
$$css: true
|
|
186
|
-
}
|
|
187
|
-
};
|
|
188
174
|
const truncatedSizeStyles = {
|
|
189
175
|
sm: {
|
|
190
176
|
kZKoxP: "x6k0iem",
|
|
@@ -271,6 +257,13 @@ const CREATABLE_ID_PREFIX = '__xds_create__';
|
|
|
271
257
|
*
|
|
272
258
|
* Renders nothing when the lane would be empty, so `useEndLaneReserve` sees no
|
|
273
259
|
* element, publishes no width, and the input keeps its full content box.
|
|
260
|
+
*
|
|
261
|
+
* With `hasBusySlot` the Spinner stays mounted while idle, unrendered, not
|
|
262
|
+
* animating and out of the accessibility tree, so its box keeps the themed
|
|
263
|
+
* Spinner's own size.
|
|
264
|
+
* Token rows reserve the lane's measured width, so a Spinner that came and
|
|
265
|
+
* went with the search would re-wrap them, and widen a content-sized field,
|
|
266
|
+
* on every search.
|
|
274
267
|
*/
|
|
275
268
|
function EndLane({
|
|
276
269
|
lane,
|
|
@@ -279,6 +272,7 @@ function EndLane({
|
|
|
279
272
|
loadingLabel,
|
|
280
273
|
anchorRef,
|
|
281
274
|
hasStaticContent,
|
|
275
|
+
hasBusySlot,
|
|
282
276
|
children
|
|
283
277
|
}) {
|
|
284
278
|
const isBusy = useIsBusy(lane);
|
|
@@ -286,17 +280,22 @@ function EndLane({
|
|
|
286
280
|
// live region; the field announces each search's busy start instead
|
|
287
281
|
// (spec:AST-086 FR8).
|
|
288
282
|
useBusyAnnouncement(isBusy, loadingLabel, anchorRef);
|
|
289
|
-
if (!isBusy && !hasStaticContent) {
|
|
283
|
+
if (!isBusy && !hasStaticContent && !hasBusySlot) {
|
|
290
284
|
return null;
|
|
291
285
|
}
|
|
292
286
|
return /*#__PURE__*/_jsxs("div", {
|
|
293
287
|
ref: laneRef,
|
|
294
288
|
...stylex.props(laneXStyle),
|
|
295
|
-
children: [isBusy && /*#__PURE__*/_jsx(Spinner
|
|
289
|
+
children: [(isBusy || hasBusySlot) && /*#__PURE__*/_jsx(Spinner
|
|
290
|
+
// Remounts when a search starts, so the ring syncs its phase on
|
|
291
|
+
// mount exactly as a newly shown Spinner did before.
|
|
292
|
+
, {
|
|
296
293
|
size: "sm",
|
|
297
294
|
"aria-label": loadingLabel,
|
|
298
|
-
role: "img"
|
|
299
|
-
|
|
295
|
+
role: "img",
|
|
296
|
+
"aria-hidden": isBusy ? undefined : true,
|
|
297
|
+
xstyle: isBusy ? undefined : styles.idleSpinner
|
|
298
|
+
}, isBusy ? 'busy' : 'idle'), children]
|
|
300
299
|
});
|
|
301
300
|
}
|
|
302
301
|
export function Tokenizer({
|
|
@@ -633,7 +632,7 @@ export function Tokenizer({
|
|
|
633
632
|
if (renderToken) {
|
|
634
633
|
return /*#__PURE__*/_jsx("span", {
|
|
635
634
|
...{
|
|
636
|
-
className: "x78zum5 x2lah0s x1n2onr6 x1vjfegm"
|
|
635
|
+
className: "x78zum5 x2lah0s x193iq5w x1n2onr6 x1vjfegm"
|
|
637
636
|
},
|
|
638
637
|
children: renderToken(item, onRemoveItem)
|
|
639
638
|
}, item.id);
|
|
@@ -734,10 +733,10 @@ export function Tokenizer({
|
|
|
734
733
|
onKeyDown: handleKeyDown,
|
|
735
734
|
anchorRef: wrapperRef,
|
|
736
735
|
size: size,
|
|
737
|
-
inputXStyle: [isAtMax || isTruncated ? styles.inputAtMax : value.length > 0 ?
|
|
738
|
-
//
|
|
739
|
-
//
|
|
740
|
-
//
|
|
736
|
+
inputXStyle: [isAtMax || isTruncated ? styles.inputAtMax : value.length > 0 ? styles.inputCompact : undefined,
|
|
737
|
+
// With tokens, wrapperWithTokens reserves the lane for every row.
|
|
738
|
+
// This rule is only for an empty Tokenizer; it still resolves to
|
|
739
|
+
// zero until the lane publishes a width.
|
|
741
740
|
!(isAtMax || isTruncated || value.length > 0) && laneReserve]
|
|
742
741
|
})
|
|
743
742
|
})
|
|
@@ -755,7 +754,12 @@ export function Tokenizer({
|
|
|
755
754
|
laneXStyle: [styles.endSection, endSectionSizeStyles[size]],
|
|
756
755
|
loadingLabel: t('@astryx.typeahead.loading'),
|
|
757
756
|
anchorRef: wrapperRef,
|
|
758
|
-
hasStaticContent: hasStaticEndLane
|
|
757
|
+
hasStaticContent: hasStaticEndLane
|
|
758
|
+
// Wherever a search can run beside tokens: enabled and below
|
|
759
|
+
// maxEntries. A collapsed overflow row keeps it too, since a search
|
|
760
|
+
// started while focused can settle after focus leaves.
|
|
761
|
+
,
|
|
762
|
+
hasBusySlot: value.length > 0 && !isDisabled && !isAtMax,
|
|
759
763
|
children: [endContent, hasClearButton && /*#__PURE__*/_jsx(InputClearButton, {
|
|
760
764
|
label: t('@astryx.tokenizer.clearAll'),
|
|
761
765
|
onClick: e => {
|
|
@@ -23,8 +23,9 @@ import * as stylex from '@stylexjs/stylex';
|
|
|
23
23
|
*
|
|
24
24
|
* Takes the lane's inset from the field's inline-end border, as the CSS
|
|
25
25
|
* expression that positions it. Returns a ref callback for the lane, and the
|
|
26
|
-
* ordinary input reserve style. Tokenizer's
|
|
27
|
-
* property directly so
|
|
26
|
+
* ordinary input reserve style. With tokens, Tokenizer's field wrapper reads the
|
|
27
|
+
* same custom property directly so every token row, and the input after the
|
|
28
|
+
* last token, stops before the lane.
|
|
28
29
|
* With no lane mounted the variable is absent and either rule resolves to its
|
|
29
30
|
* no-lane fallback, so neither path needs to know whether a search is running.
|
|
30
31
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useEndLaneReserve.d.ts","sourceRoot":"","sources":["../../src/Tokenizer/useEndLaneReserve.ts"],"names":[],"mappings":"AA4BA,OAAO,KAAK,MAAM,MAAM,kBAAkB,CAAC;AA+B3C
|
|
1
|
+
{"version":3,"file":"useEndLaneReserve.d.ts","sourceRoot":"","sources":["../../src/Tokenizer/useEndLaneReserve.ts"],"names":[],"mappings":"AA4BA,OAAO,KAAK,MAAM,MAAM,kBAAkB,CAAC;AA+B3C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,MAAM,GAChB,CAAC,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,KAAK,IAAI,EAAE,MAAM,CAAC,YAAY,CAAC,CAuD3D"}
|
|
@@ -81,8 +81,9 @@ const reserveStyles = {
|
|
|
81
81
|
*
|
|
82
82
|
* Takes the lane's inset from the field's inline-end border, as the CSS
|
|
83
83
|
* expression that positions it. Returns a ref callback for the lane, and the
|
|
84
|
-
* ordinary input reserve style. Tokenizer's
|
|
85
|
-
* property directly so
|
|
84
|
+
* ordinary input reserve style. With tokens, Tokenizer's field wrapper reads the
|
|
85
|
+
* same custom property directly so every token row, and the input after the
|
|
86
|
+
* last token, stops before the lane.
|
|
86
87
|
* With no lane mounted the variable is absent and either rule resolves to its
|
|
87
88
|
* no-lane fallback, so neither path needs to know whether a search is running.
|
|
88
89
|
*/
|
package/dist/astryx.css
CHANGED
|
@@ -650,7 +650,7 @@
|
|
|
650
650
|
.x2uiyk:has(input:focus):not(#\#):not(#\#){border-color:var(--color-accent)}
|
|
651
651
|
.xb0m1lw:has(> div > .astryx-layout-content):not(#\#):not(#\#){margin-inline:0}
|
|
652
652
|
.x16s19ga:has(:focus-visible):not(#\#):not(#\#){outline:var(--focus-outline-width) var(--focus-outline-style) var(--focus-outline-color)}
|
|
653
|
-
.
|
|
653
|
+
.x1q9bidn:placeholder-shown:not(#\#):not(#\#){flex:1 1 0}
|
|
654
654
|
.x64lkcz:focus:not(#\#):not(#\#){border-color:var(--color-error)}
|
|
655
655
|
.x1lglk5v:focus:not(#\#):not(#\#){border-color:var(--color-success)}
|
|
656
656
|
.x12sck4v:focus:not(#\#):not(#\#){border-color:var(--color-warning)}
|
|
@@ -928,6 +928,7 @@
|
|
|
928
928
|
.xawy1a7:not(#\#):not(#\#):not(#\#){contain:layout style}
|
|
929
929
|
.x12h1iku:not(#\#):not(#\#):not(#\#){container-type:inline-size}
|
|
930
930
|
.xb5mbof:not(#\#):not(#\#):not(#\#){content-visibility:auto}
|
|
931
|
+
.xdps06:not(#\#):not(#\#):not(#\#){content-visibility:hidden}
|
|
931
932
|
.xfrknyr:not(#\#):not(#\#):not(#\#){counter-increment:astryx-list}
|
|
932
933
|
.xif0320:not(#\#):not(#\#):not(#\#){counter-reset:astryx-list}
|
|
933
934
|
.x1khind5:not(#\#):not(#\#):not(#\#){counter-reset:var(--x-counterReset)}
|
|
@@ -1193,6 +1194,7 @@
|
|
|
1193
1194
|
.x1mzt3pk:not(#\#):not(#\#):not(#\#){overflow-wrap:break-word}
|
|
1194
1195
|
.x1h4wwuj:not(#\#):not(#\#):not(#\#){overflow-wrap:normal}
|
|
1195
1196
|
.xyri2b:not(#\#):not(#\#):not(#\#){padding-inline-end:0}
|
|
1197
|
+
.xx0flbe:not(#\#):not(#\#):not(#\#){padding-inline-end:calc(calc(var(--spacing-1) - 1px) + var(--_tokenizer-end-lane-reserve,0px))}
|
|
1196
1198
|
.xtvv6yu:not(#\#):not(#\#):not(#\#){padding-inline-end:calc(var(--_tab-strip-bleed,0px) + max(var(--container-padding-inline-end,0px) - var(--spacing-3),0px))}
|
|
1197
1199
|
.x1g2apga:not(#\#):not(#\#):not(#\#){padding-inline-end:calc(var(--_textarea-inline-padding) + var(--spacing-12))}
|
|
1198
1200
|
.xp2dy4z:not(#\#):not(#\#):not(#\#){padding-inline-end:calc(var(--_textarea-inline-padding) + var(--spacing-6))}
|
|
@@ -1491,6 +1493,7 @@
|
|
|
1491
1493
|
.x9o1np9.x9o1np9:hover:where(:not(:disabled,[aria-disabled="true"])):not(#\#):not(#\#):not(#\#){color:var(--color-text-primary)}
|
|
1492
1494
|
.x16khyan:is(:disabled,[aria-disabled="true"]):not(#\#):not(#\#):not(#\#){cursor:default}
|
|
1493
1495
|
.x1y40yee:popover-open:not(#\#):not(#\#):not(#\#){display:flex}
|
|
1496
|
+
.x1rq9t4t:placeholder-shown:is(:not(:focus), :read-only):not(#\#):not(#\#):not(#\#){margin-inline-start:calc(-1 * var(--spacing-1))}
|
|
1494
1497
|
.x17xqekc:is([dir="rtl"] *):not(#\#):not(#\#):not(#\#){mask-image:linear-gradient(to left,transparent 0%,rgba(0,0,0,.3) 2px,black var(--spacing-1))}
|
|
1495
1498
|
.x1nwey30:is([dir="rtl"] *):not(#\#):not(#\#):not(#\#){mask-image:linear-gradient(to left,transparent var(--_tab-strip-bleed),black calc(var(--_tab-strip-bleed) + var(--spacing-8)))}
|
|
1496
1499
|
.x8ulde1:is([dir="rtl"] *):not(#\#):not(#\#):not(#\#){mask-image:linear-gradient(to right,transparent 0%,rgba(0,0,0,.3) 2px,black var(--spacing-1))}
|
|
@@ -1513,6 +1516,7 @@
|
|
|
1513
1516
|
.x1vidyx5:focus-visible:not(#\#):not(#\#):not(#\#){outline-width:var(--focus-outline-width)}
|
|
1514
1517
|
.x1n6uwha:is([dir="rtl"] *):not(#\#):not(#\#):not(#\#){padding-inline-end:max(var(--spacing-4),env(safe-area-inset-left,0px))}
|
|
1515
1518
|
.xtjjor6:is([dir="rtl"] *):not(#\#):not(#\#):not(#\#){padding-inline-end:var(--astryx-dialog-padding-inline-end,var(--astryx-dialog-padding-inline,var(--astryx-dialog-padding,max(var(--spacing-4),env(safe-area-inset-left,0px)))))}
|
|
1519
|
+
.x1jwxvv4:placeholder-shown:is(:not(:focus), :read-only):not(#\#):not(#\#):not(#\#){padding-inline-start:0}
|
|
1516
1520
|
.x19efkpl:is([dir="rtl"] *):not(#\#):not(#\#):not(#\#){padding-inline-start:max(var(--spacing-4),env(safe-area-inset-right,0px))}
|
|
1517
1521
|
.x1i7f2ot:is([dir="rtl"] *):not(#\#):not(#\#):not(#\#){padding-inline-start:var(--astryx-dialog-padding-inline-start,var(--astryx-dialog-padding-inline,var(--astryx-dialog-padding,max(var(--spacing-4),env(safe-area-inset-right,0px)))))}
|
|
1518
1522
|
.xbt4iw:focus-within:not(#\#):not(#\#):not(#\#){pointer-events:auto}
|
|
@@ -1576,12 +1580,6 @@
|
|
|
1576
1580
|
.x189zj9d:nth-child(even of :not([data-expansion-panel])):not(#\#):not(#\#):not(#\#){background-color:var(--color-background-muted)}
|
|
1577
1581
|
.xjl7jj:empty:not(#\#):not(#\#):not(#\#){display:none}
|
|
1578
1582
|
.x1e0d1ib:empty:not(#\#):not(#\#):not(#\#){padding-inline-start:0}
|
|
1579
|
-
.x1cs7wtd:placeholder-shown:not(#\#):not(#\#):not(#\#){inset-inline-start:calc(var(--spacing-1) - 1px)}
|
|
1580
|
-
.x1wg34a1:placeholder-shown:not(#\#):not(#\#):not(#\#){padding-inline-end:var(--spacing-1)}
|
|
1581
|
-
.xf0ofad:placeholder-shown:not(#\#):not(#\#):not(#\#){padding-inline-start:0}
|
|
1582
|
-
.x1utg46d:placeholder-shown:not(#\#):not(#\#):not(#\#){position:absolute}
|
|
1583
|
-
.x75j1tr:placeholder-shown:not(#\#):not(#\#):not(#\#){text-align:end}
|
|
1584
|
-
.xv6p7n0:placeholder-shown:not(#\#):not(#\#):not(#\#){transform:translateY(-50%)}
|
|
1585
1583
|
.x1c52tdz:focus:not(#\#):not(#\#):not(#\#){background-color:var(--color-overlay-hover)}
|
|
1586
1584
|
.x1rsz1da:focus:not(#\#):not(#\#):not(#\#){clip-path:none}
|
|
1587
1585
|
.xodanix:focus:not(#\#):not(#\#):not(#\#){inset-inline-start:var(--spacing-2)}
|
|
@@ -1963,7 +1961,6 @@
|
|
|
1963
1961
|
.xs7f9wi:not(#\#):not(#\#):not(#\#):not(#\#){top:2px}
|
|
1964
1962
|
.xwa60dl:not(#\#):not(#\#):not(#\#):not(#\#){top:50%}
|
|
1965
1963
|
.x1ltwjim:not(#\#):not(#\#):not(#\#):not(#\#){top:anchor(--outline-active top,0px)}
|
|
1966
|
-
.x80663w:not(#\#):not(#\#):not(#\#):not(#\#){top:auto}
|
|
1967
1964
|
.x7rbydg:not(#\#):not(#\#):not(#\#):not(#\#){top:calc(50% - (28px / 2))}
|
|
1968
1965
|
.x1oysc8t:not(#\#):not(#\#):not(#\#):not(#\#){top:calc(var(--size-element-lg) / 2 - 1px)}
|
|
1969
1966
|
.xkmsrut:not(#\#):not(#\#):not(#\#):not(#\#){top:calc(var(--size-element-md) / 2 - 1px)}
|
|
@@ -2056,11 +2053,7 @@
|
|
|
2056
2053
|
.x1t1lzn6:last-child:not(#\#):not(#\#):not(#\#):not(#\#){border-bottom-width:0}
|
|
2057
2054
|
.xlayyun:last-child:not(#\#):not(#\#):not(#\#):not(#\#){margin-bottom:calc(-1 * var(--container-padding-block-end,0px))}
|
|
2058
2055
|
.xup0pd7:last-child:not(#\#):not(#\#):not(#\#):not(#\#){padding-bottom:0}
|
|
2059
|
-
.
|
|
2060
|
-
.x1o13q2w:placeholder-shown:not(#\#):not(#\#):not(#\#):not(#\#){top:calc(var(--size-element-lg) / 2 - 1px)}
|
|
2061
|
-
.x811pv0:placeholder-shown:not(#\#):not(#\#):not(#\#):not(#\#){top:calc(var(--size-element-md) / 2 - 1px)}
|
|
2062
|
-
.x1a21zuh:placeholder-shown:not(#\#):not(#\#):not(#\#):not(#\#){top:calc(var(--size-element-sm) / 2 - 1px)}
|
|
2063
|
-
.xbuu7xz:placeholder-shown:not(#\#):not(#\#):not(#\#):not(#\#){width:calc(100% - calc(var(--spacing-1) - 1px) - var(--_tokenizer-end-lane-reserve,calc(var(--spacing-1) - 1px)) - 2px - 1px)}
|
|
2056
|
+
.x14dcwvw:placeholder-shown:not(#\#):not(#\#):not(#\#):not(#\#){min-width:0}
|
|
2064
2057
|
.x15cytp8:focus:not(#\#):not(#\#):not(#\#):not(#\#){height:auto}
|
|
2065
2058
|
.x1rw3289:focus:not(#\#):not(#\#):not(#\#):not(#\#){top:var(--spacing-2)}
|
|
2066
2059
|
.x1jqxupm:focus:not(#\#):not(#\#):not(#\#):not(#\#){width:auto}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astryxdesign/core",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.5-canary.1093026",
|
|
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",
|
|
@@ -792,6 +792,8 @@
|
|
|
792
792
|
"react-dom": ">=19.0.0"
|
|
793
793
|
},
|
|
794
794
|
"devDependencies": {
|
|
795
|
+
"@astryxdesign/a11y-spec": "0.7.5-canary.1093026",
|
|
796
|
+
"@astryxdesign/cli": "0.7.5-canary.1093026",
|
|
795
797
|
"@babel/cli": "^7.29.7",
|
|
796
798
|
"@babel/core": "^7.29.7",
|
|
797
799
|
"@babel/preset-react": "^7.29.7",
|
|
@@ -802,9 +804,7 @@
|
|
|
802
804
|
"@testing-library/jest-dom": "^6.6.0",
|
|
803
805
|
"@testing-library/react": "^16.3.2",
|
|
804
806
|
"@types/babel__core": "^7.20.5",
|
|
805
|
-
"tailwindcss": "^4.3.2"
|
|
806
|
-
"@astryxdesign/a11y-spec": "0.0.0",
|
|
807
|
-
"@astryxdesign/cli": "0.7.4"
|
|
807
|
+
"tailwindcss": "^4.3.2"
|
|
808
808
|
},
|
|
809
809
|
"dependencies": {
|
|
810
810
|
"intl-messageformat": "^11.2.9"
|
package/src/Badge/Badge.doc.mjs
CHANGED
|
@@ -18,6 +18,7 @@ export const docs = {
|
|
|
18
18
|
{
|
|
19
19
|
name: 'label',
|
|
20
20
|
type: 'ReactNode',
|
|
21
|
+
required: true,
|
|
21
22
|
description:
|
|
22
23
|
'Badge text content. A badge is one line, so a label wider than the space available is cut with an ellipsis rather than escaping its container; a string or number label is also set as the badge\'s title so the full text stays reachable on hover.',
|
|
23
24
|
},
|
|
@@ -93,7 +94,7 @@ export const docsZh = {
|
|
|
93
94
|
description: '视觉样式变体。语义变体使用实色背景,非语义颜色变体使用浅色背景配彩色文字。',
|
|
94
95
|
default: "'neutral'",
|
|
95
96
|
},
|
|
96
|
-
{name: 'label', type: 'ReactNode', description: '徽章文本内容。'},
|
|
97
|
+
{name: 'label', type: 'ReactNode', required: true, description: '徽章文本内容。'},
|
|
97
98
|
{name: 'icon', type: 'ReactNode', description: '可选的前置图标。'},
|
|
98
99
|
],
|
|
99
100
|
theming: {
|
package/src/Icon/Icon.spec.md
CHANGED
|
@@ -27,7 +27,7 @@ architecture:
|
|
|
27
27
|
architecture:public-component-api,
|
|
28
28
|
]
|
|
29
29
|
contributing: []
|
|
30
|
-
system_specs: []
|
|
30
|
+
system_specs: [spec:AST-087]
|
|
31
31
|
---
|
|
32
32
|
|
|
33
33
|
# Icon component contract
|
|
@@ -53,6 +53,7 @@ Consumer migration instructions belong in consumer docs and release notes.
|
|
|
53
53
|
**Owns**
|
|
54
54
|
|
|
55
55
|
- Accepting a semantic icon key or a supplied icon component as the glyph source.
|
|
56
|
+
- Passing the resolved size and a variant the entry can receive to a registry entry.
|
|
56
57
|
- Resolving explicit, component-owned contextual, and standalone size defaults.
|
|
57
58
|
- Applying Icon's size, color, theming target, and accessibility semantics to
|
|
58
59
|
the rendered glyph.
|
|
@@ -71,12 +72,13 @@ Consumer migration instructions belong in consumer docs and release notes.
|
|
|
71
72
|
|
|
72
73
|
## Public concepts
|
|
73
74
|
|
|
74
|
-
| Concept | Closed values or states | Meaning
|
|
75
|
-
| ------------- | --------------------------------------------------------- |
|
|
76
|
-
| Glyph source | semantic key, namespaced extension key, or icon component | Selects the visual symbol.
|
|
77
|
-
| Size | `xsm`, `sm`, `md`, `lg` | Selects the icon box size.
|
|
78
|
-
| Color | documented semantic and palette values | Selects the glyph color or inherits it from context.
|
|
79
|
-
| Accessibility | decorative or meaningfully labelled | Controls whether assistive technology receives the glyph.
|
|
75
|
+
| Concept | Closed values or states | Meaning | Availability by variant/orientation/state | Default | Owner | Stability | Invalid-value behavior |
|
|
76
|
+
| ------------- | --------------------------------------------------------- | ---------------------------------------------------------- | ------------------------------------------------ | ----------------------------------------------- | ---------------- | --------- | --------------------------------------------------------------------------------- |
|
|
77
|
+
| Glyph source | semantic key, namespaced extension key, or icon component | Selects the visual symbol. | Every render | Required | `component:Icon` | Stable | TypeScript rejects unsupported built-in string values. |
|
|
78
|
+
| Size | `xsm`, `sm`, `md`, `lg` | Selects the icon box size. | Every rendered glyph | nearest component-owned default; otherwise `md` | `component:Icon` | Stable | TypeScript rejects unsupported values. |
|
|
79
|
+
| Color | documented semantic and palette values | Selects the glyph color or inherits it from context. | Every rendered glyph | `inherit` | `component:Icon` | Stable | TypeScript rejects unsupported values. |
|
|
80
|
+
| Accessibility | decorative or meaningfully labelled | Controls whether assistive technology receives the glyph. | Every rendered glyph | Decorative | `component:Icon` | Stable | Empty labels use the decorative behavior. |
|
|
81
|
+
| Variant | names in `IconVariantMap`; the active theme lists its own | Requests a theme's alternate artwork for a registry glyph. | Registry glyphs only; never a supplied component | None | `component:Icon` | Stable | TypeScript rejects unknown names; an unlisted variant renders the normal artwork. |
|
|
80
82
|
|
|
81
83
|
A namespaced key that does not resolve currently renders nothing. This is
|
|
82
84
|
existing behavior, not an intentional fallback promise.
|
|
@@ -86,13 +88,15 @@ existing behavior, not an intentional fallback promise.
|
|
|
86
88
|
Requirements identify their basis so observed code is not mistaken for an
|
|
87
89
|
intentional decision.
|
|
88
90
|
|
|
89
|
-
| ID | Candidate invariant
|
|
90
|
-
| --- |
|
|
91
|
-
| FR1 | Icon MUST present at most one glyph from the supplied semantic key, namespaced key, or icon component.
|
|
92
|
-
| FR2 | Every rendered glyph MUST carry the `icon` theming target with its resolved size and selected color reflected as target data.
|
|
93
|
-
| FR3 | Icon MUST apply the resolved size as width and height for every glyph, plus font size where needed for 1em-based icon sources. This sizing contract MUST NOT promise an HTML or SVG element type.
|
|
94
|
-
| FR4 | Supported SVG and styling escape hatches MUST retain their established merge and override behavior.
|
|
95
|
-
| FR5 | An explicit Icon `size` MUST win. Without one, Icon MUST use the nearest default supplied by an owning Astryx component for its documented icon slot; without such a default, Icon MUST use `md`.
|
|
91
|
+
| ID | Candidate invariant | Basis | Draft review state |
|
|
92
|
+
| --- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | ------------------------------------------------------- |
|
|
93
|
+
| FR1 | Icon MUST present at most one glyph from the supplied semantic key, namespaced key, or icon component. | Documented promise and current tests. | Settled intent. |
|
|
94
|
+
| FR2 | Every rendered glyph MUST carry the `icon` theming target with its resolved size and selected color reflected as target data. | Current source, docs, and theming tests. | Settled intent. |
|
|
95
|
+
| FR3 | Icon MUST apply the resolved size as width and height for every glyph, plus font size where needed for 1em-based icon sources. This sizing contract MUST NOT promise an HTML or SVG element type. | Human decision, current source, and tests. | Settled intent. |
|
|
96
|
+
| FR4 | Supported SVG and styling escape hatches MUST retain their established merge and override behavior. | Current source and regression tests. | Current compatibility behavior; verify before changing. |
|
|
97
|
+
| FR5 | An explicit Icon `size` MUST win. Without one, Icon MUST use the nearest default supplied by an owning Astryx component for its documented icon slot; without such a default, Icon MUST use `md`. | Component-size precedence and Button sizing decision. | Settled intent. |
|
|
98
|
+
| FR6 | In registry mode, Icon MUST pass `{size, variant}` to the selected entry, where `size` is the size resolved by ORD1 and `variant` is the requested variant when the active theme lists it and, for a theme-supplied entry, the theme that defined the entry lists it too, otherwise `undefined`. A supplied icon component MUST NOT receive `variant`, Icon MUST NOT forward `variant` to the DOM, and the `icon` target MUST NOT reflect it. | `spec:AST-087` FR1–FR5 and FR9. | Settled intent. |
|
|
99
|
+
| FR7 | The `icon` target MUST guarantee `width` and `height` on its `size:*` selectors as reviewed target-specific additions, so a theme sets each size's dimension. Without theme styles, sizes are 12, 16, 20, and 24px at a 16px root and scale with the root font size. | `spec:AST-087` FR10. | Settled intent. |
|
|
96
100
|
|
|
97
101
|
### Allowed variation
|
|
98
102
|
|
|
@@ -109,15 +113,18 @@ intentional decision.
|
|
|
109
113
|
|
|
110
114
|
### Representative states
|
|
111
115
|
|
|
112
|
-
| State | Required invariant
|
|
113
|
-
| -------------------------------- |
|
|
114
|
-
| Semantic key | One resolved glyph carries the `icon` target.
|
|
115
|
-
| Namespaced extension key | A resolved extension glyph carries the `icon` target.
|
|
116
|
-
| Supplied icon component | The supplied glyph carries the `icon` target.
|
|
117
|
-
| Standalone Icon without `size` | The resolved size is `md`.
|
|
118
|
-
| Owned icon slot without `size` | The nearest documented component default is used.
|
|
119
|
-
| Explicit Icon size in owned slot | The explicit Icon size wins over the contextual default.
|
|
120
|
-
| Unresolved namespaced key | No glyph is currently rendered.
|
|
116
|
+
| State | Required invariant | Allowed variation |
|
|
117
|
+
| -------------------------------- | ---------------------------------------------------------------------------------- | ---------------------------------------------- |
|
|
118
|
+
| Semantic key | One resolved glyph carries the `icon` target. | Theme, registry, or built-in artwork. |
|
|
119
|
+
| Namespaced extension key | A resolved extension glyph carries the `icon` target. | Consumer- or library-owned artwork. |
|
|
120
|
+
| Supplied icon component | The supplied glyph carries the `icon` target. | Component implementation and SVG internals. |
|
|
121
|
+
| Standalone Icon without `size` | The resolved size is `md`. | Glyph source and color. |
|
|
122
|
+
| Owned icon slot without `size` | The nearest documented component default is used. | The owning component's declared size mapping. |
|
|
123
|
+
| Explicit Icon size in owned slot | The explicit Icon size wins over the contextual default. | Any supported Icon size. |
|
|
124
|
+
| Unresolved namespaced key | No glyph is currently rendered. | No fallback behavior is established by intent. |
|
|
125
|
+
| Listed variant | The entry receives the size and the variant when its defining theme also lists it. | The theme's artwork for that request. |
|
|
126
|
+
| Unlisted or omitted variant | The entry receives `variant: undefined`. | The theme's normal artwork. |
|
|
127
|
+
| Variant on a supplied component | The component renders as given without `variant`. | Component implementation. |
|
|
121
128
|
|
|
122
129
|
### Transformation and precedence order
|
|
123
130
|
|
|
@@ -172,6 +179,8 @@ not promise a `span`, `svg`, or other element type.
|
|
|
172
179
|
rules for the `icon` target.
|
|
173
180
|
- `architecture:icon-resolution-and-component-slots` owns semantic-key and
|
|
174
181
|
component-slot resolution before Icon renders the selected source.
|
|
182
|
+
- `spec:AST-087` decides the `variant` request, theme-listed variant names, and
|
|
183
|
+
theme-set size dimensions.
|
|
175
184
|
- `architecture:public-component-api` owns admission and compatibility rules for
|
|
176
185
|
Icon's public props and observable defaults.
|
|
177
186
|
- The owning component or family specifies each contextual Icon-size mapping.
|
|
@@ -181,12 +190,14 @@ not promise a `span`, `svg`, or other element type.
|
|
|
181
190
|
|
|
182
191
|
## Verification map
|
|
183
192
|
|
|
184
|
-
| Contract | Verification | Representative states
|
|
185
|
-
| ------------------- | ------------------------------------------------------------------------------ |
|
|
186
|
-
| FR1, FR3, FR5 | `Icon.test.tsx` sizing suites and `IconButton.test.tsx` contextual-size suites | standalone default; Button sizes; explicit override; both modes
|
|
187
|
-
| FR2, FR4 | `Icon.test.tsx` target/styling suites plus source inspection | Both source modes; size and color variants
|
|
188
|
-
| AR1, AR2, AR3 | `Icon.test.tsx` accessible-name suites | Decorative, labelled, explicit ARIA override
|
|
189
|
-
|
|
|
193
|
+
| Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
|
|
194
|
+
| ------------------- | ------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------- |
|
|
195
|
+
| FR1, FR3, FR5 | `Icon.test.tsx` sizing suites and `IconButton.test.tsx` contextual-size suites | standalone default; Button sizes; explicit override; both modes | Breaking source rendering, standalone fallback, contextual mapping, explicit precedence, or cross-mode sizing fails assertions. | `audit:Icon/behavior` |
|
|
196
|
+
| FR2, FR4 | `Icon.test.tsx` target/styling suites plus source inspection | Both source modes; size and color variants | Removing the target or breaking override composition fails existing assertions; `data-color` reflection currently lacks focused assertions. | `audit:Icon/theming` |
|
|
197
|
+
| AR1, AR2, AR3 | `Icon.test.tsx` accessible-name suites | Decorative, labelled, explicit ARIA override | Changing default or labelled semantics fails accessibility assertions. | `audit:Icon/accessibility` |
|
|
198
|
+
| FR6 | `Icon.test.tsx` variant suites (pending) | listed, unlisted, and omitted variant; no active list; inherited entry under a replaced list; supplied component | An unlisted variant, or one the entry's defining theme does not list, reaches an entry, or `variant` reaches a supplied component or the DOM. | `audit:Icon/behavior` |
|
|
199
|
+
| FR7 | Theme styling tests for each size (pending) | each size with and without theme styles | A themed size does not change the glyph dimension, or an unstyled size changes. | `audit:Icon/theming` |
|
|
200
|
+
| Theming anatomy map | `scripts/check-knowledge.mjs` | Canonical consumer anatomy and current target | Missing, extra, prefixed, or stale mappings fail repository validation. | `audit:Icon/theming` |
|
|
190
201
|
|
|
191
202
|
Focused coverage for `data-color` on both glyph source modes is a checkable test
|
|
192
203
|
gap; source inspection is the current evidence for that part of FR2.
|
|
@@ -1692,6 +1692,27 @@ describe('Tokenizer end-lane reserve', () => {
|
|
|
1692
1692
|
);
|
|
1693
1693
|
});
|
|
1694
1694
|
|
|
1695
|
+
it('keeps token rows, not only the input, short of the lane', () => {
|
|
1696
|
+
// The lane is out of flow, so a token reaching the end of the first row
|
|
1697
|
+
// ran under the clear button. The field's own inline-end padding spends
|
|
1698
|
+
// the reserve, and every row of tokens wraps inside it. Real geometry is
|
|
1699
|
+
// proved in TokenizerEndLane.a11y.chromium.spec.ts.
|
|
1700
|
+
const {container} = render(
|
|
1701
|
+
<Tokenizer
|
|
1702
|
+
label="Users"
|
|
1703
|
+
searchSource={userSource}
|
|
1704
|
+
value={[users[0]]}
|
|
1705
|
+
onChange={() => {}}
|
|
1706
|
+
hasClear
|
|
1707
|
+
/>,
|
|
1708
|
+
);
|
|
1709
|
+
const field = laneHost(container);
|
|
1710
|
+
expect(field).not.toBeNull();
|
|
1711
|
+
expect(getComputedStyle(field!).paddingInlineEnd).toContain(
|
|
1712
|
+
'--_tokenizer-end-lane-reserve',
|
|
1713
|
+
);
|
|
1714
|
+
});
|
|
1715
|
+
|
|
1695
1716
|
it('does not pad the input on the collapsed paths', () => {
|
|
1696
1717
|
// Collapsed: with tokens truncated the input is given no width to pad —
|
|
1697
1718
|
// `inputAtMax` zeroes its padding outright — so a reserve there would be
|
|
@@ -1707,16 +1728,13 @@ describe('Tokenizer end-lane reserve', () => {
|
|
|
1707
1728
|
tokenOverflowBehavior="unfocusedInline"
|
|
1708
1729
|
/>,
|
|
1709
1730
|
);
|
|
1731
|
+
// The collapsed row keeps the Spinner's slot, so the lane is up and
|
|
1732
|
+
// publishing; the input still must not spend the reserve.
|
|
1733
|
+
expect(reserveOf(container)).toBeTruthy();
|
|
1710
1734
|
const input = screen.getByRole('combobox');
|
|
1711
|
-
expect(input.
|
|
1712
|
-
|
|
1713
|
-
// collapsed input must not carry it.
|
|
1714
|
-
const reserveClasses = new Set(
|
|
1715
|
-
(laneHost(container)?.className ?? '').split(' '),
|
|
1735
|
+
expect(getComputedStyle(input).paddingInlineEnd).not.toContain(
|
|
1736
|
+
'--_tokenizer-end-lane-reserve',
|
|
1716
1737
|
);
|
|
1717
|
-
expect(
|
|
1718
|
-
[...input.classList].some(c => reserveClasses.has(c) && c !== ''),
|
|
1719
|
-
).toBe(false);
|
|
1720
1738
|
});
|
|
1721
1739
|
|
|
1722
1740
|
it('costs no commit of its own across a whole search', async () => {
|
|
@@ -272,11 +272,8 @@ export interface TokenizerProps<T extends SearchableItem> extends Omit<
|
|
|
272
272
|
// because the input's reserve is derived from the same value.
|
|
273
273
|
const END_LANE_INSET = spacingVars['--spacing-2'];
|
|
274
274
|
const TOKEN_ROW_INSET = `calc(${spacingVars['--spacing-1']} - 1px)`;
|
|
275
|
-
//
|
|
276
|
-
//
|
|
277
|
-
const INPUT_CLEAR_COARSE_POINTER_OUTSET = '2px';
|
|
278
|
-
// Kept in sync with useEndLaneReserve: inputCompact reads the custom property
|
|
279
|
-
// directly so its empty-query pseudo-class can suppress the inner reserve.
|
|
275
|
+
// Kept in sync with useEndLaneReserve: wrapperWithTokens reads the custom
|
|
276
|
+
// property directly, to keep token rows and the input short of the lane.
|
|
280
277
|
const END_LANE_RESERVE_VAR = '--_tokenizer-end-lane-reserve';
|
|
281
278
|
|
|
282
279
|
const styles = stylex.create({
|
|
@@ -297,6 +294,10 @@ const styles = stylex.create({
|
|
|
297
294
|
// = 8 - 4 - 1 = 3px.
|
|
298
295
|
paddingBlock: TOKEN_ROW_INSET,
|
|
299
296
|
paddingInline: TOKEN_ROW_INSET,
|
|
297
|
+
// The end lane is out of flow, so tokens would wrap under it. Every row
|
|
298
|
+
// stops short of the measured lane instead; with no lane up the variable
|
|
299
|
+
// is absent and this is the ordinary inset.
|
|
300
|
+
paddingInlineEnd: `calc(${TOKEN_ROW_INSET} + var(${END_LANE_RESERVE_VAR}, 0px))`,
|
|
300
301
|
// Row gap must match paddingBlock so wrapped rows look evenly spaced.
|
|
301
302
|
rowGap: TOKEN_ROW_INSET,
|
|
302
303
|
},
|
|
@@ -310,6 +311,9 @@ const styles = stylex.create({
|
|
|
310
311
|
token: {
|
|
311
312
|
display: 'flex',
|
|
312
313
|
flexShrink: 0,
|
|
314
|
+
// A renderToken wrapper sizes to its content; cap it at the row so the
|
|
315
|
+
// pill inside truncates instead of running past the field.
|
|
316
|
+
maxWidth: '100%',
|
|
313
317
|
position: 'relative',
|
|
314
318
|
zIndex: 1,
|
|
315
319
|
},
|
|
@@ -325,6 +329,14 @@ const styles = stylex.create({
|
|
|
325
329
|
flexShrink: 0,
|
|
326
330
|
zIndex: 1,
|
|
327
331
|
},
|
|
332
|
+
idleSpinner: {
|
|
333
|
+
// Hides the Spinner's own box too, which a theme may paint, and covers
|
|
334
|
+
// engines without content-visibility. Both keep the box's size.
|
|
335
|
+
visibility: 'hidden',
|
|
336
|
+
// Skips the ring's rendering, and with it the infinite arc animation,
|
|
337
|
+
// while the root keeps its own explicit, themed width and height.
|
|
338
|
+
contentVisibility: 'hidden',
|
|
339
|
+
},
|
|
328
340
|
inputAtMax: {
|
|
329
341
|
width: 0,
|
|
330
342
|
minWidth: 0,
|
|
@@ -334,46 +346,36 @@ const styles = stylex.create({
|
|
|
334
346
|
position: 'absolute',
|
|
335
347
|
},
|
|
336
348
|
inputCompact: {
|
|
337
|
-
position:
|
|
338
|
-
default: 'relative',
|
|
339
|
-
':placeholder-shown': 'absolute',
|
|
340
|
-
},
|
|
349
|
+
position: 'relative',
|
|
341
350
|
boxSizing: 'border-box',
|
|
342
|
-
insetInlineStart: {
|
|
343
|
-
default: 'auto',
|
|
344
|
-
':placeholder-shown': TOKEN_ROW_INSET,
|
|
345
|
-
},
|
|
346
351
|
zIndex: 0,
|
|
352
|
+
// The input follows the last token in flow, so a focused empty input's
|
|
353
|
+
// caret sits where typed text will start. Empty, it asks only for that
|
|
354
|
+
// inset, and only while focused and editable: blurred or read-only (a
|
|
355
|
+
// disabled field with a disabledMessage stays focusable), it claims no
|
|
356
|
+
// room at all, not even the column gap, so it never grows the field a
|
|
357
|
+
// blank row. Typing restores the full editing width, which may wrap.
|
|
347
358
|
minWidth: {
|
|
348
359
|
default: '40px',
|
|
349
|
-
':placeholder-shown':
|
|
360
|
+
':placeholder-shown': 0,
|
|
350
361
|
},
|
|
351
362
|
flex: {
|
|
352
363
|
default: '1 1 40px',
|
|
353
|
-
':placeholder-shown': '
|
|
364
|
+
':placeholder-shown': '1 1 0px',
|
|
354
365
|
},
|
|
355
|
-
width:
|
|
366
|
+
width: 0,
|
|
367
|
+
marginInlineStart: {
|
|
356
368
|
default: 0,
|
|
357
|
-
':placeholder-shown': `calc(
|
|
369
|
+
':placeholder-shown:is(:not(:focus), :read-only)': `calc(-1 * ${spacingVars['--spacing-1']})`,
|
|
358
370
|
},
|
|
359
|
-
//
|
|
360
|
-
//
|
|
361
|
-
// with the pills for width. Logical insets stop it before the measured end
|
|
362
|
-
// lane plus InputClearButton's maximum invisible pointer outset; tokens and
|
|
363
|
-
// controls sit above it, leaving only genuine whitespace clickable as
|
|
364
|
-
// search. Typing restores the ordinary in-flow input.
|
|
371
|
+
// Restore normal text inset when input follows tokens, since the
|
|
372
|
+
// wrapper padding is reduced for border concentricity.
|
|
365
373
|
paddingInlineStart: {
|
|
366
374
|
default: `calc(${spacingVars['--spacing-2']} - ${spacingVars['--spacing-1']} + 1px)`,
|
|
367
|
-
':placeholder-shown': 0,
|
|
368
|
-
},
|
|
369
|
-
paddingInlineEnd: {
|
|
370
|
-
default: `var(${END_LANE_RESERVE_VAR}, 0px)`,
|
|
371
|
-
':placeholder-shown': spacingVars['--spacing-1'],
|
|
372
|
-
},
|
|
373
|
-
textAlign: {
|
|
374
|
-
default: 'start',
|
|
375
|
-
':placeholder-shown': 'end',
|
|
375
|
+
':placeholder-shown:is(:not(:focus), :read-only)': 0,
|
|
376
376
|
},
|
|
377
|
+
// wrapperWithTokens already keeps the in-flow input short of the lane.
|
|
378
|
+
paddingInlineEnd: 0,
|
|
377
379
|
},
|
|
378
380
|
truncatedWrapper: {
|
|
379
381
|
flexWrap: 'nowrap',
|
|
@@ -415,39 +417,6 @@ const endSectionSizeStyles = stylex.create({
|
|
|
415
417
|
},
|
|
416
418
|
});
|
|
417
419
|
|
|
418
|
-
const compactInputSizeStyles = stylex.create({
|
|
419
|
-
sm: {
|
|
420
|
-
insetBlockStart: {
|
|
421
|
-
default: 'auto',
|
|
422
|
-
':placeholder-shown': `calc(${sizeVars['--size-element-sm']} / 2 - 1px)`,
|
|
423
|
-
},
|
|
424
|
-
transform: {
|
|
425
|
-
default: 'none',
|
|
426
|
-
':placeholder-shown': 'translateY(-50%)',
|
|
427
|
-
},
|
|
428
|
-
},
|
|
429
|
-
md: {
|
|
430
|
-
insetBlockStart: {
|
|
431
|
-
default: 'auto',
|
|
432
|
-
':placeholder-shown': `calc(${sizeVars['--size-element-md']} / 2 - 1px)`,
|
|
433
|
-
},
|
|
434
|
-
transform: {
|
|
435
|
-
default: 'none',
|
|
436
|
-
':placeholder-shown': 'translateY(-50%)',
|
|
437
|
-
},
|
|
438
|
-
},
|
|
439
|
-
lg: {
|
|
440
|
-
insetBlockStart: {
|
|
441
|
-
default: 'auto',
|
|
442
|
-
':placeholder-shown': `calc(${sizeVars['--size-element-lg']} / 2 - 1px)`,
|
|
443
|
-
},
|
|
444
|
-
transform: {
|
|
445
|
-
default: 'none',
|
|
446
|
-
':placeholder-shown': 'translateY(-50%)',
|
|
447
|
-
},
|
|
448
|
-
},
|
|
449
|
-
});
|
|
450
|
-
|
|
451
420
|
const truncatedSizeStyles = stylex.create({
|
|
452
421
|
sm: {height: sizeVars['--size-element-sm']},
|
|
453
422
|
md: {height: sizeVars['--size-element-md']},
|
|
@@ -517,6 +486,13 @@ const CREATABLE_ID_PREFIX = '__xds_create__';
|
|
|
517
486
|
*
|
|
518
487
|
* Renders nothing when the lane would be empty, so `useEndLaneReserve` sees no
|
|
519
488
|
* element, publishes no width, and the input keeps its full content box.
|
|
489
|
+
*
|
|
490
|
+
* With `hasBusySlot` the Spinner stays mounted while idle, unrendered, not
|
|
491
|
+
* animating and out of the accessibility tree, so its box keeps the themed
|
|
492
|
+
* Spinner's own size.
|
|
493
|
+
* Token rows reserve the lane's measured width, so a Spinner that came and
|
|
494
|
+
* went with the search would re-wrap them, and widen a content-sized field,
|
|
495
|
+
* on every search.
|
|
520
496
|
*/
|
|
521
497
|
function EndLane({
|
|
522
498
|
lane,
|
|
@@ -525,6 +501,7 @@ function EndLane({
|
|
|
525
501
|
loadingLabel,
|
|
526
502
|
anchorRef,
|
|
527
503
|
hasStaticContent,
|
|
504
|
+
hasBusySlot,
|
|
528
505
|
children,
|
|
529
506
|
}: {
|
|
530
507
|
lane: BusyIndicatorLane;
|
|
@@ -533,6 +510,7 @@ function EndLane({
|
|
|
533
510
|
loadingLabel: string;
|
|
534
511
|
anchorRef: RefObject<Element | null>;
|
|
535
512
|
hasStaticContent: boolean;
|
|
513
|
+
hasBusySlot: boolean;
|
|
536
514
|
children: ReactNode;
|
|
537
515
|
}) {
|
|
538
516
|
const isBusy = useIsBusy(lane);
|
|
@@ -540,12 +518,23 @@ function EndLane({
|
|
|
540
518
|
// live region; the field announces each search's busy start instead
|
|
541
519
|
// (spec:AST-086 FR8).
|
|
542
520
|
useBusyAnnouncement(isBusy, loadingLabel, anchorRef);
|
|
543
|
-
if (!isBusy && !hasStaticContent) {
|
|
521
|
+
if (!isBusy && !hasStaticContent && !hasBusySlot) {
|
|
544
522
|
return null;
|
|
545
523
|
}
|
|
546
524
|
return (
|
|
547
525
|
<div ref={laneRef} {...stylex.props(laneXStyle)}>
|
|
548
|
-
{isBusy
|
|
526
|
+
{(isBusy || hasBusySlot) && (
|
|
527
|
+
<Spinner
|
|
528
|
+
// Remounts when a search starts, so the ring syncs its phase on
|
|
529
|
+
// mount exactly as a newly shown Spinner did before.
|
|
530
|
+
key={isBusy ? 'busy' : 'idle'}
|
|
531
|
+
size="sm"
|
|
532
|
+
aria-label={loadingLabel}
|
|
533
|
+
role="img"
|
|
534
|
+
aria-hidden={isBusy ? undefined : true}
|
|
535
|
+
xstyle={isBusy ? undefined : styles.idleSpinner}
|
|
536
|
+
/>
|
|
537
|
+
)}
|
|
549
538
|
{children}
|
|
550
539
|
</div>
|
|
551
540
|
);
|
|
@@ -1065,11 +1054,11 @@ export function Tokenizer<T extends SearchableItem>({
|
|
|
1065
1054
|
isAtMax || isTruncated
|
|
1066
1055
|
? styles.inputAtMax
|
|
1067
1056
|
: value.length > 0
|
|
1068
|
-
?
|
|
1057
|
+
? styles.inputCompact
|
|
1069
1058
|
: undefined,
|
|
1070
|
-
//
|
|
1071
|
-
//
|
|
1072
|
-
//
|
|
1059
|
+
// With tokens, wrapperWithTokens reserves the lane for every row.
|
|
1060
|
+
// This rule is only for an empty Tokenizer; it still resolves to
|
|
1061
|
+
// zero until the lane publishes a width.
|
|
1073
1062
|
!(isAtMax || isTruncated || value.length > 0) && laneReserve,
|
|
1074
1063
|
]}
|
|
1075
1064
|
/>
|
|
@@ -1093,7 +1082,11 @@ export function Tokenizer<T extends SearchableItem>({
|
|
|
1093
1082
|
laneXStyle={[styles.endSection, endSectionSizeStyles[size]]}
|
|
1094
1083
|
loadingLabel={t('@astryx.typeahead.loading')}
|
|
1095
1084
|
anchorRef={wrapperRef}
|
|
1096
|
-
hasStaticContent={hasStaticEndLane}
|
|
1085
|
+
hasStaticContent={hasStaticEndLane}
|
|
1086
|
+
// Wherever a search can run beside tokens: enabled and below
|
|
1087
|
+
// maxEntries. A collapsed overflow row keeps it too, since a search
|
|
1088
|
+
// started while focused can settle after focus leaves.
|
|
1089
|
+
hasBusySlot={value.length > 0 && !isDisabled && !isAtMax}>
|
|
1097
1090
|
{endContent}
|
|
1098
1091
|
{hasClearButton && (
|
|
1099
1092
|
<InputClearButton
|
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file TokenizerEndLane.a11y.chromium.spec.ts
|
|
5
|
+
* @input `Core/Tokenizer` and `Core/PowerSearch` stories in a built Storybook.
|
|
6
|
+
* @output Real-engine proof that tokens, the overflow count and the input's
|
|
7
|
+
* text box never paint under the end lane (clear-all and busy indicator),
|
|
8
|
+
* across field widths, both writing directions and both overflow modes.
|
|
9
|
+
* @position Run with `pnpm test:a11y-contract`. The lane is absolutely
|
|
10
|
+
* positioned, so whether a token reaches it depends on how rows wrap at a
|
|
11
|
+
* given width, which only a shipping layout engine can show.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import {expect, test, type Page} from '@playwright/test';
|
|
15
|
+
import {
|
|
16
|
+
DEFAULT_STORYBOOK_DIR,
|
|
17
|
+
serveStorybook,
|
|
18
|
+
type StaticServer,
|
|
19
|
+
} from '@astryxdesign/a11y-spec/storybook';
|
|
20
|
+
|
|
21
|
+
let storybook: StaticServer;
|
|
22
|
+
|
|
23
|
+
test.beforeAll(async () => {
|
|
24
|
+
storybook = await serveStorybook(
|
|
25
|
+
process.env.ASTRYX_STORYBOOK_DIR ?? DEFAULT_STORYBOOK_DIR,
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test.afterAll(async () => {
|
|
30
|
+
await storybook?.close();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
type Direction = 'ltr' | 'rtl';
|
|
34
|
+
|
|
35
|
+
async function open(
|
|
36
|
+
page: Page,
|
|
37
|
+
id: string,
|
|
38
|
+
direction: Direction,
|
|
39
|
+
args = '',
|
|
40
|
+
): Promise<void> {
|
|
41
|
+
await page.goto(
|
|
42
|
+
`${storybook.origin}/iframe.html?id=${id}&viewMode=story&globals=direction:${direction}${args ? `&args=${args}` : ''}`,
|
|
43
|
+
{waitUntil: 'load'},
|
|
44
|
+
);
|
|
45
|
+
await page
|
|
46
|
+
.locator('button[aria-label="Clear all"]')
|
|
47
|
+
.first()
|
|
48
|
+
.waitFor({state: 'attached'});
|
|
49
|
+
await page.evaluate(async () => {
|
|
50
|
+
await document.fonts.ready;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Sets the story's sizing container to `width` and, for every field with a
|
|
56
|
+
* visible clear-all lane, returns each visible box that intersects the lane:
|
|
57
|
+
* tokens, the "+N more" count, and the input's content box (where its text
|
|
58
|
+
* and caret paint).
|
|
59
|
+
*/
|
|
60
|
+
async function overlapsAt(page: Page, width: number): Promise<string[]> {
|
|
61
|
+
return page.evaluate(async width => {
|
|
62
|
+
// The story decorator's fixed-width box; the layer mode's popover takes
|
|
63
|
+
// its width from the in-flow field inside it.
|
|
64
|
+
const host = document.querySelector<HTMLElement>(
|
|
65
|
+
'#storybook-root [style*="width"]',
|
|
66
|
+
);
|
|
67
|
+
if (!host) {
|
|
68
|
+
throw new Error('No sizing container in the story');
|
|
69
|
+
}
|
|
70
|
+
host.style.width = `${width}px`;
|
|
71
|
+
for (let frame = 0; frame < 4; frame++) {
|
|
72
|
+
await new Promise(resolve => requestAnimationFrame(resolve));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const isShown = (el: Element) =>
|
|
76
|
+
el.checkVisibility({visibilityProperty: true}) &&
|
|
77
|
+
!el.closest('[aria-hidden="true"]');
|
|
78
|
+
const lanes = [
|
|
79
|
+
...document.querySelectorAll('button[aria-label="Clear all"]'),
|
|
80
|
+
]
|
|
81
|
+
.filter(isShown)
|
|
82
|
+
.map(clear => clear.parentElement)
|
|
83
|
+
.filter(lane => lane?.parentElement != null) as HTMLElement[];
|
|
84
|
+
if (lanes.length === 0) {
|
|
85
|
+
throw new Error('No clear-all lane is visible');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return lanes.flatMap((lane, fieldIndex) => {
|
|
89
|
+
const field = lane.parentElement as HTMLElement;
|
|
90
|
+
const boxes: {name: string; rect: DOMRect}[] = [];
|
|
91
|
+
field.querySelectorAll('.astryx-token').forEach((token, index) => {
|
|
92
|
+
if (isShown(token)) {
|
|
93
|
+
boxes.push({
|
|
94
|
+
name: `token ${index}`,
|
|
95
|
+
rect: token.getBoundingClientRect(),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
field.querySelectorAll('span').forEach(span => {
|
|
100
|
+
if (/^\+\d+ more$/.test(span.textContent ?? '') && isShown(span)) {
|
|
101
|
+
boxes.push({
|
|
102
|
+
name: 'overflow count',
|
|
103
|
+
rect: span.getBoundingClientRect(),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
const input = field.querySelector<HTMLInputElement>('[role="combobox"]');
|
|
108
|
+
if (input && isShown(input) && getComputedStyle(input).opacity !== '0') {
|
|
109
|
+
const style = getComputedStyle(input);
|
|
110
|
+
const rect = input.getBoundingClientRect();
|
|
111
|
+
const left = rect.left + parseFloat(style.paddingLeft);
|
|
112
|
+
const right = rect.right - parseFloat(style.paddingRight);
|
|
113
|
+
if (right > left) {
|
|
114
|
+
boxes.push({
|
|
115
|
+
name: 'input text box',
|
|
116
|
+
rect: new DOMRect(left, rect.top, right - left, rect.height),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const laneRect = lane.getBoundingClientRect();
|
|
122
|
+
return boxes.flatMap(({name, rect}) => {
|
|
123
|
+
const x =
|
|
124
|
+
Math.min(rect.right, laneRect.right) -
|
|
125
|
+
Math.max(rect.left, laneRect.left);
|
|
126
|
+
const y =
|
|
127
|
+
Math.min(rect.bottom, laneRect.bottom) -
|
|
128
|
+
Math.max(rect.top, laneRect.top);
|
|
129
|
+
return x > 0.5 && y > 0.5
|
|
130
|
+
? [
|
|
131
|
+
`${width}px field ${fieldIndex}: ${name} overlaps the end lane by ${x.toFixed(1)}px`,
|
|
132
|
+
]
|
|
133
|
+
: [];
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
}, width);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function sweep(page: Page, from: number, to: number): Promise<string[]> {
|
|
140
|
+
const found: string[] = [];
|
|
141
|
+
for (let width = from; width <= to; width += 4) {
|
|
142
|
+
found.push(...(await overlapsAt(page, width)));
|
|
143
|
+
}
|
|
144
|
+
return found;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The same sweep with a search in flight at every width. The busy Spinner joins
|
|
149
|
+
* the lane only while a search runs, so each width is measured while the
|
|
150
|
+
* combobox reports `aria-busy`, typing again whenever the last search settled.
|
|
151
|
+
*/
|
|
152
|
+
async function sweepWhileBusy(
|
|
153
|
+
page: Page,
|
|
154
|
+
from: number,
|
|
155
|
+
to: number,
|
|
156
|
+
): Promise<string[]> {
|
|
157
|
+
const search = page.getByRole('combobox');
|
|
158
|
+
const found: string[] = [];
|
|
159
|
+
for (let width = from; width <= to; width += 4) {
|
|
160
|
+
for (;;) {
|
|
161
|
+
if ((await search.getAttribute('aria-busy')) !== 'true') {
|
|
162
|
+
await search.press('a');
|
|
163
|
+
await expect(search).toHaveAttribute('aria-busy', 'true');
|
|
164
|
+
}
|
|
165
|
+
const overlaps = await overlapsAt(page, width);
|
|
166
|
+
if ((await search.getAttribute('aria-busy')) === 'true') {
|
|
167
|
+
found.push(...overlaps);
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return found;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Sizes the story container (a pixel width, or `max-content` for a
|
|
177
|
+
* content-sized field) and returns the field's border box.
|
|
178
|
+
*/
|
|
179
|
+
async function fieldBoxAt(
|
|
180
|
+
page: Page,
|
|
181
|
+
width: string,
|
|
182
|
+
): Promise<{width: number; height: number}> {
|
|
183
|
+
return page.evaluate(async width => {
|
|
184
|
+
const host = document.querySelector<HTMLElement>(
|
|
185
|
+
'#storybook-root [style*="width"]',
|
|
186
|
+
);
|
|
187
|
+
const field = document.querySelector('button[aria-label="Clear all"]')
|
|
188
|
+
?.parentElement?.parentElement;
|
|
189
|
+
if (!host || !field) {
|
|
190
|
+
throw new Error('Field or sizing container did not render');
|
|
191
|
+
}
|
|
192
|
+
host.style.width = width;
|
|
193
|
+
for (let frame = 0; frame < 4; frame++) {
|
|
194
|
+
await new Promise(resolve => requestAnimationFrame(resolve));
|
|
195
|
+
}
|
|
196
|
+
const {width: w, height: h} = field.getBoundingClientRect();
|
|
197
|
+
return {
|
|
198
|
+
width: Math.round(w * 100) / 100,
|
|
199
|
+
height: Math.round(h * 100) / 100,
|
|
200
|
+
};
|
|
201
|
+
}, width);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Adds tokens through the menu, as a person would. */
|
|
205
|
+
async function addTokens(page: Page, names: string[]): Promise<void> {
|
|
206
|
+
const search = page.getByRole('combobox');
|
|
207
|
+
for (const name of names) {
|
|
208
|
+
await search.fill(name.split(' ')[0]);
|
|
209
|
+
await page.getByRole('option', {name}).click();
|
|
210
|
+
await expect(search).not.toHaveAttribute('aria-busy', 'true');
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* The field's box with a search settled and again with one in flight. The
|
|
216
|
+
* query is non-empty both times, so only the busy state differs.
|
|
217
|
+
*/
|
|
218
|
+
async function settledAndBusy(
|
|
219
|
+
page: Page,
|
|
220
|
+
width: string,
|
|
221
|
+
): Promise<string | null> {
|
|
222
|
+
const search = page.getByRole('combobox');
|
|
223
|
+
await search.fill('a');
|
|
224
|
+
await expect(search).not.toHaveAttribute('aria-busy', 'true');
|
|
225
|
+
const settled = await fieldBoxAt(page, width);
|
|
226
|
+
await search.press('b');
|
|
227
|
+
await expect(search).toHaveAttribute('aria-busy', 'true');
|
|
228
|
+
const busy = await fieldBoxAt(page, width);
|
|
229
|
+
const stillBusy = (await search.getAttribute('aria-busy')) === 'true';
|
|
230
|
+
if (!stillBusy) {
|
|
231
|
+
throw new Error(`The search settled before ${width} was measured`);
|
|
232
|
+
}
|
|
233
|
+
return settled.width === busy.width && settled.height === busy.height
|
|
234
|
+
? null
|
|
235
|
+
: `${width}: settled ${settled.width}x${settled.height}, busy ${busy.width}x${busy.height}`;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* With four tokens on the Loading story, every width from 200px to 400px and a
|
|
240
|
+
* content-sized container where the field's box differs settled vs busy.
|
|
241
|
+
*/
|
|
242
|
+
async function busyLayoutChanges(page: Page, step: number): Promise<string[]> {
|
|
243
|
+
await addTokens(page, ['Bob Smith', 'Charlie Brown', 'Diana Prince']);
|
|
244
|
+
await expect(page.locator('.astryx-token')).toHaveCount(4);
|
|
245
|
+
const changed: string[] = [];
|
|
246
|
+
for (let width = 200; width <= 400; width += step) {
|
|
247
|
+
const change = await settledAndBusy(page, `${width}px`);
|
|
248
|
+
if (change) {
|
|
249
|
+
changed.push(change);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
// Content-sized: the field may not widen while busy (input-fields FR1).
|
|
253
|
+
const contentSized = await settledAndBusy(page, 'max-content');
|
|
254
|
+
if (contentSized) {
|
|
255
|
+
changed.push(contentSized);
|
|
256
|
+
}
|
|
257
|
+
return changed;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* The blurred, collapsed field at `width`: its border box, how many tokens
|
|
262
|
+
* show, and the "+N more" count.
|
|
263
|
+
*/
|
|
264
|
+
async function collapsedRowAt(page: Page, width: string): Promise<string> {
|
|
265
|
+
const box = await fieldBoxAt(page, width);
|
|
266
|
+
const row = await page.evaluate(() => {
|
|
267
|
+
const field = document.querySelector('button[aria-label="Clear all"]')
|
|
268
|
+
?.parentElement?.parentElement;
|
|
269
|
+
if (!field) {
|
|
270
|
+
throw new Error('Field did not render');
|
|
271
|
+
}
|
|
272
|
+
const shown = [...field.querySelectorAll('.astryx-token')].filter(
|
|
273
|
+
token =>
|
|
274
|
+
token.checkVisibility({visibilityProperty: true}) &&
|
|
275
|
+
!token.closest('[aria-hidden="true"]'),
|
|
276
|
+
).length;
|
|
277
|
+
const count = [...field.querySelectorAll('span')].find(span =>
|
|
278
|
+
/^\+\d+ more$/.test(span.textContent ?? ''),
|
|
279
|
+
);
|
|
280
|
+
return `${shown} tokens, ${count?.textContent ?? 'no count'}`;
|
|
281
|
+
});
|
|
282
|
+
return `${box.width}x${box.height}, ${row}`;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
for (const direction of ['ltr', 'rtl'] as const) {
|
|
286
|
+
test.describe(direction, () => {
|
|
287
|
+
test('PowerSearch tokens (custom renderToken) wrap before the end lane', async ({
|
|
288
|
+
page,
|
|
289
|
+
}) => {
|
|
290
|
+
await open(page, 'core-powersearch--near-full-token-row', direction);
|
|
291
|
+
expect(await sweep(page, 160, 640)).toEqual([]);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test('a token wider than the row truncates short of the end lane', async ({
|
|
295
|
+
page,
|
|
296
|
+
}) => {
|
|
297
|
+
await open(page, 'core-tokenizer--long-token-end-lane', direction);
|
|
298
|
+
expect(await sweep(page, 160, 400)).toEqual([]);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test('default Tokenizer tokens wrap before the end lane', async ({
|
|
302
|
+
page,
|
|
303
|
+
}) => {
|
|
304
|
+
await open(
|
|
305
|
+
page,
|
|
306
|
+
'core-tokenizer--with-preselected',
|
|
307
|
+
direction,
|
|
308
|
+
'hasClear:!true',
|
|
309
|
+
);
|
|
310
|
+
expect(await sweep(page, 160, 400)).toEqual([]);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test('tokens stay clear of the lane while a search is busy', async ({
|
|
314
|
+
page,
|
|
315
|
+
}) => {
|
|
316
|
+
await open(page, 'core-tokenizer--loading', direction);
|
|
317
|
+
await page.getByRole('combobox').focus();
|
|
318
|
+
expect(await sweepWhileBusy(page, 200, 400)).toEqual([]);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test('a search starting does not move the field or its token rows', async ({
|
|
322
|
+
page,
|
|
323
|
+
}) => {
|
|
324
|
+
test.setTimeout(240_000);
|
|
325
|
+
await open(page, 'core-tokenizer--loading', direction);
|
|
326
|
+
expect(await busyLayoutChanges(page, 4)).toEqual([]);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
test('the reserved Spinner costs no frames and stays silent while idle', async ({
|
|
330
|
+
page,
|
|
331
|
+
}) => {
|
|
332
|
+
await open(page, 'core-tokenizer--loading', direction);
|
|
333
|
+
const search = page.getByRole('combobox');
|
|
334
|
+
const cdp = await page.context().newCDPSession(page);
|
|
335
|
+
await cdp.send('Performance.enable');
|
|
336
|
+
// Style recalculations over a short window: a ticking ring animation
|
|
337
|
+
// recalculates every frame (about 30 in 500ms), an unrendered one does
|
|
338
|
+
// not. The idle allowance absorbs a stray one-off recalculation.
|
|
339
|
+
const recalcsOver = async (ms: number) => {
|
|
340
|
+
const count = async () => {
|
|
341
|
+
const {metrics} = await cdp.send('Performance.getMetrics');
|
|
342
|
+
return metrics.find(m => m.name === 'RecalcStyleCount')?.value ?? 0;
|
|
343
|
+
};
|
|
344
|
+
const before = await count();
|
|
345
|
+
await page.waitForTimeout(ms);
|
|
346
|
+
return (await count()) - before;
|
|
347
|
+
};
|
|
348
|
+
const spinnerState = async () =>
|
|
349
|
+
page.evaluate(() => {
|
|
350
|
+
const spinner = document
|
|
351
|
+
.querySelector('button[aria-label="Clear all"]')
|
|
352
|
+
?.parentElement?.querySelector('.astryx-spinner');
|
|
353
|
+
if (!spinner) {
|
|
354
|
+
throw new Error('The lane holds no Spinner');
|
|
355
|
+
}
|
|
356
|
+
return {
|
|
357
|
+
hidden: spinner.getAttribute('aria-hidden') === 'true',
|
|
358
|
+
width: spinner.getBoundingClientRect().width,
|
|
359
|
+
};
|
|
360
|
+
});
|
|
361
|
+
const idle = await spinnerState();
|
|
362
|
+
expect(idle.hidden).toBe(true);
|
|
363
|
+
expect(await recalcsOver(500)).toBeLessThanOrEqual(2);
|
|
364
|
+
await search.focus();
|
|
365
|
+
await search.press('a');
|
|
366
|
+
await expect(search).toHaveAttribute('aria-busy', 'true');
|
|
367
|
+
const busy = await spinnerState();
|
|
368
|
+
expect(busy.hidden).toBe(false);
|
|
369
|
+
expect(await recalcsOver(500)).toBeGreaterThanOrEqual(10);
|
|
370
|
+
expect(busy.width).toBe(idle.width);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
test('the idle Spinner paints nothing, even when a theme styles its box', async ({
|
|
374
|
+
page,
|
|
375
|
+
}) => {
|
|
376
|
+
await page.goto(
|
|
377
|
+
`${storybook.origin}/iframe.html?id=core-tokenizer--loading&viewMode=story&globals=direction:${direction};astryxTheme:probe`,
|
|
378
|
+
{waitUntil: 'load'},
|
|
379
|
+
);
|
|
380
|
+
await page
|
|
381
|
+
.locator('button[aria-label="Clear all"]')
|
|
382
|
+
.waitFor({state: 'attached'});
|
|
383
|
+
const search = page.getByRole('combobox');
|
|
384
|
+
const spinnerPaint = async () =>
|
|
385
|
+
page.evaluate(() => {
|
|
386
|
+
const spinner = document
|
|
387
|
+
.querySelector('button[aria-label="Clear all"]')
|
|
388
|
+
?.parentElement?.querySelector('.astryx-spinner');
|
|
389
|
+
if (!spinner) {
|
|
390
|
+
throw new Error('The lane holds no Spinner');
|
|
391
|
+
}
|
|
392
|
+
const style = getComputedStyle(spinner);
|
|
393
|
+
return {
|
|
394
|
+
visible: spinner.checkVisibility({visibilityProperty: true}),
|
|
395
|
+
visibility: style.visibility,
|
|
396
|
+
background: style.backgroundColor,
|
|
397
|
+
};
|
|
398
|
+
});
|
|
399
|
+
const idle = await spinnerPaint();
|
|
400
|
+
// The probe theme paints the Spinner's own box, so a box that is only
|
|
401
|
+
// content-skipped would still show beside Clear all.
|
|
402
|
+
expect(idle.background).not.toBe('rgba(0, 0, 0, 0)');
|
|
403
|
+
expect(idle.visibility).toBe('hidden');
|
|
404
|
+
expect(idle.visible).toBe(false);
|
|
405
|
+
await search.focus();
|
|
406
|
+
await search.press('a');
|
|
407
|
+
await expect(search).toHaveAttribute('aria-busy', 'true');
|
|
408
|
+
expect((await spinnerPaint()).visible).toBe(true);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
test('a themed, larger Spinner keeps its space too', async ({page}) => {
|
|
412
|
+
test.setTimeout(240_000);
|
|
413
|
+
await open(page, 'core-tokenizer--loading', direction);
|
|
414
|
+
// The public size var a theme sets for a named Spinner size.
|
|
415
|
+
await page.addStyleTag({
|
|
416
|
+
content: '.astryx-spinner { --spinner-diameter: 24px !important; }',
|
|
417
|
+
});
|
|
418
|
+
expect(await busyLayoutChanges(page, 8)).toEqual([]);
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
test('a search settling after blur does not change the collapsed row', async ({
|
|
422
|
+
page,
|
|
423
|
+
}) => {
|
|
424
|
+
test.setTimeout(240_000);
|
|
425
|
+
await open(
|
|
426
|
+
page,
|
|
427
|
+
'core-tokenizer--loading',
|
|
428
|
+
direction,
|
|
429
|
+
'tokenOverflowBehavior:unfocusedInline',
|
|
430
|
+
);
|
|
431
|
+
const search = page.getByRole('combobox');
|
|
432
|
+
await search.focus();
|
|
433
|
+
await addTokens(page, ['Bob Smith', 'Charlie Brown', 'Diana Prince']);
|
|
434
|
+
const changed: string[] = [];
|
|
435
|
+
for (let width = 200; width <= 400; width += 8) {
|
|
436
|
+
await search.focus();
|
|
437
|
+
await search.fill('');
|
|
438
|
+
await search.press('a');
|
|
439
|
+
await expect(search).toHaveAttribute('aria-busy', 'true');
|
|
440
|
+
await search.blur();
|
|
441
|
+
const busy = await collapsedRowAt(page, `${width}px`);
|
|
442
|
+
await expect(search).not.toHaveAttribute('aria-busy', 'true');
|
|
443
|
+
const settled = await collapsedRowAt(page, `${width}px`);
|
|
444
|
+
if (busy !== settled) {
|
|
445
|
+
changed.push(`${width}px: busy ${busy}, settled ${settled}`);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
expect(changed).toEqual([]);
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
for (const mode of ['overflow-inline', 'overflow-layer']) {
|
|
452
|
+
test(`${mode}: blurred and focused tokens stay clear of the end lane`, async ({
|
|
453
|
+
page,
|
|
454
|
+
}) => {
|
|
455
|
+
await open(page, `core-powersearch--${mode}`, direction);
|
|
456
|
+
// Blurred: only the inline mode keeps its lane on the page.
|
|
457
|
+
if (mode === 'overflow-inline') {
|
|
458
|
+
expect(await sweep(page, 320, 640)).toEqual([]);
|
|
459
|
+
}
|
|
460
|
+
// The field's own corner, clear of every token: the click a person
|
|
461
|
+
// makes to start typing, which expands both modes.
|
|
462
|
+
await page
|
|
463
|
+
.locator('.astryx-tokenizer')
|
|
464
|
+
.first()
|
|
465
|
+
.click({position: {x: 1, y: 1}});
|
|
466
|
+
await expect(page.getByRole('combobox')).toBeFocused();
|
|
467
|
+
expect(await sweep(page, 320, 640)).toEqual([]);
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
}
|
|
@@ -81,8 +81,9 @@ const reserveStyles = stylex.create({
|
|
|
81
81
|
*
|
|
82
82
|
* Takes the lane's inset from the field's inline-end border, as the CSS
|
|
83
83
|
* expression that positions it. Returns a ref callback for the lane, and the
|
|
84
|
-
* ordinary input reserve style. Tokenizer's
|
|
85
|
-
* property directly so
|
|
84
|
+
* ordinary input reserve style. With tokens, Tokenizer's field wrapper reads the
|
|
85
|
+
* same custom property directly so every token row, and the input after the
|
|
86
|
+
* last token, stops before the lane.
|
|
86
87
|
* With no lane mounted the variable is absent and either rule resolves to its
|
|
87
88
|
* no-lane fallback, so neither path needs to know whether a search is running.
|
|
88
89
|
*/
|