@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
|
@@ -10,11 +10,26 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Drives the active outline item from scroll position. On each scroll
|
|
12
12
|
* (rAF-throttled) it reads live heading positions and marks the last heading
|
|
13
|
-
* whose top has passed its activation line
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
13
|
+
* whose top has passed its activation line. This is stable — it never compares
|
|
14
|
+
* stale cached positions — so the indicator moves monotonically instead of
|
|
15
|
+
* jumping. Defaults to the first item at the top and the last item at the
|
|
16
|
+
* bottom so short final sections still activate.
|
|
17
|
+
*
|
|
18
|
+
* The activation line and the scroll landing are the same number, computed once
|
|
19
|
+
* in `getRestingTop`: scroll-root top + `offset` (a fixed header overlaying the
|
|
20
|
+
* root) + the heading's own `scroll-margin-top`. A heading therefore activates
|
|
21
|
+
* exactly where navigating to it puts it. `offset` and `scroll-margin-top`
|
|
22
|
+
* compose — the header, then the breathing room below it — they do not
|
|
23
|
+
* duplicate each other.
|
|
24
|
+
*
|
|
25
|
+
* It also owns the single navigation path (`scrollTo`), shared by click and
|
|
26
|
+
* keyboard activation: it announces the jump, suppresses scroll-spy for the
|
|
27
|
+
* duration of the programmatic scroll (so the indicator doesn't chase it), and
|
|
28
|
+
* resolves when the scroll settles — via `scrollend` where supported, or a
|
|
29
|
+
* timeout fallback where it is not.
|
|
30
|
+
*
|
|
31
|
+
* The scroll root is the `scrollContainerRef` element when provided, otherwise
|
|
32
|
+
* the nearest scrollable ancestor of the outline, otherwise the viewport.
|
|
18
33
|
*
|
|
19
34
|
* SYNC: When modified, update /packages/core/src/Outline/Outline.tsx
|
|
20
35
|
*/
|
|
@@ -22,6 +37,14 @@
|
|
|
22
37
|
import {useCallback, useEffect, useRef, useState} from 'react';
|
|
23
38
|
import type {OutlineItem} from './types';
|
|
24
39
|
|
|
40
|
+
/**
|
|
41
|
+
* How long to wait for a programmatic smooth scroll to settle when the
|
|
42
|
+
* `scrollend` event never arrives — either because the browser does not
|
|
43
|
+
* support it (Safari at time of writing) or because the target was already in
|
|
44
|
+
* position, so no scroll happened at all and no events were emitted.
|
|
45
|
+
*/
|
|
46
|
+
const SCROLL_SETTLE_TIMEOUT_MS = 1200;
|
|
47
|
+
|
|
25
48
|
/** Keys that scroll the viewport — used to detect a manual scroll intent. */
|
|
26
49
|
const SCROLL_KEYS = new Set([
|
|
27
50
|
'ArrowUp',
|
|
@@ -58,26 +81,82 @@ function getScrollableAncestor(
|
|
|
58
81
|
return null;
|
|
59
82
|
}
|
|
60
83
|
|
|
84
|
+
/** The element's own `scroll-margin-top` in px (0 when unset). */
|
|
85
|
+
function getScrollMarginTop(element: HTMLElement): number {
|
|
86
|
+
return (
|
|
87
|
+
Number.parseFloat(window.getComputedStyle(element).scrollMarginTop) || 0
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The top of the scroll root in viewport coordinates (0 for the viewport). */
|
|
92
|
+
function getRootTop(scrollRoot: HTMLElement | null): number {
|
|
93
|
+
return scrollRoot != null ? scrollRoot.getBoundingClientRect().top : 0;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Where a heading comes to rest, in viewport coordinates: below the fixed
|
|
98
|
+
* header (`offset`) and below its own `scroll-margin-top` breathing room.
|
|
99
|
+
*
|
|
100
|
+
* This is the single source of truth shared by the activation line and the
|
|
101
|
+
* scroll landing — the two must agree, or a heading activates somewhere other
|
|
102
|
+
* than where navigating to it puts it.
|
|
103
|
+
*/
|
|
104
|
+
function getRestingTop(
|
|
105
|
+
target: HTMLElement,
|
|
106
|
+
scrollRoot: HTMLElement | null,
|
|
107
|
+
offset: number,
|
|
108
|
+
): number {
|
|
109
|
+
return getRootTop(scrollRoot) + offset + getScrollMarginTop(target);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Bring `target` to rest at the top of the scroll root, below any fixed header.
|
|
114
|
+
*
|
|
115
|
+
* With no `offset`, this is the CSS-native path: `scrollIntoView` already
|
|
116
|
+
* honors the heading's `scroll-margin-top`, and it walks *every* scrollable
|
|
117
|
+
* ancestor, so the browser does it better than we can.
|
|
118
|
+
*
|
|
119
|
+
* An `offset` describes a fixed header overlaying the top of the scroll root —
|
|
120
|
+
* which the browser cannot know about, so `scrollIntoView` would park the
|
|
121
|
+
* heading underneath it, hidden. Compute the landing explicitly instead, from
|
|
122
|
+
* the same {@link getRestingTop} the activation line uses.
|
|
123
|
+
*/
|
|
124
|
+
function scrollToTarget(
|
|
125
|
+
target: HTMLElement,
|
|
126
|
+
scrollRoot: HTMLElement | null,
|
|
127
|
+
scrollTarget: HTMLElement | Window,
|
|
128
|
+
offset: number,
|
|
129
|
+
): void {
|
|
130
|
+
if (offset === 0) {
|
|
131
|
+
target.scrollIntoView({behavior: 'smooth', block: 'start'});
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const delta =
|
|
136
|
+
target.getBoundingClientRect().top -
|
|
137
|
+
getRestingTop(target, scrollRoot, offset);
|
|
138
|
+
scrollTarget.scrollBy({top: delta, behavior: 'smooth'});
|
|
139
|
+
}
|
|
140
|
+
|
|
61
141
|
/**
|
|
62
142
|
* Resolve the active heading id from current scroll position.
|
|
63
143
|
*
|
|
64
|
-
* A heading is "passed" once its top reaches its activation line —
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
144
|
+
* A heading is "passed" once its top reaches its activation line — exactly
|
|
145
|
+
* where navigating to it would land it ({@link getRestingTop}): the scroll
|
|
146
|
+
* root's top, plus `offset` (a fixed header overlaying the scroll root), plus
|
|
147
|
+
* the heading's own scroll-margin-top. The active heading is the last passed
|
|
148
|
+
* one (headings are in document order). When none have passed (scrolled above
|
|
149
|
+
* the first), the first item is active; at the bottom, the last item is active.
|
|
69
150
|
*/
|
|
70
151
|
function resolveActiveId(
|
|
71
152
|
items: OutlineItem[],
|
|
72
153
|
scrollRoot: HTMLElement | null,
|
|
154
|
+
offset: number,
|
|
73
155
|
): string | undefined {
|
|
74
156
|
if (items.length === 0) {
|
|
75
157
|
return undefined;
|
|
76
158
|
}
|
|
77
159
|
|
|
78
|
-
const rootTop =
|
|
79
|
-
scrollRoot != null ? scrollRoot.getBoundingClientRect().top : 0;
|
|
80
|
-
|
|
81
160
|
const atBottom =
|
|
82
161
|
scrollRoot != null
|
|
83
162
|
? scrollRoot.scrollTop + scrollRoot.clientHeight >=
|
|
@@ -94,11 +173,10 @@ function resolveActiveId(
|
|
|
94
173
|
if (element == null) {
|
|
95
174
|
continue;
|
|
96
175
|
}
|
|
97
|
-
const top = element.getBoundingClientRect().top;
|
|
98
|
-
const marginTop =
|
|
99
|
-
Number.parseFloat(window.getComputedStyle(element).scrollMarginTop) || 0;
|
|
100
176
|
|
|
101
|
-
|
|
177
|
+
// 1px of tolerance absorbs sub-pixel rounding after a scroll lands.
|
|
178
|
+
const top = element.getBoundingClientRect().top;
|
|
179
|
+
if (top <= getRestingTop(element, scrollRoot, offset) + 1) {
|
|
102
180
|
activeId = item.id;
|
|
103
181
|
} else {
|
|
104
182
|
break;
|
|
@@ -112,6 +190,20 @@ interface UseScrollSpyOptions {
|
|
|
112
190
|
items: OutlineItem[];
|
|
113
191
|
onActiveIdChange?: (id: string) => void;
|
|
114
192
|
rootRef: React.RefObject<HTMLElement | null>;
|
|
193
|
+
/**
|
|
194
|
+
* Height in px of a fixed header overlaying the top of the scroll root.
|
|
195
|
+
* Shifts both the activation line and the scroll landing by the same amount,
|
|
196
|
+
* on top of each heading's own `scroll-margin-top`.
|
|
197
|
+
*/
|
|
198
|
+
offset?: number;
|
|
199
|
+
/** Scroll container to track, instead of the nearest scrollable ancestor. */
|
|
200
|
+
scrollContainerRef?: React.RefObject<HTMLElement | null>;
|
|
201
|
+
/** Whether {@link UseScrollSpyResult.scrollTo} performs the smooth scroll. */
|
|
202
|
+
hasScrollOnClick?: boolean;
|
|
203
|
+
/** Called when a navigation begins, before the scroll starts. */
|
|
204
|
+
onNavigateStart?: (id: string) => void;
|
|
205
|
+
/** Called once per navigation, when the scroll settles or is interrupted. */
|
|
206
|
+
onNavigateEnd?: (id: string) => void;
|
|
115
207
|
}
|
|
116
208
|
|
|
117
209
|
interface UseScrollSpyResult {
|
|
@@ -119,13 +211,19 @@ interface UseScrollSpyResult {
|
|
|
119
211
|
/** Set the active id (notifies onActiveIdChange). For controlled consumers. */
|
|
120
212
|
setActiveId: (id: string) => void;
|
|
121
213
|
/**
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
* scroll
|
|
214
|
+
* Navigate to the item with id `id` — the single path shared by click and
|
|
215
|
+
* keyboard activation.
|
|
216
|
+
*
|
|
217
|
+
* Fires `onNavigateStart(id)`, scrolls (unless `hasScrollOnClick` is false),
|
|
218
|
+
* and fires `onNavigateEnd(id)` exactly once when the scroll settles or the
|
|
219
|
+
* user interrupts it. While uncontrolled, scroll-spy is suppressed for the
|
|
220
|
+
* duration so the indicator doesn't chase the scroll through intervening
|
|
221
|
+
* sections; it lands on the target once the scroll settles, or resumes
|
|
222
|
+
* position tracking if the user scrolls away mid-flight.
|
|
223
|
+
*
|
|
224
|
+
* Returns false (and does nothing) when no element with `id` exists.
|
|
127
225
|
*/
|
|
128
|
-
|
|
226
|
+
scrollTo: (id: string) => boolean;
|
|
129
227
|
}
|
|
130
228
|
|
|
131
229
|
export function useScrollSpy({
|
|
@@ -133,18 +231,29 @@ export function useScrollSpy({
|
|
|
133
231
|
items,
|
|
134
232
|
onActiveIdChange,
|
|
135
233
|
rootRef,
|
|
234
|
+
offset = 0,
|
|
235
|
+
scrollContainerRef,
|
|
236
|
+
hasScrollOnClick = true,
|
|
237
|
+
onNavigateStart,
|
|
238
|
+
onNavigateEnd,
|
|
136
239
|
}: UseScrollSpyOptions): UseScrollSpyResult {
|
|
137
240
|
const isControlled = activeId !== undefined;
|
|
138
241
|
const [uncontrolledActiveId, setUncontrolledActiveId] = useState<
|
|
139
242
|
string | undefined
|
|
140
243
|
>(items[0]?.id);
|
|
141
244
|
const activeIdRef = useRef<string | undefined>(activeId);
|
|
142
|
-
// While true, scroll-spy ignores scroll updates because a
|
|
143
|
-
// programmatic scroll. Released when
|
|
245
|
+
// While true, scroll-spy ignores scroll updates because a navigation is
|
|
246
|
+
// driving a programmatic scroll. Released when it settles or the user scrolls.
|
|
144
247
|
const suppressRef = useRef(false);
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
248
|
+
// The navigation currently in flight, if any.
|
|
249
|
+
const navigationRef = useRef<{
|
|
250
|
+
/** End it (without resuming tracking) because a new one is starting. */
|
|
251
|
+
supersede: () => void;
|
|
252
|
+
/** Drop its listeners without firing onNavigateEnd (unmount). */
|
|
253
|
+
teardown: () => void;
|
|
254
|
+
} | null>(null);
|
|
255
|
+
// Latest scroll-position resolver, so a navigation can resume tracking when
|
|
256
|
+
// the user scrolls during the programmatic scroll.
|
|
148
257
|
const syncRef = useRef<(() => void) | null>(null);
|
|
149
258
|
// Keep latest items/callback in refs so the scroll listener effect doesn't
|
|
150
259
|
// re-subscribe on every render (items is a fresh array each render).
|
|
@@ -155,12 +264,23 @@ export function useScrollSpy({
|
|
|
155
264
|
const itemIds = items.map(item => item.id).join('\n');
|
|
156
265
|
activeIdRef.current = isControlled ? activeId : uncontrolledActiveId;
|
|
157
266
|
|
|
267
|
+
/**
|
|
268
|
+
* The element whose scroll position drives the outline: the explicit
|
|
269
|
+
* container when scoped, else the nearest scrollable ancestor, else null
|
|
270
|
+
* (meaning the viewport).
|
|
271
|
+
*/
|
|
272
|
+
const getScrollRoot = useCallback((): HTMLElement | null => {
|
|
273
|
+
return (
|
|
274
|
+
scrollContainerRef?.current ?? getScrollableAncestor(rootRef.current)
|
|
275
|
+
);
|
|
276
|
+
}, [rootRef, scrollContainerRef]);
|
|
277
|
+
|
|
158
278
|
useEffect(() => {
|
|
159
279
|
if (isControlled || typeof window === 'undefined') {
|
|
160
280
|
return;
|
|
161
281
|
}
|
|
162
282
|
|
|
163
|
-
const scrollRoot =
|
|
283
|
+
const scrollRoot = getScrollRoot();
|
|
164
284
|
const scrollTarget: HTMLElement | Window = scrollRoot ?? window;
|
|
165
285
|
|
|
166
286
|
let frame = 0;
|
|
@@ -169,7 +289,11 @@ export function useScrollSpy({
|
|
|
169
289
|
if (suppressRef.current) {
|
|
170
290
|
return;
|
|
171
291
|
}
|
|
172
|
-
const nextActiveId = resolveActiveId(
|
|
292
|
+
const nextActiveId = resolveActiveId(
|
|
293
|
+
itemsRef.current,
|
|
294
|
+
scrollRoot,
|
|
295
|
+
offset,
|
|
296
|
+
);
|
|
173
297
|
if (nextActiveId != null && nextActiveId !== activeIdRef.current) {
|
|
174
298
|
activeIdRef.current = nextActiveId;
|
|
175
299
|
setUncontrolledActiveId(nextActiveId);
|
|
@@ -195,12 +319,12 @@ export function useScrollSpy({
|
|
|
195
319
|
cancelAnimationFrame(frame);
|
|
196
320
|
}
|
|
197
321
|
};
|
|
198
|
-
}, [isControlled, itemIds,
|
|
322
|
+
}, [isControlled, itemIds, offset, getScrollRoot]);
|
|
199
323
|
|
|
200
|
-
// Tear down any
|
|
324
|
+
// Tear down any in-flight navigation's listeners when the Outline unmounts.
|
|
201
325
|
useEffect(() => {
|
|
202
326
|
return () => {
|
|
203
|
-
|
|
327
|
+
navigationRef.current?.teardown();
|
|
204
328
|
};
|
|
205
329
|
}, []);
|
|
206
330
|
|
|
@@ -211,65 +335,117 @@ export function useScrollSpy({
|
|
|
211
335
|
onActiveIdChange?.(nextActiveId);
|
|
212
336
|
};
|
|
213
337
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
338
|
+
// Defined in render (not memoized) so it always closes over the current props
|
|
339
|
+
// — it only ever runs from an event handler, never during render.
|
|
340
|
+
const scrollTo = (id: string): boolean => {
|
|
341
|
+
const target =
|
|
342
|
+
typeof document !== 'undefined' ? document.getElementById(id) : null;
|
|
343
|
+
if (target == null) {
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// A second navigation replaces the first: end the old one (balancing its
|
|
348
|
+
// onNavigateStart) without resuming tracking, since we are about to
|
|
349
|
+
// suppress it again anyway.
|
|
350
|
+
navigationRef.current?.supersede();
|
|
351
|
+
|
|
352
|
+
onNavigateStart?.(id);
|
|
353
|
+
|
|
354
|
+
if (isControlled) {
|
|
355
|
+
// The consumer owns the active state — notify only.
|
|
356
|
+
setActiveId(id);
|
|
357
|
+
} else {
|
|
358
|
+
// Freeze the indicator during the programmatic scroll instead of moving
|
|
359
|
+
// it immediately: it lands on the target once the scroll settles, so it
|
|
360
|
+
// doesn't chase the scroll through intervening sections.
|
|
361
|
+
suppressRef.current = true;
|
|
220
362
|
}
|
|
221
363
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
// settles, so it doesn't chase the scroll through intervening sections.
|
|
225
|
-
suppressRef.current = true;
|
|
226
|
-
// Replace any in-flight handlers from a previous click.
|
|
227
|
-
releaseSuppressionRef.current?.();
|
|
364
|
+
const scrollRoot = getScrollRoot();
|
|
365
|
+
const scrollTarget: HTMLElement | Window = scrollRoot ?? window;
|
|
228
366
|
|
|
229
367
|
let settleTimer = 0;
|
|
368
|
+
let isSettled = false;
|
|
369
|
+
|
|
230
370
|
const cleanup = () => {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
371
|
+
scrollTarget.removeEventListener('scrollend', onSettle);
|
|
372
|
+
scrollTarget.removeEventListener('wheel', onManual);
|
|
373
|
+
scrollTarget.removeEventListener('touchmove', onManual);
|
|
234
374
|
window.removeEventListener('keydown', onKeyDown);
|
|
235
375
|
if (settleTimer !== 0) {
|
|
236
376
|
clearTimeout(settleTimer);
|
|
237
377
|
settleTimer = 0;
|
|
238
378
|
}
|
|
239
|
-
|
|
379
|
+
navigationRef.current = null;
|
|
240
380
|
};
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* End the navigation exactly once. `didArrive` is false when the user took
|
|
384
|
+
* over with a manual scroll mid-flight. `onNavigateEnd` fires either way,
|
|
385
|
+
* so every `onNavigateStart` is balanced and a consumer's "navigating"
|
|
386
|
+
* state can never leak.
|
|
387
|
+
*/
|
|
388
|
+
const finish = (didArrive: boolean, shouldResume = true) => {
|
|
389
|
+
if (isSettled) {
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
isSettled = true;
|
|
251
393
|
cleanup();
|
|
252
|
-
|
|
253
|
-
|
|
394
|
+
|
|
395
|
+
if (!isControlled) {
|
|
396
|
+
suppressRef.current = false;
|
|
397
|
+
if (didArrive) {
|
|
398
|
+
setUncontrolledActiveId(id);
|
|
399
|
+
activeIdRef.current = id;
|
|
400
|
+
onActiveIdChangeRef.current?.(id);
|
|
401
|
+
} else if (shouldResume) {
|
|
402
|
+
// Hand control back to scroll-position tracking.
|
|
403
|
+
syncRef.current?.();
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
onNavigateEnd?.(id);
|
|
254
408
|
};
|
|
409
|
+
|
|
410
|
+
const onSettle = () => finish(true);
|
|
411
|
+
const onManual = () => finish(false);
|
|
255
412
|
const onKeyDown = (event: KeyboardEvent) => {
|
|
256
|
-
|
|
257
|
-
|
|
413
|
+
// A key the outline itself consumed (arrow roving focus, Space
|
|
414
|
+
// activation) is prevented, so the browser will not scroll — it is not a
|
|
415
|
+
// manual scroll intent and must not cancel the navigation it just began.
|
|
416
|
+
if (!event.defaultPrevented && SCROLL_KEYS.has(event.key)) {
|
|
417
|
+
finish(false);
|
|
258
418
|
}
|
|
259
419
|
};
|
|
260
420
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
421
|
+
if (hasScrollOnClick) {
|
|
422
|
+
// Arm settle detection *before* scrolling so an instant jump (a target
|
|
423
|
+
// already in position, or prefers-reduced-motion collapsing the smooth
|
|
424
|
+
// scroll) cannot land before anyone is listening.
|
|
425
|
+
scrollTarget.addEventListener('scrollend', onSettle, {once: true});
|
|
426
|
+
scrollTarget.addEventListener('wheel', onManual, {passive: true});
|
|
427
|
+
scrollTarget.addEventListener('touchmove', onManual, {passive: true});
|
|
428
|
+
window.addEventListener('keydown', onKeyDown);
|
|
429
|
+
settleTimer = window.setTimeout(onSettle, SCROLL_SETTLE_TIMEOUT_MS);
|
|
430
|
+
navigationRef.current = {
|
|
431
|
+
supersede: () => finish(false, false),
|
|
432
|
+
teardown: cleanup,
|
|
433
|
+
};
|
|
434
|
+
scrollToTarget(target, scrollRoot, scrollTarget, offset);
|
|
435
|
+
} else {
|
|
436
|
+
// The consumer owns scrolling — there is no scroll to wait for, so the
|
|
437
|
+
// navigation is already at rest. Resolving immediately (instead of
|
|
438
|
+
// waiting on the settle timeout) keeps onNavigateEnd usable for arrival
|
|
439
|
+
// effects even when hasScrollOnClick is false.
|
|
440
|
+
finish(true);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return true;
|
|
444
|
+
};
|
|
269
445
|
|
|
270
446
|
return {
|
|
271
447
|
activeId: isControlled ? activeId : uncontrolledActiveId,
|
|
272
448
|
setActiveId,
|
|
273
|
-
|
|
449
|
+
scrollTo,
|
|
274
450
|
};
|
|
275
451
|
}
|
|
@@ -20,7 +20,7 @@ export const docs = {
|
|
|
20
20
|
name: 'icon',
|
|
21
21
|
type: 'IconType',
|
|
22
22
|
description:
|
|
23
|
-
'Icon displayed before the label. See `
|
|
23
|
+
'Icon displayed before the label. See `astryx docs icons` for valid semantic names.',
|
|
24
24
|
},
|
|
25
25
|
{
|
|
26
26
|
name: 'description',
|
|
@@ -23,12 +23,12 @@ export const docs = {
|
|
|
23
23
|
{
|
|
24
24
|
name: 'icon',
|
|
25
25
|
type: 'IconType',
|
|
26
|
-
description: 'Icon displayed in the outline (unselected) variant. See `
|
|
26
|
+
description: 'Icon displayed in the outline (unselected) variant. See `astryx docs icons` for valid semantic names.',
|
|
27
27
|
},
|
|
28
28
|
{
|
|
29
29
|
name: 'selectedIcon',
|
|
30
30
|
type: 'IconType',
|
|
31
|
-
description: 'Icon displayed when the item is selected (filled variant). See `
|
|
31
|
+
description: 'Icon displayed when the item is selected (filled variant). See `astryx docs icons` for valid semantic names.',
|
|
32
32
|
},
|
|
33
33
|
{
|
|
34
34
|
name: 'isSelected',
|
|
@@ -107,7 +107,7 @@ export const docs = {
|
|
|
107
107
|
{
|
|
108
108
|
name: 'labelIcon',
|
|
109
109
|
type: 'IconType',
|
|
110
|
-
description: 'Icon displayed before the label text. See `
|
|
110
|
+
description: 'Icon displayed before the label text. See `astryx docs icons` for valid semantic names.',
|
|
111
111
|
},
|
|
112
112
|
{
|
|
113
113
|
name: 'labelTooltip',
|
package/src/TabList/Tab.tsx
CHANGED
|
@@ -138,7 +138,11 @@ const styles = stylex.create({
|
|
|
138
138
|
},
|
|
139
139
|
indicator: {
|
|
140
140
|
position: 'absolute',
|
|
141
|
-
bottom
|
|
141
|
+
// Sits on the tab's bottom edge by default (-1px). When the tab strip
|
|
142
|
+
// reserves space for a divider rail — TabList `hasDivider` or a Toolbar
|
|
143
|
+
// with a bottom divider — that ancestor sets `--_tab-indicator-bottom`
|
|
144
|
+
// to drop the indicator onto the rail beneath the reserved gap.
|
|
145
|
+
bottom: 'var(--_tab-indicator-bottom, -1px)',
|
|
142
146
|
left: spacingVars['--spacing-3'],
|
|
143
147
|
right: spacingVars['--spacing-3'],
|
|
144
148
|
height: '2px',
|
|
@@ -92,7 +92,7 @@ export const docs = {
|
|
|
92
92
|
bestPractices: [
|
|
93
93
|
{ guidance: true, description: 'Keep tab labels short and descriptive so users can quickly scan available sections.' },
|
|
94
94
|
{ guidance: true, description: 'Use TabMenu to group overflow items when horizontal space is limited rather than scrolling tabs off-screen.' },
|
|
95
|
-
{ guidance: true, description: 'When using hasDivider with action buttons alongside tabs,
|
|
95
|
+
{ guidance: true, description: 'When using hasDivider with action buttons alongside tabs, match the Button size to the TabList size (both md, both sm); the divided tab strip reserves space so tabs and same-size buttons align to a shared baseline above the rail.' },
|
|
96
96
|
{ guidance: false, description: 'Use tabs for sequential steps or workflows; use a stepper or wizard pattern instead.' },
|
|
97
97
|
{ guidance: false, description: 'Place more than 6–8 visible tabs before the overflow menu; prioritize the most important categories.' },
|
|
98
98
|
{ guidance: false, description: 'Confuse TabList with SegmentedControl or ToggleButton. TabList is for navigation between views. SegmentedControl and ToggleButton are input controls: SegmentedControl always has exactly one selected option, while ToggleButton can be toggled on or off.' },
|
|
@@ -113,7 +113,7 @@ export const docsZh = {
|
|
|
113
113
|
bestPractices: [
|
|
114
114
|
{ guidance: true, description: 'Keep tab labels short and descriptive so users can quickly scan available sections.' },
|
|
115
115
|
{ guidance: true, description: 'Use TabMenu to group overflow items when horizontal space is limited rather than scrolling tabs off-screen.' },
|
|
116
|
-
{ guidance: true, description: 'When using hasDivider with action buttons alongside tabs,
|
|
116
|
+
{ guidance: true, description: 'When using hasDivider with action buttons alongside tabs, match the Button size to the TabList size (both md, both sm); the divided tab strip reserves space so tabs and same-size buttons align to a shared baseline above the rail.' },
|
|
117
117
|
{ guidance: false, description: 'Use tabs for sequential steps or workflows; use a stepper or wizard pattern instead.' },
|
|
118
118
|
{ guidance: false, description: 'Place more than 6–8 visible tabs before the overflow menu; prioritize the most important categories.' },
|
|
119
119
|
{ guidance: false, description: 'Confuse TabList with SegmentedControl or ToggleButton. TabList is for navigation between views. SegmentedControl and ToggleButton are input controls: SegmentedControl always has exactly one selected option, while ToggleButton can be toggled on or off.' },
|
|
@@ -135,7 +135,7 @@ export const docsDense = {
|
|
|
135
135
|
bestPractices: [
|
|
136
136
|
{ guidance: true, description: 'Keep tab labels short and descriptive so users can quickly scan available sections.' },
|
|
137
137
|
{ guidance: true, description: 'Use TabMenu to group overflow items when horizontal space is limited rather than scrolling tabs off-screen.' },
|
|
138
|
-
{ guidance: true, description: 'When using hasDivider with action buttons alongside tabs,
|
|
138
|
+
{ guidance: true, description: 'When using hasDivider with action buttons alongside tabs, match the Button size to the TabList size (both md, both sm); the divided tab strip reserves space so tabs and same-size buttons align to a shared baseline above the rail.' },
|
|
139
139
|
{ guidance: false, description: 'Use tabs for sequential steps or workflows; use a stepper or wizard pattern instead.' },
|
|
140
140
|
{ guidance: false, description: 'Place more than 6–8 visible tabs before the overflow menu; prioritize the most important categories.' },
|
|
141
141
|
{ guidance: false, description: 'Confuse TabList with SegmentedControl or ToggleButton. TabList is for navigation between views. SegmentedControl and ToggleButton are input controls: SegmentedControl always has exactly one selected option, while ToggleButton can be toggled on or off.' },
|
|
@@ -323,6 +323,72 @@ describe('TabList', () => {
|
|
|
323
323
|
});
|
|
324
324
|
});
|
|
325
325
|
|
|
326
|
+
describe('TabList divider gap', () => {
|
|
327
|
+
// The divider adds the reserved gap + indicator offset via a single class
|
|
328
|
+
// (StyleX applies deterministic classes in the test env). Capture that
|
|
329
|
+
// class set once so the assertions describe intent, not opaque hashes.
|
|
330
|
+
function navClasses(hasDivider: boolean): Set<string> {
|
|
331
|
+
const {unmount} = render(
|
|
332
|
+
<TabList value="home" onChange={() => {}} hasDivider={hasDivider}>
|
|
333
|
+
<Tab value="home" label="Home" />
|
|
334
|
+
</TabList>,
|
|
335
|
+
);
|
|
336
|
+
const nav = screen.getByRole('navigation');
|
|
337
|
+
const classes = new Set(nav.className.split(/\s+/).filter(Boolean));
|
|
338
|
+
unmount();
|
|
339
|
+
return classes;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
it('adds divider-only styling classes when hasDivider is set', () => {
|
|
343
|
+
const withDivider = navClasses(true);
|
|
344
|
+
const withoutDivider = navClasses(false);
|
|
345
|
+
|
|
346
|
+
// Every class the plain nav has must still be present when divided: the
|
|
347
|
+
// divider only adds styling (border + reserved gap + indicator offset),
|
|
348
|
+
// it never removes the base nav styles.
|
|
349
|
+
for (const cls of withoutDivider) {
|
|
350
|
+
expect(withDivider.has(cls)).toBe(true);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// And it must add at least one class the undivided nav does not have.
|
|
354
|
+
const added = [...withDivider].filter(c => !withoutDivider.has(c));
|
|
355
|
+
expect(added.length).toBeGreaterThan(0);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it('does not add divider styling to an undivided tab list (default)', () => {
|
|
359
|
+
// Default (no hasDivider) and explicit hasDivider={false} are identical:
|
|
360
|
+
// the non-divided path is untouched by the divider gap change.
|
|
361
|
+
expect(navClasses(false)).toEqual(
|
|
362
|
+
(() => {
|
|
363
|
+
const {unmount} = render(
|
|
364
|
+
<TabList value="home" onChange={() => {}}>
|
|
365
|
+
<Tab value="home" label="Home" />
|
|
366
|
+
</TabList>,
|
|
367
|
+
);
|
|
368
|
+
const nav = screen.getByRole('navigation');
|
|
369
|
+
const classes = new Set(nav.className.split(/\s+/).filter(Boolean));
|
|
370
|
+
unmount();
|
|
371
|
+
return classes;
|
|
372
|
+
})(),
|
|
373
|
+
);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it('keeps the selected indicator rendered under a divider', () => {
|
|
377
|
+
render(
|
|
378
|
+
<TabList value="home" onChange={() => {}} hasDivider>
|
|
379
|
+
<Tab value="home" label="Home" />
|
|
380
|
+
<Tab value="away" label="Away" />
|
|
381
|
+
</TabList>,
|
|
382
|
+
);
|
|
383
|
+
const selected = screen.getByRole('button', {name: 'Home'});
|
|
384
|
+
// The indicator span carries the selected marker; the divider must not
|
|
385
|
+
// drop it (it is repositioned onto the rail, not removed).
|
|
386
|
+
expect(
|
|
387
|
+
selected.querySelector('[data-selected="selected"]'),
|
|
388
|
+
).toBeInTheDocument();
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
|
|
326
392
|
describe('TabList keyboard navigation (roving tabindex)', () => {
|
|
327
393
|
it('exposes the tab strip as a single Tab stop (only selected tab is tabbable)', () => {
|
|
328
394
|
render(
|
package/src/TabList/TabList.tsx
CHANGED
|
@@ -96,6 +96,14 @@ const styles = stylex.create({
|
|
|
96
96
|
borderBottomWidth: borderVars['--border-width'],
|
|
97
97
|
borderBottomStyle: 'solid',
|
|
98
98
|
borderBottomColor: colorVars['--color-border'],
|
|
99
|
+
// Reserve a gap between the tabs and the divider rail so the hover pill
|
|
100
|
+
// (which fills the tab height) no longer touches the underline, and an
|
|
101
|
+
// adjacent same-size Button aligns to the tabs rather than butting the
|
|
102
|
+
// rail. The tabs keep their element-size height; this padding grows the
|
|
103
|
+
// strip. `--_tab-indicator-bottom` drops the selected indicator through
|
|
104
|
+
// the reserved gap (+ the 1px border) so it still sits on the rail.
|
|
105
|
+
paddingBlockEnd: spacingVars['--spacing-1'],
|
|
106
|
+
'--_tab-indicator-bottom': `calc(-1 * (${spacingVars['--spacing-1']} + ${borderVars['--border-width']}))`,
|
|
99
107
|
},
|
|
100
108
|
});
|
|
101
109
|
|
package/src/TabList/TabMenu.tsx
CHANGED
|
@@ -122,7 +122,10 @@ const styles = stylex.create({
|
|
|
122
122
|
},
|
|
123
123
|
indicator: {
|
|
124
124
|
position: 'absolute',
|
|
125
|
-
|
|
125
|
+
// Mirrors Tab's indicator: sits on the bottom edge by default (-1px), or
|
|
126
|
+
// drops onto the divider rail when an ancestor (TabList `hasDivider` or a
|
|
127
|
+
// Toolbar with a bottom divider) sets `--_tab-indicator-bottom`.
|
|
128
|
+
bottom: 'var(--_tab-indicator-bottom, -1px)',
|
|
126
129
|
left: spacingVars['--spacing-3'],
|
|
127
130
|
right: spacingVars['--spacing-3'],
|
|
128
131
|
height: '2px',
|
package/src/Table/Table.doc.mjs
CHANGED
|
@@ -111,6 +111,8 @@ export const docs = {
|
|
|
111
111
|
{name: 'useTableSelection'},
|
|
112
112
|
{name: 'useTableSelectionState'},
|
|
113
113
|
{name: 'useTableSortable'},
|
|
114
|
+
{name: 'useTableTreeData'},
|
|
115
|
+
{name: 'useTableTreeState'},
|
|
114
116
|
{name: 'useTablePagination'},
|
|
115
117
|
{name: 'useTableColumnSettings'},
|
|
116
118
|
{name: 'useTableFiltering'},
|
package/src/Table/Table.tsx
CHANGED
|
@@ -275,7 +275,7 @@ function TableInner<T extends Record<string, unknown>>({
|
|
|
275
275
|
* columns={[
|
|
276
276
|
* { key: 'name', header: 'Name', width: proportional(1), renderCell: (u) => (
|
|
277
277
|
* <HStack gap={2} align="center">
|
|
278
|
-
* <Avatar name={u.name} size="
|
|
278
|
+
* <Avatar name={u.name} size="md" />
|
|
279
279
|
* <Text weight="semibold">{u.name}</Text>
|
|
280
280
|
* </HStack>
|
|
281
281
|
* )},
|