@astryxdesign/core 0.1.7 → 0.1.8-canary.c5fa19b
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 +65 -0
- package/README.md +30 -19
- package/dist/Avatar/Avatar.d.ts +10 -5
- package/dist/Avatar/Avatar.d.ts.map +1 -1
- package/dist/Avatar/Avatar.js +12 -8
- package/dist/Avatar/AvatarSizeContext.d.ts +1 -1
- package/dist/Avatar/AvatarSizeContext.js +1 -1
- package/dist/Avatar/AvatarStatusDot.d.ts +2 -2
- package/dist/Avatar/AvatarStatusDot.js +2 -2
- package/dist/AvatarGroup/AvatarGroup.d.ts +2 -2
- package/dist/AvatarGroup/AvatarGroup.d.ts.map +1 -1
- package/dist/AvatarGroup/AvatarGroup.js +2 -2
- package/dist/AvatarGroup/AvatarGroupOverflow.d.ts +1 -1
- package/dist/AvatarGroup/AvatarGroupOverflow.d.ts.map +1 -1
- package/dist/AvatarGroup/AvatarGroupOverflow.js +16 -14
- package/dist/Carousel/Carousel.d.ts +3 -2
- package/dist/Carousel/Carousel.d.ts.map +1 -1
- package/dist/Carousel/Carousel.js +32 -2
- package/dist/Chat/ChatMessage.d.ts +1 -1
- package/dist/Chat/ChatMessage.d.ts.map +1 -1
- package/dist/Chat/ChatMessage.js +1 -1
- package/dist/Citation/Citation.d.ts.map +1 -1
- package/dist/Citation/Citation.js +10 -4
- 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/ContextMenu/ContextMenu.d.ts.map +1 -1
- package/dist/ContextMenu/ContextMenu.js +4 -3
- package/dist/ContextMenu/index.d.ts +3 -0
- package/dist/ContextMenu/index.d.ts.map +1 -1
- package/dist/ContextMenu/index.js +7 -1
- package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenu.js +8 -4
- package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts +89 -0
- package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts.map +1 -0
- package/dist/DropdownMenu/DropdownMenuCheckboxItem.js +170 -0
- package/dist/DropdownMenu/DropdownMenuContext.d.ts +17 -1
- package/dist/DropdownMenu/DropdownMenuContext.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuContext.js +18 -0
- package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts +69 -0
- package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts.map +1 -0
- package/dist/DropdownMenu/DropdownMenuRadioGroup.js +70 -0
- package/dist/DropdownMenu/DropdownMenuRadioItem.d.ts +69 -0
- package/dist/DropdownMenu/DropdownMenuRadioItem.d.ts.map +1 -0
- package/dist/DropdownMenu/DropdownMenuRadioItem.js +178 -0
- package/dist/DropdownMenu/index.d.ts +5 -1
- package/dist/DropdownMenu/index.d.ts.map +1 -1
- package/dist/DropdownMenu/index.js +10 -1
- package/dist/DropdownMenu/menuItemRoles.d.ts +13 -0
- package/dist/DropdownMenu/menuItemRoles.d.ts.map +1 -0
- package/dist/DropdownMenu/menuItemRoles.js +15 -0
- package/dist/Icon/Icon.d.ts +43 -13
- package/dist/Icon/Icon.d.ts.map +1 -1
- package/dist/Icon/Icon.js +62 -27
- 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/Table.d.ts +1 -1
- package/dist/Table/Table.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/TreeList/TreeListItem.d.ts.map +1 -1
- package/dist/TreeList/TreeListItem.js +5 -14
- package/dist/astryx.css +24 -4
- package/dist/astryx.umd.js +54 -54
- 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/docs.mjs +32 -461
- package/locales/en.json +200 -172
- package/locales/pseudo.json +21 -0
- package/package.json +3 -1
- package/scripts/postinstall.mjs +93 -0
- package/src/Avatar/Avatar.doc.mjs +6 -6
- package/src/Avatar/Avatar.tsx +16 -11
- package/src/Avatar/AvatarSizeContext.ts +1 -1
- package/src/Avatar/AvatarStatusDot.tsx +2 -2
- package/src/AvatarGroup/AvatarGroup.doc.mjs +1 -1
- package/src/AvatarGroup/AvatarGroup.test.tsx +17 -3
- package/src/AvatarGroup/AvatarGroup.tsx +3 -3
- package/src/AvatarGroup/AvatarGroupOverflow.doc.mjs +4 -4
- package/src/AvatarGroup/AvatarGroupOverflow.tsx +21 -5
- package/src/Carousel/Carousel.doc.mjs +1 -0
- package/src/Carousel/Carousel.test.tsx +96 -1
- package/src/Carousel/Carousel.tsx +29 -2
- package/src/Chat/Chat.doc.mjs +1 -1
- package/src/Chat/ChatMessage.tsx +4 -2
- package/src/CheckboxInput/CheckboxInput.doc.mjs +1 -1
- package/src/Citation/Citation.test.tsx +51 -0
- package/src/Citation/Citation.tsx +8 -2
- 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/ContextMenu/ContextMenu.test.tsx +75 -1
- package/src/ContextMenu/ContextMenu.tsx +10 -5
- package/src/ContextMenu/index.ts +15 -0
- package/src/DropdownMenu/DropdownMenu.doc.mjs +23 -0
- package/src/DropdownMenu/DropdownMenu.tsx +11 -6
- package/src/DropdownMenu/DropdownMenuCheckboxItem.doc.mjs +95 -0
- package/src/DropdownMenu/DropdownMenuCheckboxItem.tsx +237 -0
- package/src/DropdownMenu/DropdownMenuContext.tsx +29 -1
- package/src/DropdownMenu/DropdownMenuItem.doc.mjs +7 -1
- package/src/DropdownMenu/DropdownMenuRadioGroup.doc.mjs +80 -0
- package/src/DropdownMenu/DropdownMenuRadioGroup.tsx +119 -0
- package/src/DropdownMenu/DropdownMenuRadioItem.doc.mjs +80 -0
- package/src/DropdownMenu/DropdownMenuRadioItem.tsx +232 -0
- package/src/DropdownMenu/DropdownMenuSelectable.test.tsx +163 -0
- package/src/DropdownMenu/index.ts +23 -3
- package/src/DropdownMenu/menuItemRoles.ts +22 -0
- package/src/Field/Field.doc.mjs +1 -1
- package/src/Field/FieldLabel.doc.mjs +1 -1
- package/src/Icon/Icon.doc.mjs +12 -0
- package/src/Icon/Icon.test.tsx +142 -0
- package/src/Icon/Icon.tsx +98 -30
- package/src/Item/Item.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/Table.tsx +1 -1
- 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/TreeList/TreeList.test.tsx +32 -1
- package/src/TreeList/TreeListItem.tsx +19 -10
- package/src/docs-script.test.mjs +18 -13
- 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/dist/TreeList/treeListItem.markers.stylex.d.ts +0 -16
- package/dist/TreeList/treeListItem.markers.stylex.d.ts.map +0 -1
- package/dist/TreeList/treeListItem.markers.stylex.js +0 -22
- package/src/TreeList/treeListItem.markers.stylex.ts +0 -20
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @file useTableTreeState.tsx
|
|
7
|
+
* @input React
|
|
8
|
+
* @output Exports useTableTreeState hook and config/result types
|
|
9
|
+
* @position Tree state helper; owns the expanded set + flattens nested data.
|
|
10
|
+
* Pairs with useTableTreeData (the headless tree plugin).
|
|
11
|
+
*
|
|
12
|
+
* Modeled after useTableSortableState — a convenience layer that owns state
|
|
13
|
+
* (controlled or uncontrolled) and produces both the visible flattened rows
|
|
14
|
+
* and a ready-to-use config for the headless plugin. Data shaping stays
|
|
15
|
+
* outside the render pipeline: Table always receives exactly the rows it
|
|
16
|
+
* renders, so collapsed subtrees are unmounted, not hidden.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import {useCallback, useMemo, useRef, useState} from 'react';
|
|
20
|
+
import type {
|
|
21
|
+
TableTreeRowMeta,
|
|
22
|
+
UseTableTreeDataConfig,
|
|
23
|
+
} from './useTableTreeData';
|
|
24
|
+
|
|
25
|
+
// =============================================================================
|
|
26
|
+
// Config
|
|
27
|
+
// =============================================================================
|
|
28
|
+
|
|
29
|
+
export interface UseTableTreeStateConfig<T extends Record<string, unknown>> {
|
|
30
|
+
/** Nested data: rows may carry child rows under `childrenKey`. */
|
|
31
|
+
data: T[];
|
|
32
|
+
/** Row ID accessor: property name, or a function returning a unique id. */
|
|
33
|
+
idKey: (keyof T & string) | ((item: T) => string | number);
|
|
34
|
+
/**
|
|
35
|
+
* Property holding each row's children array.
|
|
36
|
+
* @default 'children'
|
|
37
|
+
*/
|
|
38
|
+
childrenKey?: string;
|
|
39
|
+
/** Initial expanded row ids for uncontrolled mode. Ignored when `expandedIds` is provided. */
|
|
40
|
+
defaultExpandedIds?: Iterable<string>;
|
|
41
|
+
/**
|
|
42
|
+
* Controlled set of expanded row ids. When provided, the hook uses this
|
|
43
|
+
* instead of internal state. Pair with `onExpandedIdsChange`.
|
|
44
|
+
*/
|
|
45
|
+
expandedIds?: ReadonlySet<string>;
|
|
46
|
+
/** Called with the next expanded set whenever expansion changes. */
|
|
47
|
+
onExpandedIdsChange?: (ids: ReadonlySet<string>) => void;
|
|
48
|
+
/**
|
|
49
|
+
* Should this row show an expander? Overrides the default
|
|
50
|
+
* "has a non-empty children array" check — use for lazy loading, where a
|
|
51
|
+
* row is expandable before its children have been fetched.
|
|
52
|
+
*/
|
|
53
|
+
isItemExpandable?: (item: T) => boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Sort each sibling group independently during flattening. Children always
|
|
56
|
+
* stay directly under their parent — sorting never crosses levels. Pass
|
|
57
|
+
* `applySort` from `useTableSortableState` to compose with column sorting.
|
|
58
|
+
*/
|
|
59
|
+
sortSiblings?: (siblings: T[]) => T[];
|
|
60
|
+
/** Indent step per level, forwarded to useTableTreeData. */
|
|
61
|
+
indent?: 'sm' | 'md' | 'lg';
|
|
62
|
+
/** Column that carries the tree affordance, forwarded to useTableTreeData. */
|
|
63
|
+
treeColumnKey?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// =============================================================================
|
|
67
|
+
// Result
|
|
68
|
+
// =============================================================================
|
|
69
|
+
|
|
70
|
+
export interface UseTableTreeStateResult<T extends Record<string, unknown>> {
|
|
71
|
+
/** The flattened, currently-visible rows. Pass to `<Table data>`. */
|
|
72
|
+
visibleData: T[];
|
|
73
|
+
/** Ready-to-use config for useTableTreeData. */
|
|
74
|
+
treeConfig: UseTableTreeDataConfig<T>;
|
|
75
|
+
/** The current expanded row ids. */
|
|
76
|
+
expandedIds: ReadonlySet<string>;
|
|
77
|
+
/** Expand every expandable row in the tree. */
|
|
78
|
+
expandAll: () => void;
|
|
79
|
+
/** Collapse every row. */
|
|
80
|
+
collapseAll: () => void;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// =============================================================================
|
|
84
|
+
// Hook
|
|
85
|
+
// =============================================================================
|
|
86
|
+
|
|
87
|
+
export function useTableTreeState<T extends Record<string, unknown>>(
|
|
88
|
+
config: UseTableTreeStateConfig<T>,
|
|
89
|
+
): UseTableTreeStateResult<T> {
|
|
90
|
+
const {
|
|
91
|
+
data,
|
|
92
|
+
idKey,
|
|
93
|
+
childrenKey = 'children',
|
|
94
|
+
defaultExpandedIds,
|
|
95
|
+
expandedIds: controlledExpandedIds,
|
|
96
|
+
onExpandedIdsChange,
|
|
97
|
+
isItemExpandable,
|
|
98
|
+
sortSiblings,
|
|
99
|
+
indent,
|
|
100
|
+
treeColumnKey,
|
|
101
|
+
} = config;
|
|
102
|
+
|
|
103
|
+
const getId = useCallback(
|
|
104
|
+
(item: T): string =>
|
|
105
|
+
typeof idKey === 'function' ? String(idKey(item)) : String(item[idKey]),
|
|
106
|
+
[idKey],
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const getChildren = useCallback(
|
|
110
|
+
(item: T): T[] => {
|
|
111
|
+
const children = item[childrenKey];
|
|
112
|
+
return Array.isArray(children) ? (children as T[]) : [];
|
|
113
|
+
},
|
|
114
|
+
[childrenKey],
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const getIsExpandable = useCallback(
|
|
118
|
+
(item: T): boolean =>
|
|
119
|
+
isItemExpandable ? isItemExpandable(item) : getChildren(item).length > 0,
|
|
120
|
+
[isItemExpandable, getChildren],
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
// Internal state (used in uncontrolled mode)
|
|
124
|
+
const [internalExpandedIds, setInternalExpandedIds] = useState<
|
|
125
|
+
ReadonlySet<string>
|
|
126
|
+
>(() => new Set(defaultExpandedIds));
|
|
127
|
+
|
|
128
|
+
// Resolve controlled vs uncontrolled
|
|
129
|
+
const isControlled = controlledExpandedIds !== undefined;
|
|
130
|
+
const expandedIds = isControlled
|
|
131
|
+
? controlledExpandedIds
|
|
132
|
+
: internalExpandedIds;
|
|
133
|
+
|
|
134
|
+
// Stable refs so callbacks don't churn on inline-prop identity changes
|
|
135
|
+
const expandedIdsRef = useRef(expandedIds);
|
|
136
|
+
expandedIdsRef.current = expandedIds;
|
|
137
|
+
const onExpandedIdsChangeRef = useRef(onExpandedIdsChange);
|
|
138
|
+
onExpandedIdsChangeRef.current = onExpandedIdsChange;
|
|
139
|
+
|
|
140
|
+
const commitExpandedIds = useCallback(
|
|
141
|
+
(next: ReadonlySet<string>) => {
|
|
142
|
+
// Advance the ref immediately so a second commit in the same React
|
|
143
|
+
// batch (e.g. two onToggleItem calls in one event handler) builds
|
|
144
|
+
// on this one instead of the pre-batch set. The ref re-syncs from
|
|
145
|
+
// the resolved state on the next render either way.
|
|
146
|
+
expandedIdsRef.current = next;
|
|
147
|
+
if (!isControlled) {
|
|
148
|
+
setInternalExpandedIds(next);
|
|
149
|
+
}
|
|
150
|
+
onExpandedIdsChangeRef.current?.(next);
|
|
151
|
+
},
|
|
152
|
+
[isControlled],
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
// Flatten: depth-first walk emitting only visible rows, collecting per-row
|
|
156
|
+
// meta ({level, hasChildren, isExpanded}) in the same pass. Children mount
|
|
157
|
+
// only when their parent is expanded, so collapsed subtrees stay unmounted.
|
|
158
|
+
// `path` holds the ids on the current ancestor chain: an edge pointing back
|
|
159
|
+
// at an ancestor is skipped instead of recursing forever. Sibling arrays
|
|
160
|
+
// are copied before sortSiblings so an in-place sorter can't reorder the
|
|
161
|
+
// consumer's data.
|
|
162
|
+
const {visibleData, metaMap} = useMemo(() => {
|
|
163
|
+
const rows: T[] = [];
|
|
164
|
+
const meta = new Map<string, TableTreeRowMeta>();
|
|
165
|
+
const path = new Set<string>();
|
|
166
|
+
const walk = (items: T[], level: number) => {
|
|
167
|
+
const siblings = sortSiblings ? sortSiblings([...items]) : items;
|
|
168
|
+
for (const item of siblings) {
|
|
169
|
+
const id = getId(item);
|
|
170
|
+
if (path.has(id)) {
|
|
171
|
+
continue; // cyclic edge — this row is its own ancestor
|
|
172
|
+
}
|
|
173
|
+
const hasChildren = getIsExpandable(item);
|
|
174
|
+
const isExpanded = hasChildren && expandedIds.has(id);
|
|
175
|
+
rows.push(item);
|
|
176
|
+
meta.set(id, {id, level, hasChildren, isExpanded});
|
|
177
|
+
if (isExpanded) {
|
|
178
|
+
path.add(id);
|
|
179
|
+
walk(getChildren(item), level + 1);
|
|
180
|
+
path.delete(id);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
walk(data, 0);
|
|
185
|
+
return {visibleData: rows, metaMap: meta};
|
|
186
|
+
}, [data, expandedIds, getId, getChildren, getIsExpandable, sortSiblings]);
|
|
187
|
+
|
|
188
|
+
// Every expandable row id across the whole tree, including collapsed
|
|
189
|
+
// subtrees (drives expandAll and the migration no-op flag). Same
|
|
190
|
+
// ancestor-chain guard as the flatten walk.
|
|
191
|
+
const allExpandableIds = useMemo(() => {
|
|
192
|
+
const idsAcc: string[] = [];
|
|
193
|
+
const path = new Set<string>();
|
|
194
|
+
const walk = (items: T[]) => {
|
|
195
|
+
for (const item of items) {
|
|
196
|
+
const id = getId(item);
|
|
197
|
+
if (path.has(id)) {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (getIsExpandable(item)) {
|
|
201
|
+
idsAcc.push(id);
|
|
202
|
+
}
|
|
203
|
+
path.add(id);
|
|
204
|
+
walk(getChildren(item));
|
|
205
|
+
path.delete(id);
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
walk(data);
|
|
209
|
+
return idsAcc;
|
|
210
|
+
}, [data, getId, getChildren, getIsExpandable]);
|
|
211
|
+
|
|
212
|
+
const hasExpandableRows = allExpandableIds.length > 0;
|
|
213
|
+
|
|
214
|
+
const onToggleItem = useCallback(
|
|
215
|
+
(item: T) => {
|
|
216
|
+
const id = getId(item);
|
|
217
|
+
const next = new Set(expandedIdsRef.current);
|
|
218
|
+
if (next.has(id)) {
|
|
219
|
+
next.delete(id);
|
|
220
|
+
} else {
|
|
221
|
+
next.add(id);
|
|
222
|
+
}
|
|
223
|
+
commitExpandedIds(next);
|
|
224
|
+
},
|
|
225
|
+
[getId, commitExpandedIds],
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
const expandAll = useCallback(() => {
|
|
229
|
+
commitExpandedIds(new Set(allExpandableIds));
|
|
230
|
+
}, [commitExpandedIds, allExpandableIds]);
|
|
231
|
+
|
|
232
|
+
const collapseAll = useCallback(() => {
|
|
233
|
+
commitExpandedIds(new Set());
|
|
234
|
+
}, [commitExpandedIds]);
|
|
235
|
+
|
|
236
|
+
const getRowMeta = useCallback(
|
|
237
|
+
(item: T): TableTreeRowMeta | undefined => metaMap.get(getId(item)),
|
|
238
|
+
[metaMap, getId],
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
const treeConfig = useMemo(
|
|
242
|
+
(): UseTableTreeDataConfig<T> => ({
|
|
243
|
+
getRowMeta,
|
|
244
|
+
onToggleItem,
|
|
245
|
+
hasExpandableRows,
|
|
246
|
+
indent,
|
|
247
|
+
treeColumnKey,
|
|
248
|
+
}),
|
|
249
|
+
[getRowMeta, onToggleItem, hasExpandableRows, indent, treeColumnKey],
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
return {visibleData, treeConfig, expandedIds, expandAll, collapseAll};
|
|
253
|
+
}
|
|
@@ -18,12 +18,15 @@
|
|
|
18
18
|
* Canonical order:
|
|
19
19
|
* 1. columnSettings — column filtering (future: transformColumns)
|
|
20
20
|
* 2. sort — header cell sort controls
|
|
21
|
-
* 3.
|
|
22
|
-
* 4.
|
|
21
|
+
* 3. tree — indent + expander on the tree column
|
|
22
|
+
* 4. selection — checkbox column + row selection
|
|
23
|
+
* 5. pagination — pagination controls around the table
|
|
23
24
|
*
|
|
24
25
|
* Rationale:
|
|
25
26
|
* - columnSettings filters columns before sort/selection see them
|
|
26
27
|
* - sort adds header cell UI before selection adds its header column
|
|
28
|
+
* - tree wraps the first *user* column before selection prepends its
|
|
29
|
+
* checkbox column, so the expander never lands in the checkbox column
|
|
27
30
|
* - selection adds its column after sort so the checkbox header
|
|
28
31
|
* doesn't get a sort button
|
|
29
32
|
* - pagination wraps the table in context last (outermost provider)
|
|
@@ -46,6 +49,7 @@ import type {TablePlugin} from './types';
|
|
|
46
49
|
const PLUGIN_ORDER: ReadonlyArray<string> = [
|
|
47
50
|
'columnSettings',
|
|
48
51
|
'sort',
|
|
52
|
+
'tree',
|
|
49
53
|
'selection',
|
|
50
54
|
'pagination',
|
|
51
55
|
];
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/** @type {import('../docs-types').ComponentDoc} */
|
|
4
|
+
|
|
5
|
+
export const docs = {
|
|
6
|
+
name: 'useTableTreeData',
|
|
7
|
+
subComponentOf: 'Table',
|
|
8
|
+
displayName: 'useTableTreeData',
|
|
9
|
+
description:
|
|
10
|
+
'Headless tree plugin for Table: renders nested rows with per-level indentation and expand/collapse chevrons in the tree column (the first column by default), and reflects hierarchy on body rows via aria-level and aria-expanded. Composable with the other Table plugins: the canonical plugin order places tree before selection, so the checkbox column lands left of the indented tree column. Feed it the treeConfig from useTableTreeState, or construct the config directly for server-driven or pre-flattened trees. When no row is expandable (flat data), every transform is a pass-through and the table renders identically to one without the plugin. Known limitation: the tree column wraps its cell content, so textOverflow="truncate" tooltips do not apply within the tree column.',
|
|
11
|
+
props: [
|
|
12
|
+
{
|
|
13
|
+
name: 'getRowMeta',
|
|
14
|
+
type: '(item: T) => TableTreeRowMeta | undefined',
|
|
15
|
+
description:
|
|
16
|
+
'Structural meta for a visible row: {id, level (0-based), hasChildren, isExpanded}.',
|
|
17
|
+
required: true,
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: 'onToggleItem',
|
|
21
|
+
type: '(item: T) => void',
|
|
22
|
+
description: 'Toggle a row\'s expansion.',
|
|
23
|
+
required: true,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: 'hasExpandableRows',
|
|
27
|
+
type: 'boolean',
|
|
28
|
+
description:
|
|
29
|
+
'Whether any row in the dataset is expandable. When false the plugin is a no-op: no expanders, no indent, no tree ARIA.',
|
|
30
|
+
required: true,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: 'indent',
|
|
34
|
+
type: "'sm' | 'md' | 'lg'",
|
|
35
|
+
description:
|
|
36
|
+
'Indent step per level, mapped to the spacing-3 / spacing-4 / spacing-6 tokens.',
|
|
37
|
+
default: "'md'",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: 'treeColumnKey',
|
|
41
|
+
type: 'string',
|
|
42
|
+
description:
|
|
43
|
+
'Column that carries the indent + expander. Defaults to the first column.',
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
examples: [
|
|
47
|
+
{
|
|
48
|
+
label: 'File tree with expandable folders',
|
|
49
|
+
code: `const {visibleData, treeConfig} = useTableTreeState({
|
|
50
|
+
data: files, // rows may nest under 'children'
|
|
51
|
+
idKey: 'id',
|
|
52
|
+
defaultExpandedIds: ['src'],
|
|
53
|
+
});
|
|
54
|
+
const tree = useTableTreeData(treeConfig);
|
|
55
|
+
|
|
56
|
+
<Table data={visibleData} columns={columns} idKey="id" plugins={{tree}} />;`,
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const docsDense = {
|
|
62
|
+
name: 'useTableTreeData',
|
|
63
|
+
displayName: 'useTableTreeData',
|
|
64
|
+
description:
|
|
65
|
+
'Headless tree plugin: indent + expander chevron on the tree column (first column by default), aria-level/aria-expanded on body rows. Canonical plugin order puts tree before selection (checkbox column lands left of tree column). Consume treeConfig from useTableTreeState, or construct directly for server-driven trees. hasExpandableRows=false => full no-op (flat-data migration).',
|
|
66
|
+
propDescriptions: {
|
|
67
|
+
getRowMeta:
|
|
68
|
+
'structural meta per visible row: {id, level (0-based), hasChildren, isExpanded}',
|
|
69
|
+
onToggleItem: 'toggle row expansion',
|
|
70
|
+
hasExpandableRows: 'false => plugin is a no-op (no expanders/indent/ARIA)',
|
|
71
|
+
indent:
|
|
72
|
+
"indent step per level: 'sm' | 'md' | 'lg' (spacing-3/4/6). Defaults to 'md'.",
|
|
73
|
+
treeColumnKey: 'column carrying indent + expander. Defaults to first column.',
|
|
74
|
+
},
|
|
75
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/** @type {import('../docs-types').ComponentDoc} */
|
|
4
|
+
|
|
5
|
+
export const docs = {
|
|
6
|
+
name: 'useTableTreeState',
|
|
7
|
+
subComponentOf: 'Table',
|
|
8
|
+
displayName: 'useTableTreeState',
|
|
9
|
+
description:
|
|
10
|
+
'State management companion for useTableTreeData. Owns the expanded set (controlled or uncontrolled) and flattens nested data into the visible row array; collapsed subtrees are unmounted, not hidden, so the table body contains exactly the visible rows. Returns expandAll/collapseAll helpers and a ready-to-use config for the tree plugin. Note: because collapsed rows unmount, cell-local React state inside collapsed subtrees is lost on collapse; lift state that must survive.',
|
|
11
|
+
props: [
|
|
12
|
+
{
|
|
13
|
+
name: 'data',
|
|
14
|
+
type: 'T[]',
|
|
15
|
+
description:
|
|
16
|
+
'Nested data: rows may carry child rows under childrenKey. Flat data (no children anywhere) makes the plugin a no-op, so it can be adopted before the data becomes hierarchical.',
|
|
17
|
+
required: true,
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: 'idKey',
|
|
21
|
+
type: '(keyof T & string) | ((item: T) => string | number)',
|
|
22
|
+
description:
|
|
23
|
+
'Row ID accessor: property name or function returning a unique id.',
|
|
24
|
+
required: true,
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: 'childrenKey',
|
|
28
|
+
type: 'string',
|
|
29
|
+
description: 'Property holding each row\'s children array.',
|
|
30
|
+
default: "'children'",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: 'defaultExpandedIds',
|
|
34
|
+
type: 'Iterable<string>',
|
|
35
|
+
description:
|
|
36
|
+
'Initial expanded row ids for uncontrolled mode. Ignored when expandedIds is provided.',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: 'expandedIds',
|
|
40
|
+
type: 'ReadonlySet<string>',
|
|
41
|
+
description:
|
|
42
|
+
'Controlled set of expanded row ids. Pair with onExpandedIdsChange.',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'onExpandedIdsChange',
|
|
46
|
+
type: '(ids: ReadonlySet<string>) => void',
|
|
47
|
+
description:
|
|
48
|
+
'Called with the next expanded set whenever expansion changes (both modes). In lazy-loading setups, trigger the children fetch here.',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: 'isItemExpandable',
|
|
52
|
+
type: '(item: T) => boolean',
|
|
53
|
+
description:
|
|
54
|
+
'Should this row show an expander? Overrides the default non-empty-children check; use for lazy loading, where a row is expandable before its children have been fetched.',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: 'sortSiblings',
|
|
58
|
+
type: '(siblings: T[]) => T[]',
|
|
59
|
+
description:
|
|
60
|
+
'Sort each sibling group independently during flattening; children always stay directly under their parent. Pass applySort from useTableSortableState to compose with column sorting.',
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: 'indent',
|
|
64
|
+
type: "'sm' | 'md' | 'lg'",
|
|
65
|
+
description:
|
|
66
|
+
'Indent step per level (spacing-3 / spacing-4 / spacing-6), forwarded to useTableTreeData.',
|
|
67
|
+
default: "'md'",
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: 'treeColumnKey',
|
|
71
|
+
type: 'string',
|
|
72
|
+
description:
|
|
73
|
+
'Column that carries the indent + expander, forwarded to useTableTreeData. Defaults to the first column.',
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export const docsDense = {
|
|
79
|
+
name: 'useTableTreeState',
|
|
80
|
+
displayName: 'useTableTreeState',
|
|
81
|
+
description:
|
|
82
|
+
'State companion for useTableTreeData. Owns expanded set (controlled or uncontrolled), flattens nested data to visible rows (collapsed subtrees unmounted, not hidden). Returns {visibleData, treeConfig, expandedIds, expandAll, collapseAll}. Cell-local state in collapsed subtrees is lost on collapse; lift state that must survive.',
|
|
83
|
+
propDescriptions: {
|
|
84
|
+
data: 'nested data; rows carry child rows under childrenKey. Flat data => plugin is a no-op.',
|
|
85
|
+
idKey: 'row ID accessor: property name or fn returning unique id',
|
|
86
|
+
childrenKey: "property holding children array. Defaults to 'children'.",
|
|
87
|
+
defaultExpandedIds: 'initial expanded ids (uncontrolled)',
|
|
88
|
+
expandedIds: 'controlled expanded set; pair with onExpandedIdsChange',
|
|
89
|
+
onExpandedIdsChange:
|
|
90
|
+
'called with next expanded set on change (both modes); trigger lazy children fetch here',
|
|
91
|
+
isItemExpandable:
|
|
92
|
+
'overrides non-empty-children expandability check (lazy loading)',
|
|
93
|
+
sortSiblings:
|
|
94
|
+
'sorts each sibling group during flattening; pass applySort from useTableSortableState',
|
|
95
|
+
indent: "indent step per level: 'sm' | 'md' | 'lg'. Defaults to 'md'.",
|
|
96
|
+
treeColumnKey: 'column carrying indent + expander. Defaults to first column.',
|
|
97
|
+
},
|
|
98
|
+
};
|
|
@@ -116,7 +116,7 @@ export const docs = {
|
|
|
116
116
|
name: 'startIcon',
|
|
117
117
|
type: 'IconType',
|
|
118
118
|
description:
|
|
119
|
-
'Icon component rendered inside the leading edge of the textarea wrapper. See `
|
|
119
|
+
'Icon component rendered inside the leading edge of the textarea wrapper. See `astryx docs icons` for valid semantic names.',
|
|
120
120
|
},
|
|
121
121
|
{
|
|
122
122
|
name: 'hasSpellCheck',
|
|
@@ -106,7 +106,7 @@ export const docs = {
|
|
|
106
106
|
name: 'startIcon',
|
|
107
107
|
type: 'IconType',
|
|
108
108
|
description:
|
|
109
|
-
'SVG icon component displayed at the start of the input. See `
|
|
109
|
+
'SVG icon component displayed at the start of the input. See `astryx docs icons` for valid semantic names.',
|
|
110
110
|
},
|
|
111
111
|
{
|
|
112
112
|
name: 'status',
|
package/src/Toolbar/Toolbar.tsx
CHANGED
|
@@ -239,6 +239,7 @@ export function Toolbar({
|
|
|
239
239
|
const hasCenterContent = centerContent != null;
|
|
240
240
|
const hasStartContent = startContent != null;
|
|
241
241
|
const hasEndContent = endContent != null;
|
|
242
|
+
const hasBottomDivider = dividers?.includes('bottom') ?? false;
|
|
242
243
|
|
|
243
244
|
const gapVar = spacingVars[spacingStepToVar[gap]] as string;
|
|
244
245
|
|
|
@@ -316,9 +317,15 @@ export function Toolbar({
|
|
|
316
317
|
orientation === 'vertical' && styles.vertical,
|
|
317
318
|
sizeStyles.base,
|
|
318
319
|
dynamicStyles.gap(gapVar),
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
320
|
+
// Only drop the tab indicator through the toolbar's block padding
|
|
321
|
+
// onto the rail when a bottom divider is actually present.
|
|
322
|
+
// Without a divider, leave `--_tab-indicator-bottom` unset so a
|
|
323
|
+
// nested TabList's indicator keeps its own default (hugging the
|
|
324
|
+
// tab) instead of floating down into the toolbar's padding.
|
|
325
|
+
hasBottomDivider &&
|
|
326
|
+
dynamicStyles.tabIndicatorBottom(
|
|
327
|
+
`calc(-1 * (${blockPaddingVarForSize[size]} + 1px))`,
|
|
328
|
+
),
|
|
322
329
|
),
|
|
323
330
|
)}
|
|
324
331
|
{...props}>
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import {describe, it, expect, vi} from 'vitest';
|
|
13
|
-
import {render, screen} from '@testing-library/react';
|
|
13
|
+
import {render, screen, fireEvent} from '@testing-library/react';
|
|
14
14
|
import userEvent from '@testing-library/user-event';
|
|
15
15
|
import {TreeList} from './TreeList';
|
|
16
16
|
import type {TreeListItemData} from './TreeListTypes';
|
|
@@ -251,6 +251,37 @@ describe('TreeList', () => {
|
|
|
251
251
|
expect(screen.getByText('Leaf')).toBeInTheDocument();
|
|
252
252
|
});
|
|
253
253
|
|
|
254
|
+
// ===========================================================================
|
|
255
|
+
// Focus-visible outline scoping (regression: focusing a parent row must not
|
|
256
|
+
// leak the ring onto descendant rows — see #4130)
|
|
257
|
+
// ===========================================================================
|
|
258
|
+
|
|
259
|
+
it('scopes the focus-visible outline to the focused row, not its descendants', () => {
|
|
260
|
+
render(<TreeList items={deepItems} />);
|
|
261
|
+
const root = screen.getByText('Root').closest('li')!;
|
|
262
|
+
const mid = screen.getByText('Mid').closest('li')!;
|
|
263
|
+
const leaf = screen.getByText('Leaf').closest('li')!;
|
|
264
|
+
|
|
265
|
+
// A keydown before .focus() establishes keyboard modality so jsdom's
|
|
266
|
+
// :focus-visible heuristic applies deterministically, regardless of
|
|
267
|
+
// pointer events left over from other tests in this file.
|
|
268
|
+
fireEvent.keyDown(document.body, {key: 'Tab'});
|
|
269
|
+
root.focus();
|
|
270
|
+
expect(root).toHaveFocus();
|
|
271
|
+
|
|
272
|
+
expect(
|
|
273
|
+
getComputedStyle(root).getPropertyValue('--_tree-focus-outline'),
|
|
274
|
+
).toContain('solid');
|
|
275
|
+
// Mid and Leaf are DOM descendants of Root's <li> (nested <ul role="group">
|
|
276
|
+
// subtrees) — their own outline var must stay unset, not inherit Root's.
|
|
277
|
+
expect(
|
|
278
|
+
getComputedStyle(mid).getPropertyValue('--_tree-focus-outline'),
|
|
279
|
+
).toBe('none');
|
|
280
|
+
expect(
|
|
281
|
+
getComputedStyle(leaf).getPropertyValue('--_tree-focus-outline'),
|
|
282
|
+
).toBe('none');
|
|
283
|
+
});
|
|
284
|
+
|
|
254
285
|
// ===========================================================================
|
|
255
286
|
// Interactive items
|
|
256
287
|
// ===========================================================================
|
|
@@ -29,7 +29,6 @@ import {mergeProps} from '../utils';
|
|
|
29
29
|
import {useLinkComponent} from '../Link/useLinkComponent';
|
|
30
30
|
import {TreeListBranches} from './TreeListBranches';
|
|
31
31
|
import type {TreeListDensity} from './TreeListTypes';
|
|
32
|
-
import {treeItemScope} from './treeListItem.markers.stylex';
|
|
33
32
|
import {themeProps} from '../utils/themeProps';
|
|
34
33
|
import {useTranslator} from '../i18n';
|
|
35
34
|
|
|
@@ -47,6 +46,19 @@ const styles = stylex.create({
|
|
|
47
46
|
// The treeitem row is the roving-tabindex focus owner; suppress the
|
|
48
47
|
// native focus ring in favor of the row's :focus-visible outline below.
|
|
49
48
|
outline: 'none',
|
|
49
|
+
// Publish this row's own focus state as an inheritable CSS variable
|
|
50
|
+
// instead of matching it via an ancestor selector. Every nested <li>
|
|
51
|
+
// redeclares these vars (default: 'none' / '0'), so a descendant row's
|
|
52
|
+
// default shadows an ancestor's active value — the ring can never leak
|
|
53
|
+
// past the nearest containing treeitem, however deep the tree nests.
|
|
54
|
+
'--_tree-focus-outline': {
|
|
55
|
+
default: 'none',
|
|
56
|
+
':focus-visible': `2px solid ${colorVars['--color-accent']}`,
|
|
57
|
+
},
|
|
58
|
+
'--_tree-focus-outline-offset': {
|
|
59
|
+
default: '0',
|
|
60
|
+
':focus-visible': '2px',
|
|
61
|
+
},
|
|
50
62
|
},
|
|
51
63
|
childGroup: {
|
|
52
64
|
margin: 0,
|
|
@@ -86,17 +98,14 @@ const styles = stylex.create({
|
|
|
86
98
|
},
|
|
87
99
|
focusVisibleOutline: {
|
|
88
100
|
outline: {
|
|
89
|
-
|
|
90
|
-
//
|
|
91
|
-
|
|
92
|
-
//
|
|
93
|
-
[stylex.when.ancestor(':focus-visible', treeItemScope)]:
|
|
94
|
-
`2px solid ${colorVars['--color-accent']}`,
|
|
101
|
+
// Reads the row's own --_tree-focus-outline (published on the <li> in
|
|
102
|
+
// `wrapper`), which resolves to the nearest containing treeitem only.
|
|
103
|
+
default: 'var(--_tree-focus-outline, none)',
|
|
104
|
+
// Also support inner focusable actions.
|
|
95
105
|
':has(:focus-visible)': `2px solid ${colorVars['--color-accent']}`,
|
|
96
106
|
},
|
|
97
107
|
outlineOffset: {
|
|
98
|
-
default: '0',
|
|
99
|
-
[stylex.when.ancestor(':focus-visible', treeItemScope)]: '2px',
|
|
108
|
+
default: 'var(--_tree-focus-outline-offset, 0)',
|
|
100
109
|
':has(:focus-visible)': '2px',
|
|
101
110
|
},
|
|
102
111
|
},
|
|
@@ -451,7 +460,7 @@ export function TreeListItem({
|
|
|
451
460
|
data-tree-id={id}
|
|
452
461
|
data-tree-level={nestedLevel + 1}
|
|
453
462
|
data-tree-disabled={isDisabled || undefined}
|
|
454
|
-
{...stylex.props(styles.wrapper
|
|
463
|
+
{...stylex.props(styles.wrapper)}>
|
|
455
464
|
<div {...stylex.props(styles.treeBranches)}>
|
|
456
465
|
<TreeListBranches
|
|
457
466
|
ancestorsIsLast={ancestorsIsLast}
|
package/src/docs-script.test.mjs
CHANGED
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* @file Subprocess smoke tests for the zero-dependency docs script.
|
|
5
5
|
*
|
|
6
|
-
* `docs.mjs`
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
6
|
+
* `docs.mjs` no longer prints component docs itself — it intentionally
|
|
7
|
+
* redirects to the Astryx CLI (`npx @astryxdesign/cli init`) so agents and
|
|
8
|
+
* humans converge on the CLI + `init` rather than a stale bundled snapshot.
|
|
9
|
+
* These tests spawn the script as a real subprocess from a neutral cwd and
|
|
10
|
+
* assert that, regardless of the arguments passed, it prints the redirect
|
|
11
|
+
* banner and exits 0 instead of rendering a component catalog or per-component
|
|
12
|
+
* docs.
|
|
12
13
|
*/
|
|
13
14
|
|
|
14
15
|
import {describe, it, expect} from 'vitest';
|
|
@@ -29,22 +30,26 @@ function runDocs(args) {
|
|
|
29
30
|
});
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
describe('docs.mjs
|
|
33
|
-
it('
|
|
33
|
+
describe('docs.mjs redirects to the Astryx CLI', () => {
|
|
34
|
+
it('redirects to the CLI instead of printing a component catalog for --list', () => {
|
|
34
35
|
const r = runDocs(['--list']);
|
|
35
36
|
expect(r.error).toBeUndefined();
|
|
36
37
|
expect(r.signal).toBeNull();
|
|
37
38
|
expect(r.status).toBe(0);
|
|
38
|
-
|
|
39
|
-
expect(r.stdout).toMatch(
|
|
39
|
+
// Prints the redirect banner, not a component list.
|
|
40
|
+
expect(r.stdout).toMatch(/docs are served by the Astryx CLI/);
|
|
41
|
+
expect(r.stdout).toMatch(/npx @astryxdesign\/cli init/);
|
|
42
|
+
expect(r.stdout).toMatch(/npx @astryxdesign\/cli component --list/);
|
|
40
43
|
});
|
|
41
44
|
|
|
42
|
-
it('
|
|
45
|
+
it('redirects to the CLI instead of rendering docs for a single component', () => {
|
|
43
46
|
const r = runDocs(['Button']);
|
|
44
47
|
expect(r.error).toBeUndefined();
|
|
45
48
|
expect(r.signal).toBeNull();
|
|
46
49
|
expect(r.status).toBe(0);
|
|
47
|
-
|
|
48
|
-
expect(r.stdout).toMatch(
|
|
50
|
+
// A pure redirect: it must not render the component's docs.
|
|
51
|
+
expect(r.stdout).not.toMatch(/^# Button$/m);
|
|
52
|
+
expect(r.stdout).not.toMatch(/## Props/);
|
|
53
|
+
expect(r.stdout).toMatch(/npx @astryxdesign\/cli component <Name>/);
|
|
49
54
|
});
|
|
50
55
|
});
|