@astryxdesign/core 0.1.7 → 0.1.8-canary.08a0957
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/AppShell/AppShell.d.ts.map +1 -1
- package/dist/AppShell/AppShell.js +6 -2
- package/dist/AppShell/AppShellMobileContext.d.ts +8 -0
- package/dist/AppShell/AppShellMobileContext.d.ts.map +1 -1
- 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/Banner/Banner.d.ts.map +1 -1
- package/dist/Button/Button.d.ts.map +1 -1
- package/dist/Button/Button.js +1 -0
- 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/ChatLayout.d.ts.map +1 -1
- package/dist/Chat/ChatLayoutScrollButton.d.ts.map +1 -1
- 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/Chat/ChatMessageMetadata.d.ts.map +1 -1
- package/dist/Chat/ChatSendButton.d.ts.map +1 -1
- package/dist/Chat/useTriggerMenu.d.ts.map +1 -1
- package/dist/Citation/Citation.d.ts.map +1 -1
- package/dist/Citation/Citation.js +10 -4
- package/dist/CodeBlock/CodeBlock.d.ts.map +1 -1
- 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/CommandPalette/CommandPaletteInput.d.ts +7 -1
- package/dist/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
- package/dist/CommandPalette/CommandPaletteInput.js +6 -1
- 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/DateTimeInput/DateTimeInput.d.ts.map +1 -1
- package/dist/Dialog/Dialog.d.ts.map +1 -1
- package/dist/Dialog/Dialog.js +2 -1
- package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenu.js +8 -4
- package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts +88 -0
- package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts.map +1 -0
- package/dist/DropdownMenu/DropdownMenuCheckboxItem.js +169 -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 +68 -0
- package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts.map +1 -0
- package/dist/DropdownMenu/DropdownMenuRadioGroup.js +69 -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/FileInput/FileInput.d.ts.map +1 -1
- 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/MobileNav/MobileNav.d.ts.map +1 -1
- package/dist/MobileNav/MobileNav.js +6 -1
- package/dist/MobileNav/MobileNavToggle.d.ts.map +1 -1
- package/dist/MobileNav/MobileNavToggle.js +4 -0
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.js +36 -10
- package/dist/NumberInput/NumberInput.d.ts.map +1 -1
- package/dist/Outline/Outline.d.ts +68 -3
- package/dist/Outline/Outline.d.ts.map +1 -1
- package/dist/Outline/Outline.js +128 -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/PowerSearch/usePowerSearchSource.d.ts.map +1 -1
- package/dist/SideNav/SideNavCollapseButton.d.ts.map +1 -1
- package/dist/SideNav/SideNavItem.d.ts.map +1 -1
- 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 +4 -0
- package/dist/Table/index.d.ts.map +1 -1
- package/dist/Table/index.js +2 -0
- package/dist/Table/plugins/groupedRows/useTableGroupedRows.d.ts.map +1 -1
- package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts.map +1 -1
- package/dist/Table/plugins/rowExpansion/useTableRowExpansion.js +24 -2
- package/dist/Table/plugins/rowStatus/index.d.ts +3 -0
- package/dist/Table/plugins/rowStatus/index.d.ts.map +1 -0
- package/dist/Table/plugins/rowStatus/index.js +3 -0
- package/dist/Table/plugins/rowStatus/useTableRowStatus.d.ts +60 -0
- package/dist/Table/plugins/rowStatus/useTableRowStatus.d.ts.map +1 -0
- package/dist/Table/plugins/rowStatus/useTableRowStatus.js +145 -0
- 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 +26 -4
- package/dist/astryx.umd.js +54 -54
- package/dist/astryx.umd.js.map +4 -4
- package/dist/authoring/doc.d.ts.map +1 -1
- 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/AppShell/AppShell.tsx +6 -0
- package/src/AppShell/AppShellMobileContext.tsx +8 -0
- package/src/AppShell/useAppShellMobile.doc.mjs +8 -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/Banner/Banner.tsx +10 -2
- package/src/Button/Button.test.tsx +21 -0
- package/src/Button/Button.tsx +1 -0
- 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/ChatLayout.tsx +3 -1
- package/src/Chat/ChatLayoutScrollButton.tsx +3 -1
- package/src/Chat/ChatMessage.tsx +4 -2
- package/src/Chat/ChatMessageMetadata.tsx +1 -2
- package/src/Chat/ChatSendButton.tsx +5 -1
- package/src/Chat/useTriggerMenu.tsx +3 -1
- 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/CodeBlock/CodeBlock.tsx +3 -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/CommandPalette/CommandPaletteInput.doc.mjs +26 -9
- package/src/CommandPalette/CommandPaletteInput.test.tsx +34 -0
- package/src/CommandPalette/CommandPaletteInput.tsx +11 -0
- package/src/ContextMenu/ContextMenu.test.tsx +75 -1
- package/src/ContextMenu/ContextMenu.tsx +10 -5
- package/src/ContextMenu/index.ts +15 -0
- package/src/DateTimeInput/DateTimeInput.tsx +3 -1
- package/src/Dialog/Dialog.tsx +12 -2
- 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 +236 -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 +118 -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/FileInput/FileInput.tsx +4 -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/MobileNav/MobileNav.tsx +12 -1
- package/src/MobileNav/MobileNavToggle.test.tsx +116 -0
- package/src/MobileNav/MobileNavToggle.tsx +9 -1
- package/src/MultiSelector/MultiSelector.doc.mjs +4 -2
- package/src/MultiSelector/MultiSelector.test.tsx +79 -1
- package/src/MultiSelector/MultiSelector.tsx +51 -11
- package/src/NumberInput/NumberInput.doc.mjs +2 -2
- package/src/NumberInput/NumberInput.tsx +1 -2
- package/src/Outline/Outline.doc.mjs +123 -3
- package/src/Outline/Outline.test.tsx +735 -0
- package/src/Outline/Outline.tsx +183 -33
- package/src/Outline/useScrollSpy.ts +248 -72
- package/src/PowerSearch/usePowerSearchSource.ts +1 -2
- package/src/Selector/SelectorOption.doc.mjs +1 -1
- package/src/SideNav/SideNavCollapseButton.tsx +6 -1
- package/src/SideNav/SideNavItem.doc.mjs +2 -2
- package/src/SideNav/SideNavItem.tsx +5 -1
- 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 +12 -0
- package/src/Table/plugins/groupedRows/useTableGroupedRows.tsx +6 -2
- package/src/Table/plugins/rowExpansion/useTableRowExpansion.test.tsx +90 -1
- package/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx +37 -5
- package/src/Table/plugins/rowStatus/index.ts +7 -0
- package/src/Table/plugins/rowStatus/useTableRowStatus.test.tsx +138 -0
- package/src/Table/plugins/rowStatus/useTableRowStatus.tsx +189 -0
- 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/useTableRowStatus.doc.mjs +30 -0
- 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/authoring/doc.ts +1 -3
- 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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
2
|
|
|
3
3
|
import {describe, it, expect, vi} from 'vitest';
|
|
4
|
-
import {render, screen} from '@testing-library/react';
|
|
4
|
+
import {render, screen, fireEvent} from '@testing-library/react';
|
|
5
5
|
import {Carousel} from './Carousel';
|
|
6
6
|
|
|
7
7
|
// Mock ResizeObserver (not available in jsdom)
|
|
@@ -111,4 +111,99 @@ describe('Carousel', () => {
|
|
|
111
111
|
expect(left).toBeDisabled();
|
|
112
112
|
expect(right).toBeDisabled();
|
|
113
113
|
});
|
|
114
|
+
|
|
115
|
+
describe('Shift + wheel horizontal scroll', () => {
|
|
116
|
+
// jsdom doesn't lay out elements, so we fake an overflowing scroll
|
|
117
|
+
// container and capture scrollBy calls.
|
|
118
|
+
function getScroller() {
|
|
119
|
+
const region = screen.getByRole('region');
|
|
120
|
+
return region.firstElementChild as HTMLElement;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function makeOverflowing(el: HTMLElement) {
|
|
124
|
+
Object.defineProperty(el, 'scrollWidth', {
|
|
125
|
+
value: 500,
|
|
126
|
+
configurable: true,
|
|
127
|
+
});
|
|
128
|
+
Object.defineProperty(el, 'clientWidth', {
|
|
129
|
+
value: 200,
|
|
130
|
+
configurable: true,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
it('maps Shift + vertical wheel to horizontal scroll', () => {
|
|
135
|
+
render(
|
|
136
|
+
<Carousel aria-label="Wheel">
|
|
137
|
+
<div>Item 1</div>
|
|
138
|
+
<div>Item 2</div>
|
|
139
|
+
</Carousel>,
|
|
140
|
+
);
|
|
141
|
+
const scroller = getScroller();
|
|
142
|
+
makeOverflowing(scroller);
|
|
143
|
+
const scrollBy = vi.fn();
|
|
144
|
+
scroller.scrollBy = scrollBy;
|
|
145
|
+
|
|
146
|
+
fireEvent.wheel(scroller, {shiftKey: true, deltaY: 120, deltaX: 0});
|
|
147
|
+
|
|
148
|
+
expect(scrollBy).toHaveBeenCalledWith({left: 120, behavior: 'auto'});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('does not translate wheel without Shift held', () => {
|
|
152
|
+
render(
|
|
153
|
+
<Carousel aria-label="Wheel">
|
|
154
|
+
<div>Item 1</div>
|
|
155
|
+
<div>Item 2</div>
|
|
156
|
+
</Carousel>,
|
|
157
|
+
);
|
|
158
|
+
const scroller = getScroller();
|
|
159
|
+
makeOverflowing(scroller);
|
|
160
|
+
const scrollBy = vi.fn();
|
|
161
|
+
scroller.scrollBy = scrollBy;
|
|
162
|
+
|
|
163
|
+
fireEvent.wheel(scroller, {shiftKey: false, deltaY: 120, deltaX: 0});
|
|
164
|
+
|
|
165
|
+
expect(scrollBy).not.toHaveBeenCalled();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('leaves native horizontal wheel deltas alone', () => {
|
|
169
|
+
// Trackpads emit deltaX directly — don't double-handle those.
|
|
170
|
+
render(
|
|
171
|
+
<Carousel aria-label="Wheel">
|
|
172
|
+
<div>Item 1</div>
|
|
173
|
+
<div>Item 2</div>
|
|
174
|
+
</Carousel>,
|
|
175
|
+
);
|
|
176
|
+
const scroller = getScroller();
|
|
177
|
+
makeOverflowing(scroller);
|
|
178
|
+
const scrollBy = vi.fn();
|
|
179
|
+
scroller.scrollBy = scrollBy;
|
|
180
|
+
|
|
181
|
+
fireEvent.wheel(scroller, {shiftKey: true, deltaY: 120, deltaX: 40});
|
|
182
|
+
|
|
183
|
+
expect(scrollBy).not.toHaveBeenCalled();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('does not intercept when there is no horizontal overflow', () => {
|
|
187
|
+
render(
|
|
188
|
+
<Carousel aria-label="Wheel">
|
|
189
|
+
<div>Item 1</div>
|
|
190
|
+
</Carousel>,
|
|
191
|
+
);
|
|
192
|
+
const scroller = getScroller();
|
|
193
|
+
Object.defineProperty(scroller, 'scrollWidth', {
|
|
194
|
+
value: 200,
|
|
195
|
+
configurable: true,
|
|
196
|
+
});
|
|
197
|
+
Object.defineProperty(scroller, 'clientWidth', {
|
|
198
|
+
value: 200,
|
|
199
|
+
configurable: true,
|
|
200
|
+
});
|
|
201
|
+
const scrollBy = vi.fn();
|
|
202
|
+
scroller.scrollBy = scrollBy;
|
|
203
|
+
|
|
204
|
+
fireEvent.wheel(scroller, {shiftKey: true, deltaY: 120, deltaX: 0});
|
|
205
|
+
|
|
206
|
+
expect(scrollBy).not.toHaveBeenCalled();
|
|
207
|
+
});
|
|
208
|
+
});
|
|
114
209
|
});
|
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
* @input Uses React, StyleX, useScrollOverflow, useLayer, Button, Icon, theme tokens
|
|
8
8
|
* @output Exports Carousel component
|
|
9
9
|
* @position Horizontal scroll container with fade-edge overflow indication,
|
|
10
|
-
* optional prev/next buttons on the top layer, scroll-snap,
|
|
11
|
-
* visual bleed allowance for child selection indicators
|
|
10
|
+
* optional prev/next buttons on the top layer, scroll-snap, a 1px
|
|
11
|
+
* visual bleed allowance for child selection indicators, and Shift + wheel
|
|
12
|
+
* mapping so mouse users can scroll horizontally.
|
|
12
13
|
*
|
|
13
14
|
* SYNC: When modified, update:
|
|
14
15
|
* - /packages/core/src/Carousel/index.ts (exports)
|
|
@@ -290,6 +291,31 @@ export function Carousel({
|
|
|
290
291
|
[scrollRef],
|
|
291
292
|
);
|
|
292
293
|
|
|
294
|
+
// Map Shift + vertical wheel to horizontal scroll. Trackpads emit
|
|
295
|
+
// horizontal deltas natively, but a standard mouse only produces deltaY —
|
|
296
|
+
// so mouse users can't wheel-scroll a horizontal container. Shift + wheel
|
|
297
|
+
// is the long-established convention for horizontal scroll containers; we
|
|
298
|
+
// honor it by translating the vertical delta into a horizontal scroll.
|
|
299
|
+
//
|
|
300
|
+
// Only kicks in when Shift is held and the wheel is purely vertical
|
|
301
|
+
// (deltaX === 0), so native trackpad horizontal scrolling is untouched.
|
|
302
|
+
const handleWheel = useCallback((event: React.WheelEvent<HTMLDivElement>) => {
|
|
303
|
+
if (!event.shiftKey || event.deltaY === 0 || event.deltaX !== 0) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
const el = scrollElRef.current;
|
|
307
|
+
if (!el) {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
// Nothing to scroll horizontally — let the event fall through so the page
|
|
311
|
+
// can scroll as it normally would.
|
|
312
|
+
if (el.scrollWidth <= el.clientWidth) {
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
event.preventDefault();
|
|
316
|
+
el.scrollBy({left: event.deltaY, behavior: 'auto'});
|
|
317
|
+
}, []);
|
|
318
|
+
|
|
293
319
|
const scrollBy = useCallback((direction: -1 | 1) => {
|
|
294
320
|
const el = scrollElRef.current;
|
|
295
321
|
if (!el) {
|
|
@@ -346,6 +372,7 @@ export function Carousel({
|
|
|
346
372
|
<div
|
|
347
373
|
ref={composedRef}
|
|
348
374
|
tabIndex={0}
|
|
375
|
+
onWheel={handleWheel}
|
|
349
376
|
{...stylex.props(
|
|
350
377
|
styles.scroller,
|
|
351
378
|
gapStyles[gap],
|
package/src/Chat/Chat.doc.mjs
CHANGED
|
@@ -80,7 +80,7 @@ export const docs = {
|
|
|
80
80
|
{ name: 'Scroll-to-bottom button', required: false, description: 'Appears when the user scrolls up or new messages arrive. Defaults to ChatLayoutScrollButton; pass null to hide or a custom element to override.' },
|
|
81
81
|
{ name: 'Composer', required: true, description: 'The input area for sending messages, typically ChatComposer. Docked at the bottom inside the frosted glass layer.' },
|
|
82
82
|
{ name: 'Empty state', required: false, description: 'Centered placeholder shown when no messages exist. Use EmptyState for a consistent look.' },
|
|
83
|
-
{ name: 'Avatar', required: false, description: 'A sender avatar rendered beside the message. Typically Avatar with size="
|
|
83
|
+
{ name: 'Avatar', required: false, description: 'A sender avatar rendered beside the message. Typically Avatar with size="md". Hidden for system messages.' },
|
|
84
84
|
{ name: 'Name', required: false, description: 'Sender name above the message body. Place on the bubble when using bubbles, or on the message wrapper for raw content.' },
|
|
85
85
|
{ name: 'Content', required: true, description: 'The message body: one or more ChatMessageBubble elements, or any free-form ReactNode like images or tool calls.' },
|
|
86
86
|
{ name: 'Metadata', required: false, description: 'Timestamp, delivery status, and footer actions below the message. Place on the last bubble or on the message wrapper.' },
|
package/src/Chat/ChatLayout.tsx
CHANGED
|
@@ -276,7 +276,9 @@ export function ChatLayout({
|
|
|
276
276
|
const defaultScrollButton = (
|
|
277
277
|
<ChatLayoutScrollButton
|
|
278
278
|
isVisible={scroll.isScrolledUp || newMsgs.hasNewMessages}
|
|
279
|
-
label={
|
|
279
|
+
label={
|
|
280
|
+
newMsgs.hasNewMessages ? t('@astryx.chatLayout.newMessages') : undefined
|
|
281
|
+
}
|
|
280
282
|
onClick={() => {
|
|
281
283
|
newMsgs.dismiss();
|
|
282
284
|
scroll.scrollToBottom();
|
|
@@ -135,7 +135,9 @@ export function ChatLayoutScrollButton({
|
|
|
135
135
|
)}>
|
|
136
136
|
<Button
|
|
137
137
|
label={label ?? t('@astryx.chatLayoutScrollButton.scrollToBottom')}
|
|
138
|
-
aria-label={
|
|
138
|
+
aria-label={
|
|
139
|
+
label ?? t('@astryx.chatLayoutScrollButton.scrollToBottom')
|
|
140
|
+
}
|
|
139
141
|
icon={<Icon icon="chevronDown" size="md" />}
|
|
140
142
|
variant="ghost"
|
|
141
143
|
size="md"
|
package/src/Chat/ChatMessage.tsx
CHANGED
|
@@ -150,7 +150,7 @@ const styles = stylex.create({
|
|
|
150
150
|
*
|
|
151
151
|
* @example
|
|
152
152
|
* ```
|
|
153
|
-
* <ChatMessage sender="assistant" name="Navi" avatar={<Avatar name="Navi" size="
|
|
153
|
+
* <ChatMessage sender="assistant" name="Navi" avatar={<Avatar name="Navi" size="md" />}>
|
|
154
154
|
* <ChatMessageBubble>Hello!</ChatMessageBubble>
|
|
155
155
|
* <ChatMessageMetadata timestamp="2:30 PM" />
|
|
156
156
|
* </ChatMessage>
|
|
@@ -222,7 +222,9 @@ export function ChatMessage({
|
|
|
222
222
|
{...rest}
|
|
223
223
|
ref={ref}
|
|
224
224
|
data-testid={testId}
|
|
225
|
-
aria-label={
|
|
225
|
+
aria-label={
|
|
226
|
+
!hasName ? t('@astryx.chatMessage.messageFrom', {sender}) : undefined
|
|
227
|
+
}
|
|
226
228
|
aria-labelledby={hasName ? nameId : undefined}
|
|
227
229
|
{...mergeProps(
|
|
228
230
|
themeProps('chat-message', {sender, density}),
|
|
@@ -122,8 +122,7 @@ export function ChatMessageMetadata({
|
|
|
122
122
|
const sender = msgContext?.sender ?? 'assistant';
|
|
123
123
|
|
|
124
124
|
const statusConfig = status != null ? STATUS_CONFIG[status] : null;
|
|
125
|
-
const statusLabel =
|
|
126
|
-
statusConfig != null ? t(statusConfig.i18nKey) : '';
|
|
125
|
+
const statusLabel = statusConfig != null ? t(statusConfig.i18nKey) : '';
|
|
127
126
|
|
|
128
127
|
const hasContent =
|
|
129
128
|
timestamp != null || footer != null || statusConfig != null;
|
|
@@ -96,7 +96,11 @@ export function ChatSendButton(props: ChatSendButtonProps): ReactNode {
|
|
|
96
96
|
return (
|
|
97
97
|
<Button
|
|
98
98
|
ref={ref}
|
|
99
|
-
label={
|
|
99
|
+
label={
|
|
100
|
+
isStopShown
|
|
101
|
+
? t('@astryx.chatSendButton.stop')
|
|
102
|
+
: t('@astryx.chatSendButton.send')
|
|
103
|
+
}
|
|
100
104
|
variant={isStopShown ? 'secondary' : 'primary'}
|
|
101
105
|
size={size}
|
|
102
106
|
icon={
|
|
@@ -699,7 +699,9 @@ export function useTriggerMenu(
|
|
|
699
699
|
<div
|
|
700
700
|
id={listboxId}
|
|
701
701
|
role="listbox"
|
|
702
|
-
aria-label={
|
|
702
|
+
aria-label={
|
|
703
|
+
trigger?.menuLabel ?? t('@astryx.chatTriggerMenu.suggestions')
|
|
704
|
+
}
|
|
703
705
|
{...mergeProps(
|
|
704
706
|
themeProps('trigger-menu'),
|
|
705
707
|
stylex.props(styles.dropdown),
|
|
@@ -113,7 +113,7 @@ export const docs = {
|
|
|
113
113
|
{
|
|
114
114
|
name: 'labelIcon',
|
|
115
115
|
type: 'IconType',
|
|
116
|
-
description: 'Icon to display before the label text. See `
|
|
116
|
+
description: 'Icon to display before the label text. See `astryx docs icons` for valid semantic names.',
|
|
117
117
|
},
|
|
118
118
|
{
|
|
119
119
|
name: 'status',
|
|
@@ -20,6 +20,7 @@ const probe = stylex.create({
|
|
|
20
20
|
secondaryText: {color: colorVars['--color-text-secondary']},
|
|
21
21
|
accentText: {color: colorVars['--color-text-accent']},
|
|
22
22
|
badgeBackground: {backgroundColor: colorVars['--color-accent-muted']},
|
|
23
|
+
pointerCursor: {cursor: 'pointer'},
|
|
23
24
|
});
|
|
24
25
|
|
|
25
26
|
function atomicClasses(style: (typeof probe)[keyof typeof probe]): string[] {
|
|
@@ -118,4 +119,54 @@ describe('Citation', () => {
|
|
|
118
119
|
expect(el.classList.contains(cls)).toBe(true);
|
|
119
120
|
}
|
|
120
121
|
});
|
|
122
|
+
|
|
123
|
+
// The interactive (pointer) treatment is keyed on `source.url`: a citation
|
|
124
|
+
// with no url is non-interactive and must keep the default cursor.
|
|
125
|
+
const noUrlSource = {title: 'No link'};
|
|
126
|
+
|
|
127
|
+
it('does not use the pointer cursor without a url in the label variant', () => {
|
|
128
|
+
render(<Citation source={noUrlSource} number={1} data-testid="citation" />);
|
|
129
|
+
const el = screen.getByTestId('citation');
|
|
130
|
+
for (const cls of atomicClasses(probe.pointerCursor)) {
|
|
131
|
+
expect(el.classList.contains(cls)).toBe(false);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('uses the pointer cursor with a url in the label variant', () => {
|
|
136
|
+
render(<Citation source={source} number={1} data-testid="citation" />);
|
|
137
|
+
const el = screen.getByTestId('citation');
|
|
138
|
+
for (const cls of atomicClasses(probe.pointerCursor)) {
|
|
139
|
+
expect(el.classList.contains(cls)).toBe(true);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('does not use the pointer cursor without a url in the number variant', () => {
|
|
144
|
+
render(
|
|
145
|
+
<Citation
|
|
146
|
+
source={noUrlSource}
|
|
147
|
+
number={1}
|
|
148
|
+
variant="number"
|
|
149
|
+
data-testid="citation"
|
|
150
|
+
/>,
|
|
151
|
+
);
|
|
152
|
+
const el = screen.getByTestId('citation');
|
|
153
|
+
for (const cls of atomicClasses(probe.pointerCursor)) {
|
|
154
|
+
expect(el.classList.contains(cls)).toBe(false);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('uses the pointer cursor with a url in the number variant', () => {
|
|
159
|
+
render(
|
|
160
|
+
<Citation
|
|
161
|
+
source={source}
|
|
162
|
+
number={1}
|
|
163
|
+
variant="number"
|
|
164
|
+
data-testid="citation"
|
|
165
|
+
/>,
|
|
166
|
+
);
|
|
167
|
+
const el = screen.getByTestId('citation');
|
|
168
|
+
for (const cls of atomicClasses(probe.pointerCursor)) {
|
|
169
|
+
expect(el.classList.contains(cls)).toBe(true);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
121
172
|
});
|
|
@@ -61,7 +61,6 @@ const styles = stylex.create({
|
|
|
61
61
|
paddingInline: spacingVars['--spacing-2'],
|
|
62
62
|
marginInlineStart: spacingVars['--spacing-0-5'],
|
|
63
63
|
textDecoration: 'none',
|
|
64
|
-
cursor: 'pointer',
|
|
65
64
|
transitionProperty: 'background-color, border-color, color',
|
|
66
65
|
transitionDuration: durationVars['--duration-fast-max'],
|
|
67
66
|
transitionTimingFunction: easeVars['--ease-standard'],
|
|
@@ -77,6 +76,9 @@ const styles = stylex.create({
|
|
|
77
76
|
whiteSpace: 'nowrap',
|
|
78
77
|
minWidth: 0,
|
|
79
78
|
},
|
|
79
|
+
labelInteractive: {
|
|
80
|
+
cursor: 'pointer',
|
|
81
|
+
},
|
|
80
82
|
labelHover: {
|
|
81
83
|
backgroundColor: {
|
|
82
84
|
':hover': {
|
|
@@ -108,11 +110,13 @@ const styles = stylex.create({
|
|
|
108
110
|
height: spacingVars['--spacing-5'],
|
|
109
111
|
paddingInline: spacingVars['--spacing-1'],
|
|
110
112
|
textDecoration: 'none',
|
|
111
|
-
cursor: 'pointer',
|
|
112
113
|
transitionProperty: 'background-color',
|
|
113
114
|
transitionDuration: durationVars['--duration-fast-max'],
|
|
114
115
|
transitionTimingFunction: easeVars['--ease-standard'],
|
|
115
116
|
},
|
|
117
|
+
numberInteractive: {
|
|
118
|
+
cursor: 'pointer',
|
|
119
|
+
},
|
|
116
120
|
numberHover: {
|
|
117
121
|
backgroundColor: {
|
|
118
122
|
// Explicit default for the same reason as labelHover's color: a
|
|
@@ -191,6 +195,7 @@ export function Citation({
|
|
|
191
195
|
stylex.props(
|
|
192
196
|
styles.number,
|
|
193
197
|
href != null && styles.numberHover,
|
|
198
|
+
href != null && styles.numberInteractive,
|
|
194
199
|
xstyle,
|
|
195
200
|
),
|
|
196
201
|
className,
|
|
@@ -215,6 +220,7 @@ export function Citation({
|
|
|
215
220
|
styles.label,
|
|
216
221
|
icon != null && styles.labelWithIcon,
|
|
217
222
|
href != null && styles.labelHover,
|
|
223
|
+
href != null && styles.labelInteractive,
|
|
218
224
|
xstyle,
|
|
219
225
|
),
|
|
220
226
|
className,
|
|
@@ -807,7 +807,9 @@ export function CodeBlock({
|
|
|
807
807
|
e.stopPropagation();
|
|
808
808
|
void handleCopy();
|
|
809
809
|
}}
|
|
810
|
-
aria-label={
|
|
810
|
+
aria-label={
|
|
811
|
+
copied ? t('@astryx.codeBlock.copied') : t('@astryx.codeBlock.copyCode')
|
|
812
|
+
}
|
|
811
813
|
{...stylex.props(
|
|
812
814
|
styles.copyButton,
|
|
813
815
|
!showHeader && styles.copyButtonAbsolute,
|
|
@@ -17,6 +17,7 @@ export const docs = {
|
|
|
17
17
|
theming: {
|
|
18
18
|
targets: [
|
|
19
19
|
{className: 'astryx-collapsible', visualProps: ['density']},
|
|
20
|
+
{className: 'astryx-collapsible-content', visualProps: ['density']},
|
|
20
21
|
{className: 'astryx-collapsible-group', visualProps: ['density']},
|
|
21
22
|
],
|
|
22
23
|
},
|
|
@@ -53,6 +54,12 @@ export const docs = {
|
|
|
53
54
|
type: 'boolean',
|
|
54
55
|
description: 'Controlled open state.',
|
|
55
56
|
},
|
|
57
|
+
{
|
|
58
|
+
name: 'isDisabled',
|
|
59
|
+
type: 'boolean',
|
|
60
|
+
description: "Disable the item so its trigger can't be toggled (dimmed, aria-disabled, and out of the tab order). Doesn't collapse an already-open item.",
|
|
61
|
+
default: 'false',
|
|
62
|
+
},
|
|
56
63
|
{
|
|
57
64
|
name: 'onOpenChange',
|
|
58
65
|
type: '(isOpen: boolean) => void',
|
|
@@ -72,6 +72,12 @@ describe('Collapsible', () => {
|
|
|
72
72
|
expect(screen.getByTestId('root')).toHaveClass('astryx-collapsible');
|
|
73
73
|
});
|
|
74
74
|
|
|
75
|
+
it('renders the stable astryx-collapsible-content class on the content area', () => {
|
|
76
|
+
render(<Collapsible trigger="T">c</Collapsible>);
|
|
77
|
+
const content = contentFor(screen.getByRole('button'));
|
|
78
|
+
expect(content).toHaveClass('astryx-collapsible-content');
|
|
79
|
+
});
|
|
80
|
+
|
|
75
81
|
it('renders a ReactNode trigger, not just a string', () => {
|
|
76
82
|
render(
|
|
77
83
|
<Collapsible trigger={<span data-testid="rich">Rich</span>}>
|
|
@@ -240,6 +246,80 @@ describe('Collapsible', () => {
|
|
|
240
246
|
});
|
|
241
247
|
});
|
|
242
248
|
|
|
249
|
+
describe('disabled state', () => {
|
|
250
|
+
it('marks the trigger aria-disabled and drops it from the tab order', () => {
|
|
251
|
+
render(
|
|
252
|
+
<Collapsible trigger="T" isDisabled>
|
|
253
|
+
Body
|
|
254
|
+
</Collapsible>,
|
|
255
|
+
);
|
|
256
|
+
const button = screen.getByRole('button');
|
|
257
|
+
expect(button).toHaveAttribute('aria-disabled', 'true');
|
|
258
|
+
expect(button).toHaveAttribute('tabindex', '-1');
|
|
259
|
+
// Never the native disabled attribute — it stays focusable/perceivable.
|
|
260
|
+
expect(button).not.toBeDisabled();
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('is enabled by default (no aria-disabled, stays in tab order)', () => {
|
|
264
|
+
render(<Collapsible trigger="T">Body</Collapsible>);
|
|
265
|
+
const button = screen.getByRole('button');
|
|
266
|
+
expect(button).not.toHaveAttribute('aria-disabled');
|
|
267
|
+
expect(button).not.toHaveAttribute('tabindex', '-1');
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('does not toggle when the trigger is clicked while disabled', async () => {
|
|
271
|
+
const user = userEvent.setup();
|
|
272
|
+
render(
|
|
273
|
+
<Collapsible trigger="T" isDisabled defaultIsOpen>
|
|
274
|
+
Body
|
|
275
|
+
</Collapsible>,
|
|
276
|
+
);
|
|
277
|
+
const button = screen.getByRole('button');
|
|
278
|
+
expect(button).toHaveAttribute('aria-expanded', 'true');
|
|
279
|
+
await user.click(button);
|
|
280
|
+
expect(button).toHaveAttribute('aria-expanded', 'true');
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('does not fire onOpenChange while disabled', async () => {
|
|
284
|
+
const user = userEvent.setup();
|
|
285
|
+
const onOpenChange = vi.fn();
|
|
286
|
+
render(
|
|
287
|
+
<Collapsible trigger="T" isDisabled onOpenChange={onOpenChange}>
|
|
288
|
+
Body
|
|
289
|
+
</Collapsible>,
|
|
290
|
+
);
|
|
291
|
+
await user.click(screen.getByRole('button'));
|
|
292
|
+
expect(onOpenChange).not.toHaveBeenCalled();
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('does not collapse an already-open item — content stays visible', () => {
|
|
296
|
+
render(
|
|
297
|
+
<Collapsible trigger="T" isDisabled defaultIsOpen>
|
|
298
|
+
Body
|
|
299
|
+
</Collapsible>,
|
|
300
|
+
);
|
|
301
|
+
const content = contentFor(screen.getByRole('button'));
|
|
302
|
+
expect(content).not.toHaveStyle({display: 'none'});
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it('does not toggle its group item when disabled', async () => {
|
|
306
|
+
const user = userEvent.setup();
|
|
307
|
+
render(
|
|
308
|
+
<CollapsibleGroup type="single">
|
|
309
|
+
<Collapsible trigger="A" value="a" isDisabled>
|
|
310
|
+
Body A
|
|
311
|
+
</Collapsible>
|
|
312
|
+
<Collapsible trigger="B" value="b">
|
|
313
|
+
Body B
|
|
314
|
+
</Collapsible>
|
|
315
|
+
</CollapsibleGroup>,
|
|
316
|
+
);
|
|
317
|
+
const triggerA = screen.getByRole('button', {name: /A/});
|
|
318
|
+
await user.click(triggerA);
|
|
319
|
+
expect(triggerA).toHaveAttribute('aria-expanded', 'false');
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
|
|
243
323
|
describe('prop forwarding', () => {
|
|
244
324
|
it('forwards a ref to the root element', () => {
|
|
245
325
|
const ref = vi.fn();
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
durationVars,
|
|
40
40
|
easeVars,
|
|
41
41
|
} from '../theme/tokens.stylex';
|
|
42
|
+
|
|
42
43
|
import {useCollapsible} from './useCollapsible';
|
|
43
44
|
import {CollapsibleGroupPresentationContext} from './CollapsibleGroupContext';
|
|
44
45
|
import {getIcon} from '../Icon/globalIconRegistry';
|
|
@@ -81,6 +82,13 @@ const styles = stylex.create({
|
|
|
81
82
|
textBoxEdge: 'cap alphabetic',
|
|
82
83
|
textBoxTrim: 'trim-both',
|
|
83
84
|
},
|
|
85
|
+
// Disabled trigger — non-interactive, dimmed. Native `disabled` on the
|
|
86
|
+
// button blocks click + keyboard activation; these styles restore the
|
|
87
|
+
// visual affordance that `all: unset` wipes.
|
|
88
|
+
triggerDisabled: {
|
|
89
|
+
cursor: 'not-allowed',
|
|
90
|
+
opacity: 0.5,
|
|
91
|
+
},
|
|
84
92
|
// Chevron indicator
|
|
85
93
|
chevron: {
|
|
86
94
|
display: 'inline-flex',
|
|
@@ -102,8 +110,17 @@ const styles = stylex.create({
|
|
|
102
110
|
contentHidden: {
|
|
103
111
|
display: 'none',
|
|
104
112
|
},
|
|
113
|
+
// Anchors body typography so revealed text renders at the system's body
|
|
114
|
+
// scale (family/size/weight/leading) instead of inheriting from wherever
|
|
115
|
+
// the Collapsible is placed. External themes override via the
|
|
116
|
+
// `astryx-collapsible-content` target.
|
|
105
117
|
content: {
|
|
106
118
|
paddingBlockStart: spacingVars['--spacing-1'],
|
|
119
|
+
fontFamily: typographyVars['--font-family-body'],
|
|
120
|
+
fontSize: typeScaleVars['--text-body-size'],
|
|
121
|
+
fontWeight: typeScaleVars['--text-body-weight'],
|
|
122
|
+
lineHeight: typeScaleVars['--text-body-leading'],
|
|
123
|
+
color: colorVars['--color-text-primary'],
|
|
107
124
|
},
|
|
108
125
|
// Group divider chrome — a hairline above every item except the first.
|
|
109
126
|
// The group's wrapper (or 'all' mode) owns the outer edges.
|
|
@@ -166,6 +183,17 @@ export interface CollapsibleProps extends BaseProps {
|
|
|
166
183
|
*/
|
|
167
184
|
isOpen?: boolean;
|
|
168
185
|
|
|
186
|
+
/**
|
|
187
|
+
* Whether the collapsible is disabled. A disabled item can't be toggled —
|
|
188
|
+
* its trigger is non-interactive and dimmed. Following the system-wide
|
|
189
|
+
* disabled convention, the trigger uses `aria-disabled` (not the native
|
|
190
|
+
* `disabled` attribute) and drops out of the tab order, staying perceivable
|
|
191
|
+
* to assistive tech. The content stays in whatever open state it was;
|
|
192
|
+
* disabling doesn't collapse an already-open item.
|
|
193
|
+
* @default false
|
|
194
|
+
*/
|
|
195
|
+
isDisabled?: boolean;
|
|
196
|
+
|
|
169
197
|
/**
|
|
170
198
|
* Callback when the open state changes.
|
|
171
199
|
*/
|
|
@@ -225,6 +253,7 @@ export function Collapsible({
|
|
|
225
253
|
children,
|
|
226
254
|
defaultIsOpen,
|
|
227
255
|
isOpen: controlledIsOpen,
|
|
256
|
+
isDisabled = false,
|
|
228
257
|
onOpenChange,
|
|
229
258
|
value,
|
|
230
259
|
ref,
|
|
@@ -244,6 +273,17 @@ export function Collapsible({
|
|
|
244
273
|
value,
|
|
245
274
|
});
|
|
246
275
|
|
|
276
|
+
// Activation is blocked by this guard rather than the native `disabled`
|
|
277
|
+
// attribute, so the trigger keeps `aria-disabled` semantics and stays
|
|
278
|
+
// discoverable. A native `disabled` button would silently swallow events
|
|
279
|
+
// (e.g. a wrapping tooltip's hover) — the system-wide disabled convention.
|
|
280
|
+
const handleToggle = () => {
|
|
281
|
+
if (isDisabled) {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
toggle();
|
|
285
|
+
};
|
|
286
|
+
|
|
247
287
|
const presentation = use(CollapsibleGroupPresentationContext);
|
|
248
288
|
const isDivided = presentation?.hasDividers ?? false;
|
|
249
289
|
const density = presentation?.density ?? null;
|
|
@@ -268,12 +308,20 @@ export function Collapsible({
|
|
|
268
308
|
{...props}>
|
|
269
309
|
<button
|
|
270
310
|
type="button"
|
|
271
|
-
onClick={
|
|
311
|
+
onClick={handleToggle}
|
|
312
|
+
aria-disabled={isDisabled || undefined}
|
|
272
313
|
aria-expanded={isOpen}
|
|
273
314
|
aria-controls={contentId}
|
|
315
|
+
// A disabled trigger drops out of the tab order so it isn't a silently
|
|
316
|
+
// dead tab stop; activation stays blocked by the handleToggle guard,
|
|
317
|
+
// and aria-disabled keeps the state perceivable to assistive tech —
|
|
318
|
+
// the system-wide disabled convention (never native `disabled`, which
|
|
319
|
+
// would swallow events like a wrapping tooltip's hover).
|
|
320
|
+
tabIndex={isDisabled ? -1 : undefined}
|
|
274
321
|
{...stylex.props(
|
|
275
322
|
styles.trigger,
|
|
276
323
|
density != null && triggerDensity[density],
|
|
324
|
+
isDisabled && styles.triggerDisabled,
|
|
277
325
|
)}>
|
|
278
326
|
<span {...stylex.props(styles.triggerLabel)}>{trigger}</span>
|
|
279
327
|
<span
|
|
@@ -286,10 +334,15 @@ export function Collapsible({
|
|
|
286
334
|
</button>
|
|
287
335
|
<div
|
|
288
336
|
id={contentId}
|
|
289
|
-
{...
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
337
|
+
{...mergeProps(
|
|
338
|
+
themeProps('collapsible-content', {
|
|
339
|
+
density: density ?? undefined,
|
|
340
|
+
}),
|
|
341
|
+
stylex.props(
|
|
342
|
+
styles.content,
|
|
343
|
+
density != null && contentDensity[density],
|
|
344
|
+
!isOpen && styles.contentHidden,
|
|
345
|
+
),
|
|
293
346
|
)}>
|
|
294
347
|
{presentation != null ? (
|
|
295
348
|
<CollapsibleGroupPresentationContext value={null}>
|