@astryxdesign/core 0.6.1 → 0.6.2-canary.605fa9a
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/CHANGELOG.md +33 -0
- package/dist/BottomSheet/BottomSheet.d.ts +1 -1
- package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheet.js +3 -1
- package/dist/BottomSheet/BottomSheetPanel.d.ts +7 -5
- package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetPanel.js +56 -19
- package/dist/DateRangeInput/DateRangeInput.d.ts +3 -0
- package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
- package/dist/DateRangeInput/DateRangeInput.js +16 -9
- package/dist/Dialog/DialogHeader.d.ts +1 -1
- package/dist/Dialog/DialogHeader.d.ts.map +1 -1
- package/dist/Dialog/DialogHeader.js +10 -7
- package/dist/FileInput/FileInput.d.ts.map +1 -1
- package/dist/FileInput/FileInput.js +9 -3
- package/dist/Markdown/Markdown.d.ts +10 -2
- package/dist/Markdown/Markdown.d.ts.map +1 -1
- package/dist/Markdown/Markdown.js +58 -14
- package/dist/Markdown/index.d.ts +1 -1
- package/dist/Markdown/index.d.ts.map +1 -1
- package/dist/Markdown/parser.d.ts +126 -12
- package/dist/Markdown/parser.d.ts.map +1 -1
- package/dist/Markdown/parser.js +369 -34
- package/dist/Markdown/utils.d.ts +1 -1
- package/dist/Markdown/utils.d.ts.map +1 -1
- package/dist/Slider/Slider.d.ts.map +1 -1
- package/dist/Slider/Slider.js +5 -2
- package/dist/Spinner/Spinner.d.ts +1 -1
- package/dist/Spinner/Spinner.d.ts.map +1 -1
- package/dist/Spinner/Spinner.js +23 -15
- package/dist/astryx.css +5 -2
- package/dist/hooks/scrollKeyboardDelegation.d.ts +3 -0
- package/dist/hooks/scrollKeyboardDelegation.d.ts.map +1 -0
- package/dist/hooks/scrollKeyboardDelegation.js +146 -0
- package/dist/hooks/useScrollableArea.d.ts +6 -2
- package/dist/hooks/useScrollableArea.d.ts.map +1 -1
- package/dist/hooks/useScrollableArea.js +17 -5
- package/locales/en.json +16 -0
- package/locales/pseudo.json +12 -0
- package/package.json +7 -5
- package/scripts/agent-doc-state.mjs +1 -1
- package/src/BottomSheet/BottomSheet.doc.mjs +8 -1
- package/src/BottomSheet/BottomSheet.spec.md +46 -20
- package/src/BottomSheet/BottomSheet.test.tsx +6 -3
- package/src/BottomSheet/BottomSheet.tsx +3 -1
- package/src/BottomSheet/BottomSheetKeyboard.test.tsx +195 -0
- package/src/BottomSheet/BottomSheetPanel.test.tsx +11 -1
- package/src/BottomSheet/BottomSheetPanel.tsx +49 -16
- package/src/BottomSheet/__tests__/BottomSheetKeyboard.a11y.browser.spec.ts +344 -0
- package/src/DateRangeInput/DateRangeInput.doc.mjs +35 -7
- package/src/DateRangeInput/DateRangeInput.spec.md +203 -0
- package/src/DateRangeInput/DateRangeInput.test.tsx +100 -4
- package/src/DateRangeInput/DateRangeInput.tsx +29 -20
- package/src/Dialog/Dialog.doc.mjs +3 -0
- package/src/Dialog/Dialog.spec.md +1 -1
- package/src/Dialog/DialogHeader.doc.mjs +38 -0
- package/src/Dialog/DialogHeader.test.tsx +49 -0
- package/src/Dialog/DialogHeader.tsx +23 -4
- package/src/Dialog/modules/DialogHeader.spec.md +152 -0
- package/src/FileInput/FileInput.doc.mjs +2 -0
- package/src/FileInput/FileInput.spec.md +199 -0
- package/src/FileInput/FileInput.test.tsx +14 -0
- package/src/FileInput/FileInput.tsx +13 -3
- package/src/Markdown/Markdown.doc.mjs +167 -42
- package/src/Markdown/Markdown.public.test.ts +157 -0
- package/src/Markdown/Markdown.spec.md +149 -70
- package/src/Markdown/Markdown.test.tsx +107 -3
- package/src/Markdown/Markdown.tsx +116 -35
- package/src/Markdown/incremental.test.ts +175 -7
- package/src/Markdown/index.ts +6 -0
- package/src/Markdown/parser.perf.test.ts +3 -1
- package/src/Markdown/parser.test.ts +122 -0
- package/src/Markdown/parser.ts +609 -81
- package/src/Markdown/utils.ts +6 -0
- package/src/ScrollableArea/modules/useScrollableArea.spec.md +50 -22
- package/src/Slider/Slider.doc.mjs +16 -0
- package/src/Slider/Slider.spec.md +61 -47
- package/src/Slider/Slider.test.tsx +18 -0
- package/src/Slider/Slider.tsx +12 -6
- package/src/Spinner/Spinner.doc.mjs +6 -3
- package/src/Spinner/Spinner.test.tsx +37 -0
- package/src/Spinner/Spinner.tsx +31 -14
- package/src/hooks/scrollKeyboardDelegation.test.ts +155 -0
- package/src/hooks/scrollKeyboardDelegation.ts +233 -0
- package/src/hooks/useScrollableArea.doc.mjs +15 -3
- package/src/hooks/useScrollableArea.test.tsx +59 -1
- package/src/hooks/useScrollableArea.ts +34 -10
- package/src/theme/derivedVarRegistry.test.ts +6 -4
package/dist/Spinner/Spinner.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @file Spinner.tsx
|
|
7
|
-
* @input Uses React, StyleX, SVG rendering
|
|
7
|
+
* @input Uses React, i18n (useTranslator), StyleX, SVG rendering
|
|
8
8
|
* @output Exports Spinner component, SpinnerProps, SpinnerSize, SpinnerShade types
|
|
9
9
|
* @position Core implementation of spinner loading indicator
|
|
10
10
|
*
|
|
@@ -21,6 +21,7 @@ import "../theme/tokens.stylex.js";
|
|
|
21
21
|
import { colorVars, durationVars, spacingVars } from "../theme/tokens.stylex.js";
|
|
22
22
|
import { Text } from "../Text/Text.js";
|
|
23
23
|
import { mergeProps } from "../utils/index.js";
|
|
24
|
+
import { useTranslator } from "../i18n/index.js";
|
|
24
25
|
import { themeProps } from "../utils/themeProps.js";
|
|
25
26
|
|
|
26
27
|
// =============================================================================
|
|
@@ -28,21 +29,17 @@ import { themeProps } from "../utils/themeProps.js";
|
|
|
28
29
|
// =============================================================================
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
|
-
*
|
|
32
|
-
* swept 135deg, not the 270deg its constant's comment claimed.
|
|
32
|
+
* Default fraction of the ring the moving arc covers. The canvas ring this
|
|
33
|
+
* replaces swept 135deg, not the 270deg its constant's comment claimed.
|
|
34
|
+
*
|
|
35
|
+
* Themeable via `--spinner-arc-fraction`, declared alongside the other public
|
|
36
|
+
* vars in `sizeStyles` below. Only the inline `strokeDasharray` attribute
|
|
37
|
+
* (the pre-stylesheet render — see its own comment) still reads this
|
|
38
|
+
* constant directly; the CSS side composes the dash from the live var.
|
|
33
39
|
*/
|
|
34
40
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
35
41
|
const ARC_FRACTION = 0.375;
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* The dash pattern, per unit of diameter: one arc, then the gap that closes
|
|
39
|
-
* the circle. The circumference is `pi x diameter`, so multiplying the
|
|
40
|
-
* resolved diameter by these two constants gives exactly the lengths the
|
|
41
|
-
* default render has always used, and scales them with a themed diameter.
|
|
42
|
-
*/
|
|
43
42
|
const PI = 3.141592653589793;
|
|
44
|
-
const ARC_DASH = PI * ARC_FRACTION;
|
|
45
|
-
const ARC_GAP = PI * (1 - ARC_FRACTION);
|
|
46
43
|
const SIZES = {
|
|
47
44
|
sm: {
|
|
48
45
|
diameter: 10,
|
|
@@ -269,25 +266,33 @@ const styles = {
|
|
|
269
266
|
// cost of its own — with nothing declaring the var,
|
|
270
267
|
// `theme-var-reachability.js` cannot find an element to check, so a documented
|
|
271
268
|
// var reads as unreachable.
|
|
269
|
+
// Arc fraction is not itself size-dependent, but it declares alongside the
|
|
270
|
+
// two vars that are, on the same per-size condition (`!hasLabel` gate at the
|
|
271
|
+
// call site) — it needs a home on whichever element carries the theme
|
|
272
|
+
// target, and this is the object already wired to be there.
|
|
272
273
|
const sizeStyles = {
|
|
273
274
|
sm: {
|
|
274
275
|
"--spinner-diameter": "x11wm0hx",
|
|
275
276
|
"--spinner-stroke-width": "xls98ul",
|
|
277
|
+
"--spinner-arc-fraction": "x7o5821",
|
|
276
278
|
$$css: true
|
|
277
279
|
},
|
|
278
280
|
md: {
|
|
279
281
|
"--spinner-diameter": "x15pu9g6",
|
|
280
282
|
"--spinner-stroke-width": "xr0wkrm",
|
|
283
|
+
"--spinner-arc-fraction": "x7o5821",
|
|
281
284
|
$$css: true
|
|
282
285
|
},
|
|
283
286
|
lg: {
|
|
284
287
|
"--spinner-diameter": "x1w424tr",
|
|
285
288
|
"--spinner-stroke-width": "xr0wkrm",
|
|
289
|
+
"--spinner-arc-fraction": "x7o5821",
|
|
286
290
|
$$css: true
|
|
287
291
|
},
|
|
288
292
|
xl: {
|
|
289
293
|
"--spinner-diameter": "x1orj1z9",
|
|
290
294
|
"--spinner-stroke-width": "x7y2bof",
|
|
295
|
+
"--spinner-arc-fraction": "x7o5821",
|
|
291
296
|
$$css: true
|
|
292
297
|
}
|
|
293
298
|
};
|
|
@@ -377,6 +382,7 @@ export function Spinner({
|
|
|
377
382
|
const arcLength = circumference * ARC_FRACTION;
|
|
378
383
|
const hasLabel = label != null;
|
|
379
384
|
const labelId = useId();
|
|
385
|
+
const t = useTranslator();
|
|
380
386
|
|
|
381
387
|
// When a visible string label renders (and no explicit aria-label is set),
|
|
382
388
|
// name the status element from the visible Text via aria-labelledby instead
|
|
@@ -384,8 +390,10 @@ export function Spinner({
|
|
|
384
390
|
// announced twice by screen readers (WCAG 4.1.2).
|
|
385
391
|
const namedByVisibleLabel = hasLabel && typeof label === 'string' && ariaLabel == null;
|
|
386
392
|
|
|
387
|
-
// Resolve accessible name: explicit aria-label > string label >
|
|
388
|
-
|
|
393
|
+
// Resolve accessible name: explicit aria-label > string label > the
|
|
394
|
+
// localized default. The fallback is AT-facing text, so it goes through the
|
|
395
|
+
// translation runtime like visible text does.
|
|
396
|
+
const resolvedAriaLabel = ariaLabel ?? (typeof label === 'string' ? label : undefined) ?? t('@astryx.spinner.loading');
|
|
389
397
|
const spinner = /*#__PURE__*/_jsx("span", {
|
|
390
398
|
ref: hasLabel ? undefined : ref,
|
|
391
399
|
role: "status",
|
|
@@ -440,7 +448,7 @@ export function Spinner({
|
|
|
440
448
|
,
|
|
441
449
|
strokeDasharray: `${arcLength} ${circumference - arcLength}`,
|
|
442
450
|
...{
|
|
443
|
-
className: "xbh8q5q x1764fhq x1g0ag68 x1owpc8m xio8zfp xgw3ha0 xtve3lm x9tu13d
|
|
451
|
+
className: "xbh8q5q x1764fhq x1g0ag68 x1owpc8m xio8zfp xgw3ha0 xtve3lm x9tu13d xdv9ggb"
|
|
444
452
|
}
|
|
445
453
|
})]
|
|
446
454
|
})
|
package/dist/astryx.css
CHANGED
|
@@ -333,6 +333,7 @@
|
|
|
333
333
|
.xnpej1e{--radius-element:var(--radius-full)}
|
|
334
334
|
.x11rj6l1{--selectable-card-ring-color:var(--color-accent)}
|
|
335
335
|
.xkce8z9{--separator-display:flex}
|
|
336
|
+
.x7o5821{--spinner-arc-fraction:.375}
|
|
336
337
|
.x1uzk0gl{--spinner-color:currentColor}
|
|
337
338
|
.xt1b8mc{--spinner-color:var(--color-accent)}
|
|
338
339
|
.x13u6jys{--spinner-color:var(--color-on-dark)}
|
|
@@ -515,7 +516,6 @@
|
|
|
515
516
|
.xb3r6kr:not(#\#):not(#\#){overflow:hidden}
|
|
516
517
|
.x1rea2x4:not(#\#):not(#\#){overflow:visible}
|
|
517
518
|
.xish69e:not(#\#):not(#\#){overscroll-behavior:contain}
|
|
518
|
-
.x8du1vd:not(#\#):not(#\#){overscroll-behavior:none}
|
|
519
519
|
.xt970qd:not(#\#):not(#\#){padding-block:0}
|
|
520
520
|
.x1vn5h1t:not(#\#):not(#\#){padding-block:calc((var(--size-element-lg) - var(--spacing-5) - 2 * var(--border-width)) / 2)}
|
|
521
521
|
.x1ioay2v:not(#\#):not(#\#){padding-block:calc((var(--size-element-md) - var(--spacing-5) - 2 * var(--border-width)) / 2)}
|
|
@@ -866,6 +866,7 @@
|
|
|
866
866
|
.x1lliihq:not(#\#):not(#\#):not(#\#){display:block}
|
|
867
867
|
.xjp7ctv:not(#\#):not(#\#):not(#\#){display:contents}
|
|
868
868
|
.x78zum5:not(#\#):not(#\#):not(#\#){display:flex}
|
|
869
|
+
.xufnvkl:not(#\#):not(#\#):not(#\#){display:flow-root}
|
|
869
870
|
.xrvj5dj:not(#\#):not(#\#):not(#\#){display:grid}
|
|
870
871
|
.x1rg5ohu:not(#\#):not(#\#):not(#\#){display:inline-block}
|
|
871
872
|
.x3nfvp2:not(#\#):not(#\#):not(#\#){display:inline-flex}
|
|
@@ -1173,7 +1174,7 @@
|
|
|
1173
1174
|
.x24f09q:not(#\#):not(#\#):not(#\#){scrollbar-gutter:auto}
|
|
1174
1175
|
.xosa0jh:not(#\#):not(#\#):not(#\#){scrollbar-width:auto}
|
|
1175
1176
|
.x1rohswg:not(#\#):not(#\#):not(#\#){scrollbar-width:none}
|
|
1176
|
-
.
|
|
1177
|
+
.xdv9ggb:not(#\#):not(#\#):not(#\#){stroke-dasharray:calc(var(--_spinner-ring-diameter) * 3.141592653589793 * var(--spinner-arc-fraction)) calc(var(--_spinner-ring-diameter) * 3.141592653589793 * (1 - var(--spinner-arc-fraction)))}
|
|
1177
1178
|
.x1owpc8m:not(#\#):not(#\#):not(#\#){stroke-linecap:round}
|
|
1178
1179
|
.x7bo2k:not(#\#):not(#\#):not(#\#){stroke-opacity:.3}
|
|
1179
1180
|
.x1smxkh6:not(#\#):not(#\#):not(#\#){stroke-opacity:.302}
|
|
@@ -1362,6 +1363,7 @@
|
|
|
1362
1363
|
.x1e0x2sz:focus-visible:not(#\#):not(#\#):not(#\#){outline-color:var(--color-error)}
|
|
1363
1364
|
.xx47ajj:focus-visible:not(#\#):not(#\#):not(#\#){outline-color:var(--focus-outline-color)}
|
|
1364
1365
|
.x7s97pk:focus-visible:not(#\#):not(#\#):not(#\#){outline-offset:2px}
|
|
1366
|
+
.x1nw7mhf:focus-visible:not(#\#):not(#\#):not(#\#){outline-offset:calc(-1 * var(--focus-outline-width))}
|
|
1365
1367
|
.x13aywxo:focus-visible:not(#\#):not(#\#):not(#\#){outline-offset:var(--button-focus-offset)}
|
|
1366
1368
|
.x1vwwbsn:focus-visible:not(#\#):not(#\#):not(#\#){outline-offset:var(--focus-outline-offset)}
|
|
1367
1369
|
.xwz5zln.xwz5zln:where(.x1iwu4tg:has(:focus-visible) *):not(#\#):not(#\#):not(#\#){outline-offset:var(--focus-outline-offset)}
|
|
@@ -1675,6 +1677,7 @@
|
|
|
1675
1677
|
.xjwf9q1:not(#\#):not(#\#):not(#\#):not(#\#){min-height:24px}
|
|
1676
1678
|
.xe0p6wg:not(#\#):not(#\#):not(#\#):not(#\#){min-height:28px}
|
|
1677
1679
|
.x21xpn4:not(#\#):not(#\#):not(#\#):not(#\#){min-height:32px}
|
|
1680
|
+
.x121v3j4:not(#\#):not(#\#):not(#\#):not(#\#){min-height:min-content}
|
|
1678
1681
|
.x1xf4nls:not(#\#):not(#\#):not(#\#):not(#\#){min-height:var(--size-element-lg)}
|
|
1679
1682
|
.x1uogy3y:not(#\#):not(#\#):not(#\#):not(#\#){min-height:var(--size-element-md)}
|
|
1680
1683
|
.x1gdqauq:not(#\#):not(#\#):not(#\#):not(#\#){min-height:var(--size-element-sm)}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scrollKeyboardDelegation.d.ts","sourceRoot":"","sources":["../../src/hooks/scrollKeyboardDelegation.ts"],"names":[],"mappings":"AAkIA,0EAA0E;AAC1E,wBAAgB,8BAA8B,CAC5C,QAAQ,EAAE,WAAW,EACrB,OAAO,EAAE,WAAW,GACnB,MAAM,IAAI,CAkGZ"}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file scrollKeyboardDelegation.ts
|
|
5
|
+
* @input A named, overflowing viewport and its real content box
|
|
6
|
+
* @output Focus-time delegation with native forward and reverse Tab traversal
|
|
7
|
+
* @position Private useScrollableArea keyboard behavior; no DOM observers or scrolling
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { FOCUSABLE_SELECTOR } from "./focusableSelector.js";
|
|
11
|
+
import { getRegisteredScrollOwnerState } from "./scrollOwnerRegistry.js";
|
|
12
|
+
|
|
13
|
+
// Structural roles may contain ordinary links/buttons. Unknown or interactive
|
|
14
|
+
// roles are deliberately excluded until their navigation contract is proven.
|
|
15
|
+
const PASSIVE_ROLES = new Set(['article', 'banner', 'complementary', 'contentinfo', 'definition', 'directory', 'document', 'figure', 'form', 'generic', 'group', 'heading', 'list', 'listitem', 'main', 'navigation', 'none', 'note', 'paragraph', 'presentation', 'region', 'section', 'status', 'table', 'term']);
|
|
16
|
+
function isSequential(element) {
|
|
17
|
+
if (element.tabIndex < 0 || element.matches(':disabled') || element.closest('[inert], [hidden]') != null || element.getClientRects().length === 0) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
const visibility = getComputedStyle(element).visibility;
|
|
21
|
+
return visibility !== 'hidden' && visibility !== 'collapse';
|
|
22
|
+
}
|
|
23
|
+
function firstDelegationTarget(viewport, content) {
|
|
24
|
+
const candidates = content.querySelectorAll(FOCUSABLE_SELECTOR);
|
|
25
|
+
let first = content.matches(FOCUSABLE_SELECTOR) && isSequential(content) ? content : null;
|
|
26
|
+
for (const candidate of candidates) {
|
|
27
|
+
// After finding the first target, only positive tabindex can precede it.
|
|
28
|
+
// Ordinary trailing controls need no visibility or layout reads.
|
|
29
|
+
if (first != null && candidate.tabIndex <= 0) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (!isSequential(candidate)) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
// Positive tabindex participates before the zero-index viewport. Do not
|
|
36
|
+
// revisit it or skip its ordering by delegating to a later zero-index child.
|
|
37
|
+
if (candidate.tabIndex > 0) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
first ??= candidate;
|
|
41
|
+
}
|
|
42
|
+
if (first == null || first.tabIndex !== 0 || !first.matches('a[href], button') || first.closest('[aria-hidden="true"]') != null || first.matches('[aria-disabled="true"], [aria-haspopup]:not([aria-haspopup="false"])')) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
let ancestor = first;
|
|
46
|
+
while (ancestor != null && ancestor !== viewport) {
|
|
47
|
+
const role = ancestor.getAttribute('role')?.trim().toLowerCase();
|
|
48
|
+
const nativeRole = ancestor === first && (role === 'button' && first.localName === 'button' || role === 'link' && first.localName === 'a');
|
|
49
|
+
if (role != null && role !== '' && !nativeRole && !PASSIVE_ROLES.has(role) || ancestor === first && role != null && role !== '' && !nativeRole || ancestor.hasAttribute('aria-activedescendant') || ancestor.isContentEditable || ancestor.matches('[contenteditable]:not([contenteditable="false"])') || getRegisteredScrollOwnerState(ancestor) != null) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
// Also respect native scroll owners that have not adopted the shared hook.
|
|
53
|
+
const style = getComputedStyle(ancestor);
|
|
54
|
+
if (/^(auto|scroll|overlay)$/.test(style.overflowX) && ancestor.scrollWidth > ancestor.clientWidth + 1 || /^(auto|scroll|overlay)$/.test(style.overflowY) && ancestor.scrollHeight > ancestor.clientHeight + 1) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
ancestor = ancestor.parentElement;
|
|
58
|
+
}
|
|
59
|
+
return ancestor === viewport ? first : null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Attach only while automatic ownership has an effective scroll axis. */
|
|
63
|
+
export function attachScrollKeyboardDelegation(viewport, content) {
|
|
64
|
+
const document = viewport.ownerDocument;
|
|
65
|
+
const window = document.defaultView;
|
|
66
|
+
if (window == null) {
|
|
67
|
+
return () => {};
|
|
68
|
+
}
|
|
69
|
+
let entry = null;
|
|
70
|
+
let origin = null;
|
|
71
|
+
let delegated = null;
|
|
72
|
+
let timer;
|
|
73
|
+
let restoreTabStop;
|
|
74
|
+
const reset = () => {
|
|
75
|
+
entry = null;
|
|
76
|
+
origin = null;
|
|
77
|
+
window.clearTimeout(timer);
|
|
78
|
+
restoreTabStop?.();
|
|
79
|
+
restoreTabStop = undefined;
|
|
80
|
+
};
|
|
81
|
+
const onKeyDown = event => {
|
|
82
|
+
reset();
|
|
83
|
+
if (event.key !== 'Tab' || event.ctrlKey || event.metaKey) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
// Keep the actual event: focus() during keydown is programmatic (nonzero
|
|
87
|
+
// eventPhase), whereas native Tab focus follows completed event dispatch.
|
|
88
|
+
entry = event;
|
|
89
|
+
origin = event.target;
|
|
90
|
+
if (event.shiftKey && delegated != null && document.activeElement === delegated && viewport.contains(delegated)) {
|
|
91
|
+
const tabIndex = viewport.getAttribute('tabindex');
|
|
92
|
+
viewport.tabIndex = -1;
|
|
93
|
+
restoreTabStop = () => {
|
|
94
|
+
if (viewport.getAttribute('tabindex') === '-1') {
|
|
95
|
+
if (tabIndex == null) {
|
|
96
|
+
viewport.removeAttribute('tabindex');
|
|
97
|
+
} else {
|
|
98
|
+
viewport.setAttribute('tabindex', tabIndex);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
// A canceled Tab, focus leaving the document, or a held key must not leave
|
|
104
|
+
// stale keyboard intent for a later pointer/programmatic focus operation.
|
|
105
|
+
timer = window.setTimeout(reset, 0);
|
|
106
|
+
};
|
|
107
|
+
const onFocusIn = event => {
|
|
108
|
+
const keyboardEntry = entry;
|
|
109
|
+
const previous = origin;
|
|
110
|
+
reset();
|
|
111
|
+
if (event.target !== viewport) {
|
|
112
|
+
if (!viewport.contains(event.target)) {
|
|
113
|
+
delegated = null;
|
|
114
|
+
}
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
delegated = null;
|
|
118
|
+
if (keyboardEntry == null || keyboardEntry.shiftKey || keyboardEntry.defaultPrevented || keyboardEntry.eventPhase !== 0 || event.relatedTarget != null && event.relatedTarget !== previous) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const target = firstDelegationTarget(viewport, content);
|
|
122
|
+
if (target == null) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
delegated = target;
|
|
126
|
+
// Let native focus reveal the action. Arrow/Page scrolling remains entirely
|
|
127
|
+
// browser-owned, including scroll chaining and the full viewport range.
|
|
128
|
+
target.focus();
|
|
129
|
+
if (document.activeElement !== target) {
|
|
130
|
+
delegated = null;
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
document.addEventListener('keydown', onKeyDown, true);
|
|
134
|
+
document.addEventListener('keyup', reset, true);
|
|
135
|
+
document.addEventListener('pointerdown', reset, true);
|
|
136
|
+
document.addEventListener('focusin', onFocusIn, true);
|
|
137
|
+
window.addEventListener('blur', reset);
|
|
138
|
+
return () => {
|
|
139
|
+
reset();
|
|
140
|
+
document.removeEventListener('keydown', onKeyDown, true);
|
|
141
|
+
document.removeEventListener('keyup', reset, true);
|
|
142
|
+
document.removeEventListener('pointerdown', reset, true);
|
|
143
|
+
document.removeEventListener('focusin', onFocusIn, true);
|
|
144
|
+
window.removeEventListener('blur', reset);
|
|
145
|
+
};
|
|
146
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file useScrollableArea.ts
|
|
3
|
-
* @input Logical scroll intent, keyboard
|
|
4
|
-
* @output Safe prop getters and stable per-axis effective scroll state
|
|
3
|
+
* @input Logical scroll intent, focus-time keyboard delegation, caller-owned viewport/content props
|
|
4
|
+
* @output Safe prop getters, native keyboard entry, and stable per-axis effective scroll state
|
|
5
5
|
* @position Canonical behavior core for ScrollableArea and structure-owning adopters
|
|
6
6
|
*
|
|
7
7
|
* SYNC: When modified, update:
|
|
@@ -20,6 +20,10 @@ export type ScrollKeyboardAccess = {
|
|
|
20
20
|
owner: 'viewport';
|
|
21
21
|
label: string;
|
|
22
22
|
role?: 'group' | 'region';
|
|
23
|
+
} | {
|
|
24
|
+
owner: 'contentOrViewport';
|
|
25
|
+
label: string;
|
|
26
|
+
role?: 'group' | 'region';
|
|
23
27
|
};
|
|
24
28
|
export interface ScrollAxisState {
|
|
25
29
|
isScrollable: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useScrollableArea.d.ts","sourceRoot":"","sources":["../../src/hooks/useScrollableArea.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;GAUG;AAEH,OAAO,EAOL,KAAK,aAAa,EAEnB,MAAM,OAAO,CAAC;AAEf,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"useScrollableArea.d.ts","sourceRoot":"","sources":["../../src/hooks/useScrollableArea.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;GAUG;AAEH,OAAO,EAOL,KAAK,aAAa,EAEnB,MAAM,OAAO,CAAC;AAEf,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAkB5C,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,OAAO,GAAG,MAAM,CAAC;AACrD,MAAM,MAAM,gBAAgB,GAAG,OAAO,GAAG,SAAS,CAAC;AACnD,MAAM,MAAM,uBAAuB,GAAG,gBAAgB,GAAG,QAAQ,CAAC;AAElE,MAAM,MAAM,oBAAoB,GAC5B;IAAC,KAAK,EAAE,SAAS,CAAA;CAAC,GAClB;IAAC,KAAK,EAAE,UAAU,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAA;CAAC,GAC7D;IACE,KAAK,EAAE,mBAAmB,CAAC;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC;CAC3B,CAAC;AAEN,MAAM,WAAW,eAAe;IAC9B,YAAY,EAAE,OAAO,CAAC;IACtB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,eAAe,CAAC;IACxB,KAAK,EAAE,eAAe,CAAC;CACxB;AAED,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,UAAU,CAAC;IACjB,cAAc,EAAE,oBAAoB,CAAC;IACrC,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,iBAAiB,CAAC,EAAE,uBAAuB,CAAC;CAC7C;AAED,MAAM,MAAM,sBAAsB,CAAC,CAAC,SAAS,WAAW,IAAI,SAAS,CAAC,CAAC,CAAC,GACtE,aAAa,CAAC,CAAC,CAAC,CAAC;AAEnB,MAAM,WAAW,uBAAuB;IACtC,gBAAgB,CAAC,CAAC,SAAS,WAAW,EACpC,KAAK,CAAC,EAAE,sBAAsB,CAAC,CAAC,CAAC,GAChC,sBAAsB,CAAC,CAAC,CAAC,CAAC;IAC7B,eAAe,CAAC,CAAC,SAAS,WAAW,EACnC,KAAK,CAAC,EAAE,sBAAsB,CAAC,CAAC,CAAC,GAChC,sBAAsB,CAAC,CAAC,CAAC,CAAC;IAC7B,KAAK,EAAE,mBAAmB,CAAC;CAC5B;AAiGD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,IAAI,EACJ,cAAc,EACd,UAAoB,EACpB,iBAAoC,GACrC,EAAE,wBAAwB,GAAG,uBAAuB,CA0SpD"}
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @file useScrollableArea.ts
|
|
7
|
-
* @input Logical scroll intent, keyboard
|
|
8
|
-
* @output Safe prop getters and stable per-axis effective scroll state
|
|
7
|
+
* @input Logical scroll intent, focus-time keyboard delegation, caller-owned viewport/content props
|
|
8
|
+
* @output Safe prop getters, native keyboard entry, and stable per-axis effective scroll state
|
|
9
9
|
* @position Canonical behavior core for ScrollableArea and structure-owning adopters
|
|
10
10
|
*
|
|
11
11
|
* SYNC: When modified, update:
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { useCallback, useMemo, useRef, useState } from 'react';
|
|
17
17
|
import * as stylex from '@stylexjs/stylex';
|
|
18
|
+
import { attachScrollKeyboardDelegation } from "./scrollKeyboardDelegation.js";
|
|
18
19
|
import { useIsomorphicLayoutEffect } from "./useIsomorphicLayoutEffect.js";
|
|
19
20
|
import { mergeProps } from "../utils/mergeProps.js";
|
|
20
21
|
import { mergeRefs } from "../utils/mergeRefs.js";
|
|
@@ -172,6 +173,9 @@ export function useScrollableArea({
|
|
|
172
173
|
viewport.addEventListener('animationend', scheduleMeasure);
|
|
173
174
|
content.addEventListener('transitionend', scheduleMeasure);
|
|
174
175
|
content.addEventListener('animationend', scheduleMeasure);
|
|
176
|
+
|
|
177
|
+
// Geometry invalidation is independent of keyboard eligibility. The latter
|
|
178
|
+
// is inspected only on native keyboard entry, never by these observers.
|
|
175
179
|
const mutationObserver = typeof MutationObserver === 'undefined' ? null : new MutationObserver(scheduleMeasure);
|
|
176
180
|
mutationObserver?.observe(viewport, {
|
|
177
181
|
attributes: true,
|
|
@@ -219,6 +223,13 @@ export function useScrollableArea({
|
|
|
219
223
|
inline: axisIsRequested(axis, 'inline') ? measured.inline : INACTIVE_AXIS_STATE,
|
|
220
224
|
block: axisIsRequested(axis, 'block') ? measured.block : INACTIVE_AXIS_STATE
|
|
221
225
|
}), [axis, measured]);
|
|
226
|
+
const hasEffectiveAxis = state.inline.isScrollable || state.block.isScrollable;
|
|
227
|
+
useIsomorphicLayoutEffect(() => {
|
|
228
|
+
if (keyboardAccess.owner !== 'contentOrViewport' || !hasEffectiveAxis || viewport == null || content == null) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
return attachScrollKeyboardDelegation(viewport, content);
|
|
232
|
+
}, [content, hasEffectiveAxis, keyboardAccess.owner, viewport]);
|
|
222
233
|
const getViewportProps = useCallback((props = {}) => {
|
|
223
234
|
const hasEffectiveAxis = state.inline.isScrollable || state.block.isScrollable;
|
|
224
235
|
const requestedX = axisIsRequested(axis, 'inline') && mapping.inline === 'x' || axisIsRequested(axis, 'block') && mapping.block === 'x';
|
|
@@ -237,7 +248,8 @@ export function useScrollableArea({
|
|
|
237
248
|
style: withoutOwnedOverflow(domProps.style)
|
|
238
249
|
};
|
|
239
250
|
const styledProps = mergeProps(callerProps, stylex.props(xstyle, styles.overflow(containsSticky ? scrollsOnX ? 'auto' : 'hidden' : 'clip', containsSticky ? scrollsOnY ? 'auto' : 'hidden' : 'clip')));
|
|
240
|
-
const
|
|
251
|
+
const viewportIsKeyboardOwner = keyboardAccess.owner !== 'content';
|
|
252
|
+
const keepsProgrammaticFocus = (!viewportIsKeyboardOwner || !hasEffectiveAxis) && viewport != null && typeof document !== 'undefined' && document.activeElement === viewport;
|
|
241
253
|
const behaviorStyle = {
|
|
242
254
|
...styledProps.style
|
|
243
255
|
};
|
|
@@ -251,10 +263,10 @@ export function useScrollableArea({
|
|
|
251
263
|
if (axisIsRequested(axis, 'block')) {
|
|
252
264
|
behaviorStyle[blockPhysical === 'x' ? 'overscrollBehaviorX' : 'overscrollBehaviorY'] = containBlock ? 'contain' : 'auto';
|
|
253
265
|
}
|
|
254
|
-
const keyboardProps = keyboardAccess.owner
|
|
266
|
+
const keyboardProps = keyboardAccess.owner !== 'content' ? {
|
|
255
267
|
role: keyboardAccess.role ?? 'group',
|
|
256
268
|
'aria-label': keyboardAccess.label,
|
|
257
|
-
tabIndex: hasEffectiveAxis ? 0 : keepsProgrammaticFocus ? -1 : undefined
|
|
269
|
+
tabIndex: viewportIsKeyboardOwner && hasEffectiveAxis ? 0 : keepsProgrammaticFocus ? -1 : undefined
|
|
258
270
|
} : {};
|
|
259
271
|
const result = {
|
|
260
272
|
...styledProps,
|
package/locales/en.json
CHANGED
|
@@ -447,6 +447,18 @@
|
|
|
447
447
|
"defaultMessage": "Next",
|
|
448
448
|
"description": "Screen-reader-only label on the right-arrow button in a Lightbox (navigates to next media item). Pairs with `lightbox.previous`."
|
|
449
449
|
},
|
|
450
|
+
"@astryx.chatTypingIndicator.one": {
|
|
451
|
+
"defaultMessage": "{name} is typing…",
|
|
452
|
+
"description": "Politely announced status naming the single person currently typing in a chat. `name` is a display name supplied by the app. Present-progressive form; the trailing ellipsis is the single … character."
|
|
453
|
+
},
|
|
454
|
+
"@astryx.chatTypingIndicator.many": {
|
|
455
|
+
"defaultMessage": "{names} are typing…",
|
|
456
|
+
"description": "Politely announced status when more than one person is typing in a chat. `names` is already joined for the locale by Intl.ListFormat, so translations must not add their own conjunction — place `{names}` where the joined list belongs."
|
|
457
|
+
},
|
|
458
|
+
"@astryx.chatTypingIndicator.others": {
|
|
459
|
+
"defaultMessage": "{count, number} {count, plural, one {other} other {others}}",
|
|
460
|
+
"description": "Overflow element for the chat typing status when three or more people type: the first name is shown and the rest collapse into this phrase, which is then joined to the name by Intl.ListFormat. Renders as the last list item, e.g. \"Ana and 2 others\"."
|
|
461
|
+
},
|
|
450
462
|
"@astryx.listInput.emptyTitle": {
|
|
451
463
|
"defaultMessage": "No {itemName}s yet",
|
|
452
464
|
"description": "EmptyState title shown inside a lab ListInput when its collection has no records. `{itemName}` is the consumer's singular noun for one record (e.g. \"guest\"); the source appends a literal \"s\" to pluralize it, which only works for regular English plurals. If your language cannot pluralize an interpolated noun this way, rephrase around `{itemName}` instead (e.g. \"No {itemName} added yet\")."
|
|
@@ -935,6 +947,10 @@
|
|
|
935
947
|
"defaultMessage": "Loading",
|
|
936
948
|
"description": "Screen-reader-only live-region announcement while a Button is in its loading state (spinner shown, action in flight). Progressive sense (\"work in progress\"), not a noun. Kept separate from `typeahead.loading` — translations may diverge."
|
|
937
949
|
},
|
|
950
|
+
"@astryx.spinner.loading": {
|
|
951
|
+
"defaultMessage": "Loading",
|
|
952
|
+
"description": "Screen-reader-only default name for a standalone Spinner's role=\"status\" element, used when the consumer supplies neither `aria-label` nor a visible string label. Present-progressive form. Kept separate from `button.loading` and `typeahead.loading` — translations may diverge."
|
|
953
|
+
},
|
|
938
954
|
"@astryx.chatComposerDrawer.expand": {
|
|
939
955
|
"defaultMessage": "Expand {label}",
|
|
940
956
|
"description": "Screen-reader-only label on the ChatComposerDrawer toggle when the drawer is collapsed. `{label}` is the drawer's visible name — example: `Expand Attachments`. Pairs with `collapse`."
|
package/locales/pseudo.json
CHANGED
|
@@ -335,6 +335,15 @@
|
|
|
335
335
|
"@astryx.lightbox.next": {
|
|
336
336
|
"defaultMessage": "⟦Ñéẋţ⟧"
|
|
337
337
|
},
|
|
338
|
+
"@astryx.chatTypingIndicator.one": {
|
|
339
|
+
"defaultMessage": "⟦{name} íš ţýþíñĝ…⟧"
|
|
340
|
+
},
|
|
341
|
+
"@astryx.chatTypingIndicator.many": {
|
|
342
|
+
"defaultMessage": "⟦{names} àřé ţýþíñĝ…⟧"
|
|
343
|
+
},
|
|
344
|
+
"@astryx.chatTypingIndicator.others": {
|
|
345
|
+
"defaultMessage": "⟦{count, number} {count, plural, one {other} other {others}}⟧"
|
|
346
|
+
},
|
|
338
347
|
"@astryx.listInput.emptyTitle": {
|
|
339
348
|
"defaultMessage": "⟦Ñó {itemName}š ýéţ⟧"
|
|
340
349
|
},
|
|
@@ -701,6 +710,9 @@
|
|
|
701
710
|
"@astryx.button.loading": {
|
|
702
711
|
"defaultMessage": "⟦Łóàðíñĝ⟧"
|
|
703
712
|
},
|
|
713
|
+
"@astryx.spinner.loading": {
|
|
714
|
+
"defaultMessage": "⟦Łóàðíñĝ⟧"
|
|
715
|
+
},
|
|
704
716
|
"@astryx.chatComposerDrawer.expand": {
|
|
705
717
|
"defaultMessage": "⟦Éẋþàñð {label}⟧"
|
|
706
718
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astryxdesign/core",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2-canary.605fa9a",
|
|
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",
|
|
@@ -672,16 +672,18 @@
|
|
|
672
672
|
"react-dom": ">=19.0.0"
|
|
673
673
|
},
|
|
674
674
|
"devDependencies": {
|
|
675
|
-
"@
|
|
675
|
+
"@astryxdesign/a11y-spec": "0.6.2-canary.605fa9a",
|
|
676
|
+
"@astryxdesign/cli": "0.6.2-canary.605fa9a",
|
|
677
|
+
"@babel/cli": "^7.29.7",
|
|
676
678
|
"@babel/core": "^7.29.7",
|
|
677
|
-
"@babel/preset-react": "^
|
|
679
|
+
"@babel/preset-react": "^7.29.7",
|
|
678
680
|
"@babel/preset-typescript": "^7.29.7",
|
|
681
|
+
"@heroicons/react": "^2.2.0",
|
|
679
682
|
"@stylexjs/babel-plugin": "^0.19.0",
|
|
680
683
|
"@testing-library/dom": "^10.0.0",
|
|
681
684
|
"@testing-library/jest-dom": "^6.6.0",
|
|
682
685
|
"@testing-library/react": "^16.3.2",
|
|
683
|
-
"@
|
|
684
|
-
"@astryxdesign/cli": "0.6.1"
|
|
686
|
+
"@types/babel__core": "^7.20.5"
|
|
685
687
|
},
|
|
686
688
|
"dependencies": {
|
|
687
689
|
"intl-messageformat": "^11.2.9"
|
|
@@ -57,7 +57,7 @@ export const LEGACY_MARKER_END = '<!-- XDS:END -->';
|
|
|
57
57
|
* targets are user-chosen and not enumerable here.)
|
|
58
58
|
*/
|
|
59
59
|
export const AGENT_DOC_PATHS = [
|
|
60
|
-
AGENTS_MD, // Codex / ChatGPT / generic
|
|
60
|
+
AGENTS_MD, // Codex / ChatGPT / Muse / generic
|
|
61
61
|
CLAUDE_MD, // Claude Code (root)
|
|
62
62
|
CLAUDE_DIR_MD, // Claude Code (.claude/CLAUDE.md)
|
|
63
63
|
CURSOR_RULES, // Cursor
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* @file BottomSheet.doc.mjs
|
|
5
|
+
* @input BottomSheet props, sheet anatomy, and focus-time keyboard scrolling behavior
|
|
6
|
+
* @output Consumer documentation and examples for BottomSheet
|
|
7
|
+
* @position CLI and rendered component documentation
|
|
8
|
+
*/
|
|
9
|
+
|
|
3
10
|
/** @type {import('@astryxdesign/cli/authoring').ComponentAnatomyElement[]} */
|
|
4
11
|
const anatomy = [
|
|
5
12
|
{
|
|
@@ -130,7 +137,7 @@ export const docs = {
|
|
|
130
137
|
name: 'children',
|
|
131
138
|
type: 'ReactNode',
|
|
132
139
|
description:
|
|
133
|
-
"Sheet content,
|
|
140
|
+
"Sheet content in a scrollable area. The named body is keyboard reachable while overflowing. Forward Tab entry may move directly to the first native link or button; input controls, composite widgets, and nested scroll areas retain the body stop. Shift+Tab from a delegated first child skips the body. Fitting content adds no body stop. The internal observed content box preserves block flow and percentage heights. If it includes a text-entry control that can bring up the mobile keyboard, use height='tall' and keep the sheet fully expanded while editing.",
|
|
134
141
|
required: true,
|
|
135
142
|
},
|
|
136
143
|
{
|