@astryxdesign/core 0.1.7-canary.eb8e07b → 0.1.7-canary.fb9c68c
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/README.md +24 -15
- package/dist/Collapsible/Collapsible.d.ts +11 -1
- package/dist/Collapsible/Collapsible.d.ts.map +1 -1
- package/dist/Collapsible/Collapsible.js +36 -4
- package/dist/Outline/Outline.d.ts +69 -3
- package/dist/Outline/Outline.d.ts.map +1 -1
- package/dist/Outline/Outline.js +129 -26
- package/dist/Outline/useScrollSpy.d.ts +27 -7
- package/dist/Outline/useScrollSpy.d.ts.map +1 -1
- package/dist/Outline/useScrollSpy.js +202 -70
- package/dist/TabList/Tab.d.ts.map +1 -1
- package/dist/TabList/Tab.js +2 -2
- package/dist/TabList/TabList.d.ts.map +1 -1
- package/dist/TabList/TabList.js +2 -0
- package/dist/TabList/TabMenu.d.ts.map +1 -1
- package/dist/TabList/TabMenu.js +1 -1
- package/dist/Table/index.d.ts +2 -0
- package/dist/Table/index.d.ts.map +1 -1
- package/dist/Table/index.js +1 -0
- package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts.map +1 -1
- package/dist/Table/plugins/rowExpansion/useTableRowExpansion.js +24 -2
- package/dist/Table/plugins/sortable/useTableSortable.d.ts.map +1 -1
- package/dist/Table/plugins/sortable/useTableSortable.js +18 -7
- package/dist/Table/plugins/tree/index.d.ts +5 -0
- package/dist/Table/plugins/tree/index.d.ts.map +1 -0
- package/dist/Table/plugins/tree/index.js +4 -0
- package/dist/Table/plugins/tree/useTableTreeData.d.ts +38 -0
- package/dist/Table/plugins/tree/useTableTreeData.d.ts.map +1 -0
- package/dist/Table/plugins/tree/useTableTreeData.js +358 -0
- package/dist/Table/plugins/tree/useTableTreeState.d.ts +51 -0
- package/dist/Table/plugins/tree/useTableTreeState.d.ts.map +1 -0
- package/dist/Table/plugins/tree/useTableTreeState.js +175 -0
- package/dist/Table/useBaseTablePlugins.d.ts.map +1 -1
- package/dist/Table/useBaseTablePlugins.js +6 -3
- package/dist/Toolbar/Toolbar.d.ts.map +1 -1
- package/dist/Toolbar/Toolbar.js +8 -1
- package/dist/astryx.css +3 -1
- package/dist/astryx.umd.js +43 -43
- package/dist/astryx.umd.js.map +4 -4
- package/dist/theme/Theme.js +1 -1
- package/dist/theme/defineTheme.d.ts +1 -1
- package/dist/theme/defineTheme.js +1 -1
- package/dist/theme/hct.d.ts +0 -1
- package/dist/theme/hct.d.ts.map +1 -1
- package/dist/theme/hct.js +0 -3
- package/locales/en.json +28 -0
- package/locales/pseudo.json +21 -0
- package/package.json +3 -1
- package/scripts/postinstall.mjs +93 -0
- package/src/CheckboxInput/CheckboxInput.doc.mjs +1 -1
- package/src/Collapsible/Collapsible.doc.mjs +7 -0
- package/src/Collapsible/Collapsible.test.tsx +80 -0
- package/src/Collapsible/Collapsible.tsx +58 -5
- package/src/DropdownMenu/DropdownMenu.doc.mjs +13 -0
- package/src/DropdownMenu/DropdownMenuItem.doc.mjs +7 -1
- package/src/Field/Field.doc.mjs +1 -1
- package/src/Field/FieldLabel.doc.mjs +1 -1
- package/src/NumberInput/NumberInput.doc.mjs +2 -2
- package/src/Outline/Outline.doc.mjs +123 -3
- package/src/Outline/Outline.test.tsx +735 -0
- package/src/Outline/Outline.tsx +184 -33
- package/src/Outline/useScrollSpy.ts +248 -72
- package/src/Selector/SelectorOption.doc.mjs +1 -1
- package/src/SideNav/SideNavItem.doc.mjs +2 -2
- package/src/Switch/Switch.doc.mjs +1 -1
- package/src/TabList/Tab.tsx +5 -1
- package/src/TabList/TabList.doc.mjs +3 -3
- package/src/TabList/TabList.test.tsx +66 -0
- package/src/TabList/TabList.tsx +8 -0
- package/src/TabList/TabMenu.tsx +4 -1
- package/src/Table/Table.doc.mjs +2 -0
- package/src/Table/index.ts +7 -0
- package/src/Table/plugins/rowExpansion/useTableRowExpansion.test.tsx +90 -1
- package/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx +37 -5
- package/src/Table/plugins/sortable/useTableSortable.test.tsx +95 -9
- package/src/Table/plugins/sortable/useTableSortable.tsx +23 -7
- package/src/Table/plugins/tree/index.ts +12 -0
- package/src/Table/plugins/tree/useTableTreeData-perf.test.tsx +163 -0
- package/src/Table/plugins/tree/useTableTreeData.test.tsx +594 -0
- package/src/Table/plugins/tree/useTableTreeData.tsx +495 -0
- package/src/Table/plugins/tree/useTableTreeState.test.tsx +636 -0
- package/src/Table/plugins/tree/useTableTreeState.tsx +253 -0
- package/src/Table/useBaseTablePlugins.ts +6 -2
- package/src/Table/useTableTreeData.doc.mjs +75 -0
- package/src/Table/useTableTreeState.doc.mjs +98 -0
- package/src/TextArea/TextArea.doc.mjs +1 -1
- package/src/TextInput/TextInput.doc.mjs +1 -1
- package/src/Toolbar/Toolbar.tsx +10 -3
- package/src/postinstall.test.mjs +71 -0
- package/src/theme/Theme.doc.mjs +4 -4
- package/src/theme/Theme.tsx +1 -1
- package/src/theme/defineTheme.ts +1 -1
- package/src/theme/hct.ts +0 -4
package/README.md
CHANGED
|
@@ -2,7 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
Core UI components, theme system, and utilities for the Astryx design system. For project setup, see [Quick Start](#quick-start) below.
|
|
4
4
|
|
|
5
|
-
> **Building with an AI agent?**
|
|
5
|
+
> **Building with an AI agent?** Add the CLI, then run `init`:
|
|
6
|
+
>
|
|
7
|
+
> ```bash
|
|
8
|
+
> npm install -D @astryxdesign/cli # or: pnpm add -D / yarn add -D / bun add -d
|
|
9
|
+
> npx astryx init # resolves to the CLI you just installed
|
|
10
|
+
> ```
|
|
11
|
+
>
|
|
12
|
+
> `init` writes the Astryx component index into your `AGENTS.md`/`CLAUDE.md` so your agent discovers components, templates, and design tokens instead of guessing. Need a single command without installing? Use the scoped package directly — `npx @astryxdesign/cli <cmd>` (or `pnpm dlx`/`bunx @astryxdesign/cli`). Bare `npx astryx` only works once `@astryxdesign/cli` is a dependency; before that npm resolves it to an unrelated package. See [XDS CLI](#xds-cli).
|
|
6
13
|
|
|
7
14
|
## Component Docs
|
|
8
15
|
|
|
@@ -25,9 +32,9 @@ global navigation.
|
|
|
25
32
|
Requires `@astryxdesign/cli` (`npm install -D @astryxdesign/cli`):
|
|
26
33
|
|
|
27
34
|
```bash
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
35
|
+
astryx template --list # browse all page and block templates
|
|
36
|
+
astryx template dashboard # emit full page source
|
|
37
|
+
astryx template settings --skeleton # layout skeleton with spatial annotations
|
|
31
38
|
```
|
|
32
39
|
|
|
33
40
|
## Astryx CLI
|
|
@@ -35,19 +42,21 @@ npx astryx template settings --skeleton # layout skeleton with spatial annotatio
|
|
|
35
42
|
The CLI (`@astryxdesign/cli`) provides additional tooling:
|
|
36
43
|
|
|
37
44
|
```bash
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
astryx --help # full listing of all commands
|
|
46
|
+
astryx component Button # full docs + related block templates
|
|
47
|
+
astryx docs # reference docs (principles, tokens, theming, styling)
|
|
48
|
+
astryx docs theme # theming guide (Theme, defineTheme, light/dark)
|
|
49
|
+
astryx docs tokens # spacing, color, radius, typography token reference
|
|
50
|
+
astryx init # initialize Astryx in your project
|
|
51
|
+
astryx theme build # build theme CSS for production
|
|
52
|
+
astryx swizzle Button # eject component source for customization
|
|
53
|
+
astryx upgrade --apply # run codemods to migrate between versions
|
|
54
|
+
astryx discover # discover external Astryx packages
|
|
55
|
+
astryx gap-report # report a missing capability
|
|
49
56
|
```
|
|
50
57
|
|
|
58
|
+
> Prefix these with your runner: `npx astryx …` / `pnpm exec astryx …` once the CLI is installed, or `npx @astryxdesign/cli …` to run without installing.
|
|
59
|
+
|
|
51
60
|
## Related Packages
|
|
52
61
|
|
|
53
62
|
| Package | Description |
|
|
@@ -45,6 +45,16 @@ export interface CollapsibleProps extends BaseProps {
|
|
|
45
45
|
* Controlled open state. When provided, the component is fully controlled.
|
|
46
46
|
*/
|
|
47
47
|
isOpen?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Whether the collapsible is disabled. A disabled item can't be toggled —
|
|
50
|
+
* its trigger is non-interactive and dimmed. Following the system-wide
|
|
51
|
+
* disabled convention, the trigger uses `aria-disabled` (not the native
|
|
52
|
+
* `disabled` attribute) and drops out of the tab order, staying perceivable
|
|
53
|
+
* to assistive tech. The content stays in whatever open state it was;
|
|
54
|
+
* disabling doesn't collapse an already-open item.
|
|
55
|
+
* @default false
|
|
56
|
+
*/
|
|
57
|
+
isDisabled?: boolean;
|
|
48
58
|
/**
|
|
49
59
|
* Callback when the open state changes.
|
|
50
60
|
*/
|
|
@@ -96,7 +106,7 @@ export interface CollapsibleProps extends BaseProps {
|
|
|
96
106
|
* </CollapsibleGroup>
|
|
97
107
|
* ```
|
|
98
108
|
*/
|
|
99
|
-
export declare function Collapsible({ trigger, children, defaultIsOpen, isOpen: controlledIsOpen, onOpenChange, value, ref, xstyle, className, style, ...props }: CollapsibleProps): import("react").JSX.Element;
|
|
109
|
+
export declare function Collapsible({ trigger, children, defaultIsOpen, isOpen: controlledIsOpen, isDisabled, onOpenChange, value, ref, xstyle, className, style, ...props }: CollapsibleProps): import("react").JSX.Element;
|
|
100
110
|
export declare namespace Collapsible {
|
|
101
111
|
var displayName: string;
|
|
102
112
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Collapsible.d.ts","sourceRoot":"","sources":["../../src/Collapsible/Collapsible.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAa,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"Collapsible.d.ts","sourceRoot":"","sources":["../../src/Collapsible/Collapsible.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAa,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAiBjD,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAkH5C,MAAM,WAAW,gBAAiB,SAAQ,SAAS;IACjD,wCAAwC;IACxC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAChC;;;OAGG;IACH,OAAO,EAAE,SAAS,CAAC;IAEnB;;OAEG;IACH,QAAQ,CAAC,EAAE,SAAS,CAAC;IAErB;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IAExB;;OAEG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IAEjB;;;;;;;;OAQG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;OAEG;IACH,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IAEzC;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,wBAAgB,WAAW,CAAC,EAC1B,OAAO,EACP,QAAQ,EACR,aAAa,EACb,MAAM,EAAE,gBAAgB,EACxB,UAAkB,EAClB,YAAY,EACZ,KAAK,EACL,GAAG,EACH,MAAM,EACN,SAAS,EACT,KAAK,EACL,GAAG,KAAK,EACT,EAAE,gBAAgB,+BA6FlB;yBA1Ge,WAAW"}
|
|
@@ -58,12 +58,22 @@ const styles = {
|
|
|
58
58
|
kInvED: "x1wfwxd8 x7s97pk",
|
|
59
59
|
$$css: true
|
|
60
60
|
},
|
|
61
|
+
triggerDisabled: {
|
|
62
|
+
kkrTdU: "x1h6gzvc",
|
|
63
|
+
kSiTet: "xbyyjgo",
|
|
64
|
+
$$css: true
|
|
65
|
+
},
|
|
61
66
|
contentHidden: {
|
|
62
67
|
k1xSpc: "x1s85apg",
|
|
63
68
|
$$css: true
|
|
64
69
|
},
|
|
65
70
|
content: {
|
|
66
71
|
kLKAdn: "xfsso4q",
|
|
72
|
+
kMv6JI: "x9ynric",
|
|
73
|
+
kGuDYH: "xjm74w1",
|
|
74
|
+
k63SB2: "xxovm9e",
|
|
75
|
+
kLWn49: "xw6l6zx",
|
|
76
|
+
kMwMTN: "x1tgivj0",
|
|
67
77
|
$$css: true
|
|
68
78
|
},
|
|
69
79
|
divided: {
|
|
@@ -155,6 +165,7 @@ export function Collapsible({
|
|
|
155
165
|
children,
|
|
156
166
|
defaultIsOpen,
|
|
157
167
|
isOpen: controlledIsOpen,
|
|
168
|
+
isDisabled = false,
|
|
158
169
|
onOpenChange,
|
|
159
170
|
value,
|
|
160
171
|
ref,
|
|
@@ -178,6 +189,17 @@ export function Collapsible({
|
|
|
178
189
|
isCollapsible: collapsibleConfig,
|
|
179
190
|
value
|
|
180
191
|
});
|
|
192
|
+
|
|
193
|
+
// Activation is blocked by this guard rather than the native `disabled`
|
|
194
|
+
// attribute, so the trigger keeps `aria-disabled` semantics and stays
|
|
195
|
+
// discoverable. A native `disabled` button would silently swallow events
|
|
196
|
+
// (e.g. a wrapping tooltip's hover) — the system-wide disabled convention.
|
|
197
|
+
const handleToggle = () => {
|
|
198
|
+
if (isDisabled) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
toggle();
|
|
202
|
+
};
|
|
181
203
|
const presentation = use(CollapsibleGroupPresentationContext);
|
|
182
204
|
const isDivided = presentation?.hasDividers ?? false;
|
|
183
205
|
const density = presentation?.density ?? null;
|
|
@@ -194,10 +216,18 @@ export function Collapsible({
|
|
|
194
216
|
...props,
|
|
195
217
|
children: [/*#__PURE__*/_jsxs("button", {
|
|
196
218
|
type: "button",
|
|
197
|
-
onClick:
|
|
219
|
+
onClick: handleToggle,
|
|
220
|
+
"aria-disabled": isDisabled || undefined,
|
|
198
221
|
"aria-expanded": isOpen,
|
|
199
|
-
"aria-controls": contentId
|
|
200
|
-
|
|
222
|
+
"aria-controls": contentId
|
|
223
|
+
// A disabled trigger drops out of the tab order so it isn't a silently
|
|
224
|
+
// dead tab stop; activation stays blocked by the handleToggle guard,
|
|
225
|
+
// and aria-disabled keeps the state perceivable to assistive tech —
|
|
226
|
+
// the system-wide disabled convention (never native `disabled`, which
|
|
227
|
+
// would swallow events like a wrapping tooltip's hover).
|
|
228
|
+
,
|
|
229
|
+
tabIndex: isDisabled ? -1 : undefined,
|
|
230
|
+
...stylex.props(styles.trigger, density != null && triggerDensity[density], isDisabled && styles.triggerDisabled),
|
|
201
231
|
children: [/*#__PURE__*/_jsx("span", {
|
|
202
232
|
...{
|
|
203
233
|
className: "x1b2iylo xwgcxoh"
|
|
@@ -216,7 +246,9 @@ export function Collapsible({
|
|
|
216
246
|
})]
|
|
217
247
|
}), /*#__PURE__*/_jsx("div", {
|
|
218
248
|
id: contentId,
|
|
219
|
-
...
|
|
249
|
+
...mergeProps(themeProps('collapsible-content', {
|
|
250
|
+
density: density ?? undefined
|
|
251
|
+
}), stylex.props(styles.content, density != null && contentDensity[density], !isOpen && styles.contentHidden)),
|
|
220
252
|
children: presentation != null ? /*#__PURE__*/_jsx(CollapsibleGroupPresentationContext, {
|
|
221
253
|
value: null,
|
|
222
254
|
children: children
|
|
@@ -19,6 +19,50 @@ export interface OutlineProps extends BaseProps<HTMLElement> {
|
|
|
19
19
|
* @default 'default'
|
|
20
20
|
*/
|
|
21
21
|
density?: 'default' | 'compact';
|
|
22
|
+
/**
|
|
23
|
+
* Called when navigation to an item begins, before the scroll starts.
|
|
24
|
+
* Receives the item `id`. Pair with `onNavigateEnd` to drive an arrival
|
|
25
|
+
* effect (flash, ring, pulse) on the target heading.
|
|
26
|
+
*/
|
|
27
|
+
onNavigateStart?: (id: string) => void;
|
|
28
|
+
/**
|
|
29
|
+
* Called once when navigation to an item resolves — when the smooth scroll
|
|
30
|
+
* settles, or immediately-ish when reduced motion turns it into a jump.
|
|
31
|
+
*
|
|
32
|
+
* Fires exactly once for every `onNavigateStart`, including when the user
|
|
33
|
+
* interrupts the scroll by scrolling manually, so a "navigating" state can
|
|
34
|
+
* never leak. It does not fire if the Outline unmounts mid-scroll.
|
|
35
|
+
*/
|
|
36
|
+
onNavigateEnd?: (id: string) => void;
|
|
37
|
+
/**
|
|
38
|
+
* Height in px of a fixed header overlaying the top of the scroll root.
|
|
39
|
+
*
|
|
40
|
+
* Shifts both the activation line *and* the scroll landing by the same
|
|
41
|
+
* amount, so a heading activates exactly where navigating to it puts it —
|
|
42
|
+
* below the header rather than hidden underneath it.
|
|
43
|
+
*
|
|
44
|
+
* It composes with each heading's own `scroll-margin-top` (the header, then
|
|
45
|
+
* the breathing room below it) rather than replacing it. When nothing
|
|
46
|
+
* overlays the content, leave this at 0 and let `scroll-margin-top` do the
|
|
47
|
+
* work — the browser already honors it.
|
|
48
|
+
*
|
|
49
|
+
* @default 0
|
|
50
|
+
*/
|
|
51
|
+
offset?: number;
|
|
52
|
+
/**
|
|
53
|
+
* Scroll container to track, instead of auto-detecting the nearest scrollable
|
|
54
|
+
* ancestor. Use this when the content scrolls inside a split pane, modal, or
|
|
55
|
+
* dashboard panel rather than the viewport.
|
|
56
|
+
*/
|
|
57
|
+
scrollContainerRef?: React.RefObject<HTMLElement | null>;
|
|
58
|
+
/**
|
|
59
|
+
* Whether activating an item smooth-scrolls to it. Set to false to own the
|
|
60
|
+
* scrolling yourself (virtualized content, a router) — the Outline still
|
|
61
|
+
* updates the active item, the hash, and the navigate callbacks, but performs
|
|
62
|
+
* no scroll and suppresses the anchor's default jump.
|
|
63
|
+
* @default true
|
|
64
|
+
*/
|
|
65
|
+
hasScrollOnClick?: boolean;
|
|
22
66
|
/** Test ID for testing frameworks. */
|
|
23
67
|
'data-testid'?: string;
|
|
24
68
|
}
|
|
@@ -30,8 +74,15 @@ export interface OutlineProps extends BaseProps<HTMLElement> {
|
|
|
30
74
|
* track that animates to the active item.
|
|
31
75
|
*
|
|
32
76
|
* When `activeId` is omitted, it tracks scroll position and marks the last
|
|
33
|
-
* heading whose top has passed its activation line
|
|
34
|
-
*
|
|
77
|
+
* heading whose top has passed its activation line — which is exactly where
|
|
78
|
+
* navigating to that heading lands it: `offset` (a fixed header overlaying the
|
|
79
|
+
* scroll root) plus the heading's own `scroll-margin-top`. It defaults to the
|
|
80
|
+
* first item at the top and the last at the bottom.
|
|
81
|
+
*
|
|
82
|
+
* Keyboard: the list is a single tab stop (roving tabindex), seated on the
|
|
83
|
+
* active heading. Arrow keys move between headings, Home/End jump to the ends,
|
|
84
|
+
* and Enter/Space activate — so a long table of contents costs one Tab press,
|
|
85
|
+
* not one per heading.
|
|
35
86
|
*
|
|
36
87
|
* @example
|
|
37
88
|
* ```
|
|
@@ -43,8 +94,23 @@ export interface OutlineProps extends BaseProps<HTMLElement> {
|
|
|
43
94
|
* ]}
|
|
44
95
|
* />
|
|
45
96
|
* ```
|
|
97
|
+
*
|
|
98
|
+
* Scoped to a custom scroll container, under a fixed header:
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* ```
|
|
102
|
+
* const scrollContainerRef = useRef<HTMLDivElement>(null);
|
|
103
|
+
*
|
|
104
|
+
* <div ref={scrollContainerRef} style={{overflowY: 'auto'}}>...</div>
|
|
105
|
+
* <Outline
|
|
106
|
+
* items={items}
|
|
107
|
+
* scrollContainerRef={scrollContainerRef}
|
|
108
|
+
* offset={64}
|
|
109
|
+
* onNavigateEnd={id => flashHeading(id)}
|
|
110
|
+
* />
|
|
111
|
+
* ```
|
|
46
112
|
*/
|
|
47
|
-
export declare function Outline({ items, activeId, onActiveIdChange, label: labelFromProps, density, xstyle, className, style, ref, 'data-testid': testId, ...props }: OutlineProps): import("react").JSX.Element;
|
|
113
|
+
export declare function Outline({ items, activeId, onActiveIdChange, label: labelFromProps, density, onNavigateStart, onNavigateEnd, offset, scrollContainerRef, hasScrollOnClick, xstyle, className, style, ref, 'data-testid': testId, ...props }: OutlineProps): import("react").JSX.Element;
|
|
48
114
|
export declare namespace Outline {
|
|
49
115
|
var displayName: string;
|
|
50
116
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Outline.d.ts","sourceRoot":"","sources":["../../src/Outline/Outline.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"Outline.d.ts","sourceRoot":"","sources":["../../src/Outline/Outline.tsx"],"names":[],"mappings":"AAwCA,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAE5C,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,SAAS,CAAC;AAIzC,YAAY,EAAC,WAAW,EAAC,MAAM,SAAS,CAAC;AAEzC,MAAM,WAAW,YAAa,SAAQ,SAAS,CAAC,WAAW,CAAC;IAC1D,6CAA6C;IAC7C,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IAE7B,+CAA+C;IAC/C,KAAK,EAAE,WAAW,EAAE,CAAC;IAErB,oFAAoF;IACpF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB,oEAAoE;IACpE,gBAAgB,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAExC,0EAA0E;IAC1E,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;;;;OAKG;IACH,OAAO,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;IAEhC;;;;OAIG;IACH,eAAe,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAEvC;;;;;;;OAOG;IACH,aAAa,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAErC;;;;;;;;;;;;;OAaG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAEzD;;;;;;OAMG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAE3B,sCAAsC;IACtC,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAgJD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,wBAAgB,OAAO,CAAC,EACtB,KAAK,EACL,QAAQ,EACR,gBAAgB,EAChB,KAAK,EAAE,cAAc,EACrB,OAAmB,EACnB,eAAe,EACf,aAAa,EACb,MAAU,EACV,kBAAkB,EAClB,gBAAuB,EACvB,MAAM,EACN,SAAS,EACT,KAAK,EACL,GAAG,EACH,aAAa,EAAE,MAAM,EACrB,GAAG,KAAK,EACT,EAAE,YAAY,+BAiLd;yBAlMe,OAAO"}
|
package/dist/Outline/Outline.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @file Outline.tsx
|
|
7
|
-
* @input Uses React, StyleX, Astryx link provider integration, Outline hooks/types
|
|
7
|
+
* @input Uses React, StyleX, Astryx link provider integration, Outline hooks/types, useListFocus
|
|
8
8
|
* @output Exports Outline component and OutlineProps type
|
|
9
9
|
* @position Core implementation; consumed by index.ts
|
|
10
10
|
*
|
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
* - Sliding indicator track (vertical divider + animated active bar)
|
|
13
13
|
* - Density variant (default/compact)
|
|
14
14
|
* - Scroll-spy active state when uncontrolled
|
|
15
|
+
* - Roving-tabindex keyboard navigation (Arrow/Home/End, Enter/Space activate)
|
|
16
|
+
* - onNavigateStart / onNavigateEnd callbacks around the smooth scroll
|
|
17
|
+
* - Scroll scoping (offset, scrollContainerRef, hasScrollOnClick)
|
|
15
18
|
*
|
|
16
19
|
* SYNC: When modified, update these files to stay in sync:
|
|
17
20
|
* - /packages/core/src/Outline/Outline.doc.mjs
|
|
@@ -24,6 +27,8 @@ import * as stylex from '@stylexjs/stylex';
|
|
|
24
27
|
import "../theme/tokens.stylex.js";
|
|
25
28
|
import { colorVars, durationVars, easeVars, radiusVars, spacingVars, typeScaleVars, fontWeightVars } from "../theme/tokens.stylex.js";
|
|
26
29
|
import { useLinkComponent } from "../Link/useLinkComponent.js";
|
|
30
|
+
import { useListFocus } from "../hooks/useListFocus.js";
|
|
31
|
+
import { useIsomorphicLayoutEffect } from "../hooks/useIsomorphicLayoutEffect.js";
|
|
27
32
|
import { mergeProps, mergeRefs } from "../utils/index.js";
|
|
28
33
|
import { useScrollSpy } from "./useScrollSpy.js";
|
|
29
34
|
import { themeProps } from "../utils/themeProps.js";
|
|
@@ -135,8 +140,15 @@ function getIndentStyle(level) {
|
|
|
135
140
|
* track that animates to the active item.
|
|
136
141
|
*
|
|
137
142
|
* When `activeId` is omitted, it tracks scroll position and marks the last
|
|
138
|
-
* heading whose top has passed its activation line
|
|
139
|
-
*
|
|
143
|
+
* heading whose top has passed its activation line — which is exactly where
|
|
144
|
+
* navigating to that heading lands it: `offset` (a fixed header overlaying the
|
|
145
|
+
* scroll root) plus the heading's own `scroll-margin-top`. It defaults to the
|
|
146
|
+
* first item at the top and the last at the bottom.
|
|
147
|
+
*
|
|
148
|
+
* Keyboard: the list is a single tab stop (roving tabindex), seated on the
|
|
149
|
+
* active heading. Arrow keys move between headings, Home/End jump to the ends,
|
|
150
|
+
* and Enter/Space activate — so a long table of contents costs one Tab press,
|
|
151
|
+
* not one per heading.
|
|
140
152
|
*
|
|
141
153
|
* @example
|
|
142
154
|
* ```
|
|
@@ -148,6 +160,21 @@ function getIndentStyle(level) {
|
|
|
148
160
|
* ]}
|
|
149
161
|
* />
|
|
150
162
|
* ```
|
|
163
|
+
*
|
|
164
|
+
* Scoped to a custom scroll container, under a fixed header:
|
|
165
|
+
*
|
|
166
|
+
* @example
|
|
167
|
+
* ```
|
|
168
|
+
* const scrollContainerRef = useRef<HTMLDivElement>(null);
|
|
169
|
+
*
|
|
170
|
+
* <div ref={scrollContainerRef} style={{overflowY: 'auto'}}>...</div>
|
|
171
|
+
* <Outline
|
|
172
|
+
* items={items}
|
|
173
|
+
* scrollContainerRef={scrollContainerRef}
|
|
174
|
+
* offset={64}
|
|
175
|
+
* onNavigateEnd={id => flashHeading(id)}
|
|
176
|
+
* />
|
|
177
|
+
* ```
|
|
151
178
|
*/
|
|
152
179
|
export function Outline({
|
|
153
180
|
items,
|
|
@@ -155,6 +182,11 @@ export function Outline({
|
|
|
155
182
|
onActiveIdChange,
|
|
156
183
|
label: labelFromProps,
|
|
157
184
|
density = 'default',
|
|
185
|
+
onNavigateStart,
|
|
186
|
+
onNavigateEnd,
|
|
187
|
+
offset = 0,
|
|
188
|
+
scrollContainerRef,
|
|
189
|
+
hasScrollOnClick = true,
|
|
158
190
|
xstyle,
|
|
159
191
|
className,
|
|
160
192
|
style,
|
|
@@ -166,42 +198,109 @@ export function Outline({
|
|
|
166
198
|
const label = labelFromProps ?? t('@astryx.outline.label');
|
|
167
199
|
const rootRef = useRef(null);
|
|
168
200
|
const LinkComponent = useLinkComponent();
|
|
169
|
-
const isControlled = activeId !== undefined;
|
|
170
201
|
const {
|
|
171
202
|
activeId: resolvedActiveId,
|
|
172
|
-
|
|
173
|
-
lockActiveId
|
|
203
|
+
scrollTo
|
|
174
204
|
} = useScrollSpy({
|
|
175
205
|
activeId,
|
|
176
206
|
items,
|
|
177
207
|
onActiveIdChange,
|
|
178
|
-
rootRef
|
|
208
|
+
rootRef,
|
|
209
|
+
offset,
|
|
210
|
+
scrollContainerRef,
|
|
211
|
+
hasScrollOnClick,
|
|
212
|
+
onNavigateStart,
|
|
213
|
+
onNavigateEnd
|
|
179
214
|
});
|
|
215
|
+
|
|
216
|
+
// Roving tabindex over the links: the whole outline is one tab stop, and
|
|
217
|
+
// arrows move between headings. The hook owns `tabindex` on the items.
|
|
218
|
+
const {
|
|
219
|
+
listRef,
|
|
220
|
+
handleKeyDown: handleListKeyDown,
|
|
221
|
+
handleFocus
|
|
222
|
+
} = useListFocus({
|
|
223
|
+
itemSelector: 'a[href]',
|
|
224
|
+
orientation: 'vertical',
|
|
225
|
+
hasRovingTabIndex: true
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// Seat the tab stop on the *active* heading. WAI-ARIA puts the single tab
|
|
229
|
+
// stop on the current item, so tabbing into a table of contents while
|
|
230
|
+
// reading section 7 lands on section 7 — not back at section 1, which is
|
|
231
|
+
// where useListFocus would otherwise leave it (it promotes the first item on
|
|
232
|
+
// mount and then keeps whichever item already holds the stop).
|
|
233
|
+
//
|
|
234
|
+
// Only while focus is outside the list: once the reader has arrowed to an
|
|
235
|
+
// item, the stop is theirs and scroll-spy must not yank it away. Sets
|
|
236
|
+
// `tabindex` only — it never steals focus. Registered after useListFocus so
|
|
237
|
+
// it runs after the hook's own layout effect and wins; on later commits the
|
|
238
|
+
// hook then finds this item already holding the stop and keeps it.
|
|
239
|
+
useIsomorphicLayoutEffect(() => {
|
|
240
|
+
const list = listRef.current;
|
|
241
|
+
if (list == null || list.contains(document.activeElement)) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const links = Array.from(list.querySelectorAll('a[href]'));
|
|
245
|
+
const active = links.find(link => link.getAttribute('aria-current') === 'true');
|
|
246
|
+
if (active == null) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
for (const link of links) {
|
|
250
|
+
const tabIndex = link === active ? '0' : '-1';
|
|
251
|
+
if (link.getAttribute('tabindex') !== tabIndex) {
|
|
252
|
+
link.setAttribute('tabindex', tabIndex);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* The single navigation path, shared by click and keyboard activation:
|
|
259
|
+
* push the hash, then hand off to the scroll-spy's `scrollTo`, which fires
|
|
260
|
+
* the navigate callbacks and moves the indicator when the scroll settles.
|
|
261
|
+
*/
|
|
262
|
+
const navigate = id => {
|
|
263
|
+
if (scrollTo(id)) {
|
|
264
|
+
window.history.pushState(null, '', `#${id}`);
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
/** Whether an event carries a modifier chord we must leave to the browser. */
|
|
269
|
+
const hasModifier = event => event.metaKey || event.altKey || event.ctrlKey || event.shiftKey;
|
|
180
270
|
const handleClick = id => event => {
|
|
181
|
-
|
|
271
|
+
// Let the browser handle modified clicks (open in new tab, etc.) without
|
|
272
|
+
// touching the active state.
|
|
273
|
+
if (event.defaultPrevented || hasModifier(event)) {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
182
276
|
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
|
|
277
|
+
// With no target in the DOM — lazily-rendered or virtualized content —
|
|
278
|
+
// there is nothing to scroll to and nothing to make active. Leave the
|
|
279
|
+
// anchor to the browser's native fragment navigation (which still
|
|
280
|
+
// updates the URL) rather than swallowing the click into a dead link.
|
|
281
|
+
if (document.getElementById(id) == null) {
|
|
186
282
|
return;
|
|
187
283
|
}
|
|
188
|
-
event.preventDefault();
|
|
189
|
-
window.history.pushState(null, '', `#${id}`);
|
|
190
284
|
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
285
|
+
// Suppress the anchor's default jump: we own the scroll (or, with
|
|
286
|
+
// `hasScrollOnClick={false}`, deliberately leave it to the consumer).
|
|
287
|
+
event.preventDefault();
|
|
288
|
+
navigate(id);
|
|
289
|
+
};
|
|
290
|
+
const handleKeyDown = id => event => {
|
|
291
|
+
// Enter is a link's native activation and already produces a click.
|
|
292
|
+
// Space is not, so wire it up here to match the button-like affordance.
|
|
293
|
+
if (event.key !== ' ' && event.key !== 'Spacebar') {
|
|
294
|
+
return;
|
|
200
295
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
296
|
+
if (event.defaultPrevented || hasModifier(event)) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// preventDefault does double duty: it stops the page from scrolling, and
|
|
301
|
+
// it tells useScrollSpy this Space is an activation, not a manual scroll.
|
|
302
|
+
event.preventDefault();
|
|
303
|
+
navigate(id);
|
|
205
304
|
};
|
|
206
305
|
return /*#__PURE__*/_jsxs("nav", {
|
|
207
306
|
...props,
|
|
@@ -215,7 +314,10 @@ export function Outline({
|
|
|
215
314
|
...{
|
|
216
315
|
className: "x78zum5 xdt5ytf x1lsbc85 x1ghz6dp x1717udv xe8uvvx x98rzlu xeuugli"
|
|
217
316
|
},
|
|
317
|
+
ref: listRef,
|
|
218
318
|
role: "list",
|
|
319
|
+
onKeyDown: handleListKeyDown,
|
|
320
|
+
onFocus: handleFocus,
|
|
219
321
|
children: items.map(item => {
|
|
220
322
|
const isActive = item.id === resolvedActiveId;
|
|
221
323
|
return /*#__PURE__*/_jsx("li", {
|
|
@@ -227,6 +329,7 @@ export function Outline({
|
|
|
227
329
|
href: `#${item.id}`,
|
|
228
330
|
"aria-current": isActive ? 'true' : undefined,
|
|
229
331
|
onClick: handleClick(item.id),
|
|
332
|
+
onKeyDown: handleKeyDown(item.id),
|
|
230
333
|
...mergeProps(themeProps('outline-item', {
|
|
231
334
|
active: isActive ? 'active' : null,
|
|
232
335
|
level: item.level
|
|
@@ -4,20 +4,40 @@ interface UseScrollSpyOptions {
|
|
|
4
4
|
items: OutlineItem[];
|
|
5
5
|
onActiveIdChange?: (id: string) => void;
|
|
6
6
|
rootRef: React.RefObject<HTMLElement | null>;
|
|
7
|
+
/**
|
|
8
|
+
* Height in px of a fixed header overlaying the top of the scroll root.
|
|
9
|
+
* Shifts both the activation line and the scroll landing by the same amount,
|
|
10
|
+
* on top of each heading's own `scroll-margin-top`.
|
|
11
|
+
*/
|
|
12
|
+
offset?: number;
|
|
13
|
+
/** Scroll container to track, instead of the nearest scrollable ancestor. */
|
|
14
|
+
scrollContainerRef?: React.RefObject<HTMLElement | null>;
|
|
15
|
+
/** Whether {@link UseScrollSpyResult.scrollTo} performs the smooth scroll. */
|
|
16
|
+
hasScrollOnClick?: boolean;
|
|
17
|
+
/** Called when a navigation begins, before the scroll starts. */
|
|
18
|
+
onNavigateStart?: (id: string) => void;
|
|
19
|
+
/** Called once per navigation, when the scroll settles or is interrupted. */
|
|
20
|
+
onNavigateEnd?: (id: string) => void;
|
|
7
21
|
}
|
|
8
22
|
interface UseScrollSpyResult {
|
|
9
23
|
activeId: string | undefined;
|
|
10
24
|
/** Set the active id (notifies onActiveIdChange). For controlled consumers. */
|
|
11
25
|
setActiveId: (id: string) => void;
|
|
12
26
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* scroll
|
|
27
|
+
* Navigate to the item with id `id` — the single path shared by click and
|
|
28
|
+
* keyboard activation.
|
|
29
|
+
*
|
|
30
|
+
* Fires `onNavigateStart(id)`, scrolls (unless `hasScrollOnClick` is false),
|
|
31
|
+
* and fires `onNavigateEnd(id)` exactly once when the scroll settles or the
|
|
32
|
+
* user interrupts it. While uncontrolled, scroll-spy is suppressed for the
|
|
33
|
+
* duration so the indicator doesn't chase the scroll through intervening
|
|
34
|
+
* sections; it lands on the target once the scroll settles, or resumes
|
|
35
|
+
* position tracking if the user scrolls away mid-flight.
|
|
36
|
+
*
|
|
37
|
+
* Returns false (and does nothing) when no element with `id` exists.
|
|
18
38
|
*/
|
|
19
|
-
|
|
39
|
+
scrollTo: (id: string) => boolean;
|
|
20
40
|
}
|
|
21
|
-
export declare function useScrollSpy({ activeId, items, onActiveIdChange, rootRef, }: UseScrollSpyOptions): UseScrollSpyResult;
|
|
41
|
+
export declare function useScrollSpy({ activeId, items, onActiveIdChange, rootRef, offset, scrollContainerRef, hasScrollOnClick, onNavigateStart, onNavigateEnd, }: UseScrollSpyOptions): UseScrollSpyResult;
|
|
22
42
|
export {};
|
|
23
43
|
//# sourceMappingURL=useScrollSpy.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useScrollSpy.d.ts","sourceRoot":"","sources":["../../src/Outline/useScrollSpy.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"useScrollSpy.d.ts","sourceRoot":"","sources":["../../src/Outline/useScrollSpy.ts"],"names":[],"mappings":"AAqCA,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,SAAS,CAAC;AAsJzC,UAAU,mBAAmB;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,WAAW,EAAE,CAAC;IACrB,gBAAgB,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,OAAO,EAAE,KAAK,CAAC,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAC7C;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,6EAA6E;IAC7E,kBAAkB,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IACzD,8EAA8E;IAC9E,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,iEAAiE;IACjE,eAAe,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,6EAA6E;IAC7E,aAAa,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;CACtC;AAED,UAAU,kBAAkB;IAC1B,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,+EAA+E;IAC/E,WAAW,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC;;;;;;;;;;;;OAYG;IACH,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC;CACnC;AAED,wBAAgB,YAAY,CAAC,EAC3B,QAAQ,EACR,KAAK,EACL,gBAAgB,EAChB,OAAO,EACP,MAAU,EACV,kBAAkB,EAClB,gBAAuB,EACvB,eAAe,EACf,aAAa,GACd,EAAE,mBAAmB,GAAG,kBAAkB,CAoN1C"}
|