@astryxdesign/core 0.7.2-canary.7c0f850 → 0.7.2-canary.80d2f41
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/Chat/ChatLayoutScrollButton.d.ts.map +1 -1
- package/dist/Chat/ChatLayoutScrollButton.js +1 -0
- package/package.json +3 -3
- package/src/Chat/ChatLayoutScrollButton.test.tsx +13 -0
- package/src/Chat/ChatLayoutScrollButton.tsx +4 -2
- package/src/Chat/__tests__/ChatLayoutScrollButton.a11y.chromium.spec.ts +64 -19
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ChatLayoutScrollButton.d.ts","sourceRoot":"","sources":["../../src/Chat/ChatLayoutScrollButton.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAC;AAa1B,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAS5C,MAAM,WAAW,2BAA4B,SAAQ,IAAI,CACvD,SAAS,CAAC,cAAc,CAAC,EACzB,SAAS,CACV;IACC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAChC,qCAAqC;IACrC,SAAS,EAAE,OAAO,CAAC;IACnB,iEAAiE;IACjE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qBAAqB;IACrB,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;
|
|
1
|
+
{"version":3,"file":"ChatLayoutScrollButton.d.ts","sourceRoot":"","sources":["../../src/Chat/ChatLayoutScrollButton.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAC;AAa1B,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAS5C,MAAM,WAAW,2BAA4B,SAAQ,IAAI,CACvD,SAAS,CAAC,cAAc,CAAC,EACzB,SAAS,CACV;IACC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAChC,qCAAqC;IACrC,SAAS,EAAE,OAAO,CAAC;IACnB,iEAAiE;IACjE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qBAAqB;IACrB,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAuED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,EACrC,GAAG,EACH,SAAS,EACT,KAAK,EACL,OAAO,EACP,MAAM,EACN,SAAS,EACT,KAAK,EACL,GAAG,IAAI,EACR,EAAE,2BAA2B,qBAwC7B;yBAjDe,sBAAsB"}
|
|
@@ -89,6 +89,7 @@ export function ChatLayoutScrollButton({
|
|
|
89
89
|
...mergeProps(stylex.props(styles.wrapper, xstyle), className, style),
|
|
90
90
|
...rest,
|
|
91
91
|
children: /*#__PURE__*/_jsx("div", {
|
|
92
|
+
inert: !isVisible,
|
|
92
93
|
...mergeProps(themeProps('chat-layout-scroll-button'), {
|
|
93
94
|
0: {
|
|
94
95
|
className: "xawy1a7 xb3r6kr xjspbzw x1prclbq x14hfi27 x1ueg155 xrgg2z9 xlr8y92 x2rcmwa x12w9bfk xg01cxk x47corl xlshs6z x1jys5uj"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astryxdesign/core",
|
|
3
|
-
"version": "0.7.2-canary.
|
|
3
|
+
"version": "0.7.2-canary.80d2f41",
|
|
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,8 +792,8 @@
|
|
|
792
792
|
"react-dom": ">=19.0.0"
|
|
793
793
|
},
|
|
794
794
|
"devDependencies": {
|
|
795
|
-
"@astryxdesign/a11y-spec": "0.7.2-canary.
|
|
796
|
-
"@astryxdesign/cli": "0.7.2-canary.
|
|
795
|
+
"@astryxdesign/a11y-spec": "0.7.2-canary.80d2f41",
|
|
796
|
+
"@astryxdesign/cli": "0.7.2-canary.80d2f41",
|
|
797
797
|
"@babel/cli": "^7.29.7",
|
|
798
798
|
"@babel/core": "^7.29.7",
|
|
799
799
|
"@babel/preset-react": "^7.29.7",
|
|
@@ -70,6 +70,19 @@ describe('ChatLayoutScrollButton', () => {
|
|
|
70
70
|
).toHaveFocus();
|
|
71
71
|
});
|
|
72
72
|
|
|
73
|
+
it('makes the pill inert whenever the visible state turns off', () => {
|
|
74
|
+
const {container, rerender} = render(
|
|
75
|
+
<ChatLayoutScrollButton isVisible onClick={() => {}} />,
|
|
76
|
+
);
|
|
77
|
+
const pill = container.querySelector('.astryx-chat-layout-scroll-button');
|
|
78
|
+
|
|
79
|
+
expect(pill).not.toHaveAttribute('inert');
|
|
80
|
+
|
|
81
|
+
rerender(<ChatLayoutScrollButton isVisible={false} onClick={() => {}} />);
|
|
82
|
+
|
|
83
|
+
expect(pill).toHaveAttribute('inert');
|
|
84
|
+
});
|
|
85
|
+
|
|
73
86
|
it('is not keyboard reachable while hidden', async () => {
|
|
74
87
|
const user = userEvent.setup();
|
|
75
88
|
render(
|
|
@@ -86,8 +86,9 @@ const styles = stylex.create({
|
|
|
86
86
|
opacity: 0,
|
|
87
87
|
pointerEvents: 'none',
|
|
88
88
|
// The hidden pill paints nothing, so focus landing on it would have no
|
|
89
|
-
// visible indicator (WCAG 2.2 SC 2.4.7). `
|
|
90
|
-
//
|
|
89
|
+
// visible indicator (WCAG 2.2 SC 2.4.7). `visibility` removes it from
|
|
90
|
+
// sequential navigation after the fade; `inert` immediately prevents new
|
|
91
|
+
// focus and makes an already-focused descendant relinquish focus.
|
|
91
92
|
visibility: 'hidden',
|
|
92
93
|
maxWidth: sizeVars['--size-element-md'],
|
|
93
94
|
},
|
|
@@ -151,6 +152,7 @@ export function ChatLayoutScrollButton({
|
|
|
151
152
|
{...mergeProps(stylex.props(styles.wrapper, xstyle), className, style)}
|
|
152
153
|
{...rest}>
|
|
153
154
|
<div
|
|
155
|
+
inert={!isVisible}
|
|
154
156
|
{...mergeProps(
|
|
155
157
|
themeProps('chat-layout-scroll-button'),
|
|
156
158
|
stylex.props(
|
|
@@ -44,6 +44,7 @@ const STORY_ID = 'core-chatlayout--scroll-affordance-states';
|
|
|
44
44
|
* a stable handle for it.
|
|
45
45
|
*/
|
|
46
46
|
const PILL = '.astryx-chat-layout-scroll-button';
|
|
47
|
+
const AFFORDANCE = `${PILL} button`;
|
|
47
48
|
|
|
48
49
|
/** One observed tab stop, identified structurally rather than by its text. */
|
|
49
50
|
type FocusStop = {
|
|
@@ -126,7 +127,11 @@ test.afterAll(async () => {
|
|
|
126
127
|
|
|
127
128
|
async function openStory(
|
|
128
129
|
page: Page,
|
|
129
|
-
{
|
|
130
|
+
{
|
|
131
|
+
theme = 'neutral',
|
|
132
|
+
colorMode = 'light',
|
|
133
|
+
holdMotion = true,
|
|
134
|
+
}: {theme?: string; colorMode?: string; holdMotion?: boolean} = {},
|
|
130
135
|
): Promise<Locator> {
|
|
131
136
|
page.on('pageerror', error => pageErrors.push(String(error)));
|
|
132
137
|
browserVersion = page.context().browser()?.version() ?? 'unknown';
|
|
@@ -136,7 +141,9 @@ async function openStory(
|
|
|
136
141
|
);
|
|
137
142
|
const root = page.locator('.astryx-chat-layout');
|
|
138
143
|
await root.waitFor({state: 'visible'});
|
|
139
|
-
|
|
144
|
+
if (holdMotion) {
|
|
145
|
+
await holdMotionStill(page);
|
|
146
|
+
}
|
|
140
147
|
await page.evaluate(async () => document.fonts.ready);
|
|
141
148
|
return root;
|
|
142
149
|
}
|
|
@@ -169,21 +176,25 @@ async function pillVisibility(page: Page): Promise<string> {
|
|
|
169
176
|
}
|
|
170
177
|
|
|
171
178
|
/**
|
|
172
|
-
* Can the affordance take focus at all? This is the mechanism
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
* is used rather than a role query because a hidden button
|
|
176
|
-
* accessibility tree and a role query would not resolve it.
|
|
179
|
+
* Can the affordance take new focus at all? This is one half of the mechanism
|
|
180
|
+
* under test: `inert` refuses programmatic focus immediately, while delayed
|
|
181
|
+
* `visibility: hidden` removes the control from sequential navigation after the
|
|
182
|
+
* fade. A CSS locator is used rather than a role query because a hidden button
|
|
183
|
+
* is not in the accessibility tree and a role query would not resolve it.
|
|
177
184
|
*/
|
|
178
|
-
async function affordanceAcceptsFocus(
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
185
|
+
async function affordanceAcceptsFocus(
|
|
186
|
+
page: Page,
|
|
187
|
+
resetFocus: Locator,
|
|
188
|
+
): Promise<boolean> {
|
|
189
|
+
// Start outside the affordance so this probe proves a NEW focus transfer;
|
|
190
|
+
// equality alone would also be true when a button stayed active as it hid.
|
|
191
|
+
await resetFocus.focus();
|
|
192
|
+
return page.locator(AFFORDANCE).evaluate(element => {
|
|
193
|
+
// preventScroll: focusing must not move the scroll position, or the
|
|
194
|
+
// probe would change the very state the next assertion reads.
|
|
195
|
+
(element as HTMLElement).focus({preventScroll: true});
|
|
196
|
+
return document.activeElement === element;
|
|
197
|
+
});
|
|
187
198
|
}
|
|
188
199
|
|
|
189
200
|
/**
|
|
@@ -340,6 +351,36 @@ async function capture(
|
|
|
340
351
|
return receipt;
|
|
341
352
|
}
|
|
342
353
|
|
|
354
|
+
test('the hiding affordance releases focus before its fade finishes', async ({
|
|
355
|
+
page,
|
|
356
|
+
}) => {
|
|
357
|
+
const root = await openStory(page, {holdMotion: false});
|
|
358
|
+
const pill = page.locator(PILL);
|
|
359
|
+
const affordance = page.locator(AFFORDANCE);
|
|
360
|
+
|
|
361
|
+
await scrollToTop(root);
|
|
362
|
+
await expect(pill).toHaveCSS('visibility', 'visible');
|
|
363
|
+
await affordance.focus();
|
|
364
|
+
await expect(affordance).toBeFocused();
|
|
365
|
+
|
|
366
|
+
await page.keyboard.press('Enter');
|
|
367
|
+
await expect
|
|
368
|
+
.poll(
|
|
369
|
+
async () =>
|
|
370
|
+
affordance.evaluate(element => document.activeElement === element),
|
|
371
|
+
{timeout: 5000},
|
|
372
|
+
)
|
|
373
|
+
.toBe(false);
|
|
374
|
+
|
|
375
|
+
// `visibility` flips only after the fade. Focus leaving while it is still
|
|
376
|
+
// visible proves the immediate `inert` state rather than the delayed CSS.
|
|
377
|
+
await expect(pill).toHaveCSS('visibility', 'visible');
|
|
378
|
+
await expect
|
|
379
|
+
.poll(async () => distanceFromBottom(root), {timeout: 5000})
|
|
380
|
+
.toBeLessThan(2);
|
|
381
|
+
await expect(pill).toHaveCSS('visibility', 'hidden');
|
|
382
|
+
});
|
|
383
|
+
|
|
343
384
|
test('the scroll affordance is keyboard reachable exactly while it is visible', async ({
|
|
344
385
|
page,
|
|
345
386
|
}) => {
|
|
@@ -367,7 +408,7 @@ test('the scroll affordance is keyboard reachable exactly while it is visible',
|
|
|
367
408
|
expect(await distanceFromBottom(root)).toBeLessThan(2);
|
|
368
409
|
await expect(pill).toHaveCSS('visibility', 'hidden');
|
|
369
410
|
expect(
|
|
370
|
-
await affordanceAcceptsFocus(page),
|
|
411
|
+
await affordanceAcceptsFocus(page, before),
|
|
371
412
|
'a control that paints nothing must refuse focus (WCAG 2.2 SC 2.4.7)',
|
|
372
413
|
).toBe(false);
|
|
373
414
|
const restFrame = await capture(page, 'rest-hidden', {
|
|
@@ -384,7 +425,7 @@ test('the scroll affordance is keyboard reachable exactly while it is visible',
|
|
|
384
425
|
await expect(pill).toHaveCSS('visibility', 'visible');
|
|
385
426
|
expect(await distanceFromBottom(root)).toBeGreaterThan(100);
|
|
386
427
|
expect(
|
|
387
|
-
await affordanceAcceptsFocus(page),
|
|
428
|
+
await affordanceAcceptsFocus(page, before),
|
|
388
429
|
'the visible affordance must keep its keyboard access',
|
|
389
430
|
).toBe(true);
|
|
390
431
|
await expect(
|
|
@@ -415,8 +456,12 @@ test('the scroll affordance is keyboard reachable exactly while it is visible',
|
|
|
415
456
|
.poll(async () => distanceFromBottom(root), {timeout: 5000})
|
|
416
457
|
.toBeLessThan(2);
|
|
417
458
|
await expect(pill).toHaveCSS('visibility', 'hidden');
|
|
459
|
+
await expect(
|
|
460
|
+
page.locator(AFFORDANCE),
|
|
461
|
+
'the affordance must relinquish focus when it hides',
|
|
462
|
+
).not.toBeFocused();
|
|
418
463
|
expect(
|
|
419
|
-
await affordanceAcceptsFocus(page),
|
|
464
|
+
await affordanceAcceptsFocus(page, before),
|
|
420
465
|
'after returning to the bottom the affordance must refuse focus again',
|
|
421
466
|
).toBe(false);
|
|
422
467
|
const returnedFrame = await capture(page, 'returned-hidden', {
|