@astryxdesign/core 0.6.0 → 0.6.1
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 +41 -3
- package/dist/AppShell/AppShell.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetSwitcher.d.ts +12 -1
- package/dist/BottomSheet/BottomSheetSwitcher.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetSwitcher.js +44 -15
- package/dist/Breadcrumbs/BreadcrumbItem.d.ts +3 -2
- package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
- package/dist/Breadcrumbs/BreadcrumbItem.js +3 -7
- package/dist/Center/Center.d.ts +23 -16
- package/dist/Center/Center.d.ts.map +1 -1
- package/dist/Center/Center.js +7 -5
- package/dist/CodeBlock/CodeBlock.js +2 -2
- package/dist/DateInput/DateInput.d.ts.map +1 -1
- package/dist/DateInput/DateInput.js +12 -2
- package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
- package/dist/DateTimeInput/DateTimeInput.js +12 -2
- package/dist/Field/Field.d.ts.map +1 -1
- package/dist/Field/Field.js +1 -0
- package/dist/Field/InputClearButton.d.ts +2 -2
- package/dist/Field/InputClearButton.d.ts.map +1 -1
- package/dist/Field/InputClearButton.js +5 -1
- package/dist/Field/PanelSearchInput.d.ts.map +1 -1
- package/dist/Field/PanelSearchInput.js +16 -4
- package/dist/FileInput/FileInput.d.ts.map +1 -1
- package/dist/FileInput/FileInput.js +12 -1
- package/dist/HoverCard/useHoverCard.js +2 -2
- package/dist/Indicator/CheckboxIndicator.js +2 -2
- package/dist/Indicator/RadioIndicator.js +2 -2
- package/dist/Layer/layerStack.d.ts +10 -0
- package/dist/Layer/layerStack.d.ts.map +1 -1
- package/dist/Layer/layerStack.js +21 -9
- package/dist/Layer/useLayerDismissal.d.ts +2 -3
- package/dist/Layer/useLayerDismissal.d.ts.map +1 -1
- package/dist/Layer/useLayerDismissal.js +2 -3
- package/dist/NavIcon/NavIcon.js +2 -2
- package/dist/NumberInput/NumberInput.d.ts.map +1 -1
- package/dist/NumberInput/NumberInput.js +12 -2
- package/dist/Popover/usePopover.d.ts +3 -2
- package/dist/Popover/usePopover.d.ts.map +1 -1
- package/dist/Popover/usePopover.js +4 -2
- package/dist/ProgressBar/ProgressBar.js +2 -2
- package/dist/ScrollableArea/ScrollableArea.d.ts +79 -0
- package/dist/ScrollableArea/ScrollableArea.d.ts.map +1 -0
- package/dist/ScrollableArea/ScrollableArea.js +144 -0
- package/dist/ScrollableArea/index.d.ts +11 -0
- package/dist/ScrollableArea/index.d.ts.map +1 -0
- package/dist/ScrollableArea/index.js +11 -0
- package/dist/StatusDot/StatusDot.js +2 -2
- package/dist/TextArea/TextArea.js +2 -2
- package/dist/TextInput/TextInput.d.ts.map +1 -1
- package/dist/TextInput/TextInput.js +19 -4
- package/dist/TimeInput/TimeInput.d.ts.map +1 -1
- package/dist/TimeInput/TimeInput.js +12 -2
- package/dist/Typeahead/BaseTypeahead.d.ts +21 -14
- package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
- package/dist/Typeahead/BaseTypeahead.js +56 -20
- package/dist/astryx.css +15 -0
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/scrollGeometry.d.ts +24 -0
- package/dist/hooks/scrollGeometry.d.ts.map +1 -0
- package/dist/hooks/scrollGeometry.js +86 -0
- package/dist/hooks/scrollOwnerRegistry.d.ts +15 -0
- package/dist/hooks/scrollOwnerRegistry.d.ts.map +1 -0
- package/dist/hooks/scrollOwnerRegistry.js +24 -0
- package/dist/hooks/useFocusTrap.d.ts +8 -0
- package/dist/hooks/useFocusTrap.d.ts.map +1 -1
- package/dist/hooks/useFocusTrap.js +22 -11
- package/dist/hooks/useScrollableArea.d.ts +51 -0
- package/dist/hooks/useScrollableArea.d.ts.map +1 -0
- package/dist/hooks/useScrollableArea.js +287 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/theme/defineTheme.d.ts +2 -6
- package/dist/theme/defineTheme.d.ts.map +1 -1
- package/dist/theme/defineTheme.js +1 -1
- package/dist/theme/derivedVarRegistry.js +1 -1
- package/dist/theme/localTokens.d.ts +8 -11
- package/dist/theme/localTokens.d.ts.map +1 -1
- package/dist/theme/localTokens.js +17 -71
- package/dist/theme/themeAdaptations.d.ts.map +1 -1
- package/dist/theme/themeAdaptations.js +4 -4
- package/dist/utils/themeProps.d.ts +10 -10
- package/dist/utils/themeProps.d.ts.map +1 -1
- package/dist/utils/themeProps.js +27 -10
- package/locales/en.json +16 -0
- package/locales/pseudo.json +12 -0
- package/package.json +7 -2
- package/src/AppShell/AppShell.test.tsx +36 -0
- package/src/AppShell/AppShell.tsx +4 -1
- package/src/AspectRatio/AspectRatio.doc.mjs +3 -3
- package/src/Banner/Banner.test.tsx +3 -1
- package/src/BottomSheet/BottomSheetSwitcher.doc.mjs +56 -1
- package/src/BottomSheet/BottomSheetSwitcher.spec.md +211 -0
- package/src/BottomSheet/BottomSheetSwitcher.test.tsx +134 -2
- package/src/BottomSheet/BottomSheetSwitcher.tsx +43 -20
- package/src/Breadcrumbs/BreadcrumbItem.doc.mjs +10 -5
- package/src/Breadcrumbs/BreadcrumbItem.spec.md +225 -0
- package/src/Breadcrumbs/BreadcrumbItem.tsx +8 -13
- package/src/Breadcrumbs/Breadcrumbs.doc.mjs +2 -2
- package/src/Breadcrumbs/Breadcrumbs.test.tsx +49 -2
- package/src/Center/Center.doc.mjs +32 -28
- package/src/Center/Center.spec.md +225 -0
- package/src/Center/Center.test.tsx +42 -4
- package/src/Center/Center.tsx +24 -17
- package/src/Chat/ChatSystemMessage.test.tsx +2 -9
- package/src/CodeBlock/CodeBlock.doc.mjs +2 -2
- package/src/CodeBlock/CodeBlock.tsx +2 -2
- package/src/DateInput/DateInput.test.tsx +4 -4
- package/src/DateInput/DateInput.tsx +15 -4
- package/src/DateRangeInput/DateRangeInput.test.tsx +2 -2
- package/src/DateTimeInput/DateTimeInput.test.tsx +6 -4
- package/src/DateTimeInput/DateTimeInput.tsx +18 -7
- package/src/DropdownMenu/DropdownMenuSelectable.test.tsx +4 -77
- package/src/Field/Field.test.tsx +42 -0
- package/src/Field/Field.tsx +6 -0
- package/src/Field/InputClearButton.test.tsx +35 -1
- package/src/Field/InputClearButton.tsx +7 -3
- package/src/Field/PanelSearchInput.tsx +21 -8
- package/src/FieldStatus/FieldStatus.spec.md +27 -17
- package/src/FieldStatus/FieldStatus.test.tsx +7 -5
- package/src/FieldStatus/__tests__/StatusMessage.a11y.chromium.spec.ts +198 -0
- package/src/FieldStatus/__tests__/StatusMessage.a11y.known-failures.ts +13 -0
- package/src/FieldStatus/__tests__/StatusMessage.a11y.renders.tsx +305 -0
- package/src/FieldStatus/__tests__/StatusMessage.a11y.states.ts +317 -0
- package/src/FieldStatus/__tests__/StatusMessage.a11y.test.tsx +155 -0
- package/src/FileInput/FileInput.tsx +10 -1
- package/src/FormLayout/__snapshots__/FormLayout.test.tsx.snap +3 -3
- package/src/HoverCard/HoverCard.doc.mjs +4 -4
- package/src/HoverCard/useHoverCard.tsx +2 -2
- package/src/Indicator/CheckboxIndicator.tsx +2 -2
- package/src/Indicator/Indicator.doc.mjs +2 -2
- package/src/Indicator/Indicator.test.tsx +1 -1
- package/src/Indicator/RadioIndicator.tsx +2 -2
- package/src/Layer/layerStack.ts +20 -9
- package/src/Layer/useLayerDismissal.ts +2 -3
- package/src/MultiSelector/MultiSelector.test.tsx +4 -4
- package/src/NavIcon/NavIcon.doc.mjs +4 -4
- package/src/NavIcon/NavIcon.tsx +2 -2
- package/src/NumberInput/NumberInput.tsx +18 -7
- package/src/Popover/Popover.doc.mjs +10 -10
- package/src/Popover/Popover.spec.md +55 -65
- package/src/Popover/Popover.test.tsx +29 -0
- package/src/Popover/usePopover.doc.mjs +4 -4
- package/src/Popover/usePopover.tsx +7 -4
- package/src/ProgressBar/ProgressBar.doc.mjs +4 -4
- package/src/ProgressBar/ProgressBar.test.tsx +1 -31
- package/src/ProgressBar/ProgressBar.tsx +2 -2
- package/src/RadioList/RadioList.test.tsx +5 -144
- package/src/RadioList/__tests__/RadioGroup.a11y.chromium.spec.ts +255 -0
- package/src/RadioList/__tests__/RadioGroup.a11y.known-failures.ts +12 -0
- package/src/RadioList/__tests__/RadioGroup.a11y.renders.tsx +232 -0
- package/src/RadioList/__tests__/RadioGroup.a11y.states.ts +503 -0
- package/src/RadioList/__tests__/RadioGroup.a11y.test.tsx +217 -0
- package/src/ScrollableArea/ScrollableArea.doc.mjs +100 -0
- package/src/ScrollableArea/ScrollableArea.spec.md +189 -0
- package/src/ScrollableArea/ScrollableArea.test.tsx +299 -0
- package/src/ScrollableArea/ScrollableArea.tsx +259 -0
- package/src/ScrollableArea/index.ts +26 -0
- package/src/ScrollableArea/modules/useScrollableArea.spec.md +121 -0
- package/src/SegmentedControl/SegmentedControl.test.tsx +5 -172
- package/src/Selector/Selector.test.tsx +4 -4
- package/src/Spinner/Spinner.test.tsx +0 -18
- package/src/StatusDot/StatusDot.doc.mjs +4 -4
- package/src/StatusDot/StatusDot.tsx +2 -2
- package/src/TabList/TabList.test.tsx +5 -9
- package/src/TabList/__tests__/Tabs.a11y.chromium.spec.ts +191 -0
- package/src/TabList/__tests__/Tabs.a11y.known-failures.ts +45 -0
- package/src/TabList/__tests__/Tabs.a11y.renders.tsx +92 -0
- package/src/TabList/__tests__/Tabs.a11y.states.ts +247 -0
- package/src/TabList/__tests__/Tabs.a11y.test.tsx +153 -0
- package/src/Table/Table.doc.mjs +2 -2
- package/src/TextArea/TextArea.doc.mjs +4 -4
- package/src/TextArea/TextArea.tsx +2 -2
- package/src/TextInput/TextInput.doc.mjs +2 -1
- package/src/TextInput/TextInput.test.tsx +94 -0
- package/src/TextInput/TextInput.tsx +22 -6
- package/src/TimeInput/TimeInput.tsx +18 -7
- package/src/Toast/ToastViewport.test.tsx +1 -39
- package/src/Typeahead/BaseTypeahead.doc.mjs +229 -33
- package/src/Typeahead/BaseTypeahead.spec.md +269 -0
- package/src/Typeahead/BaseTypeahead.test.tsx +200 -0
- package/src/Typeahead/BaseTypeahead.tsx +99 -30
- package/src/hooks/index.ts +13 -0
- package/src/hooks/scrollGeometry.ts +155 -0
- package/src/hooks/scrollOwnerRegistry.ts +47 -0
- package/src/hooks/useFocusTrap.ts +22 -11
- package/src/hooks/useFocusTrapEscapeShim.test.tsx +4 -3
- package/src/hooks/useScrollableArea.doc.mjs +108 -0
- package/src/hooks/useScrollableArea.test.tsx +437 -0
- package/src/hooks/useScrollableArea.ts +469 -0
- package/src/index.ts +1 -0
- package/src/theme/defineTheme.test.ts +65 -105
- package/src/theme/defineTheme.ts +3 -9
- package/src/theme/derivedVarRegistry.ts +1 -1
- package/src/theme/localTokens.ts +25 -96
- package/src/theme/publicThemeHelperContract.test.ts +2 -2
- package/src/theme/themeAdaptations.test.ts +16 -42
- package/src/theme/themeAdaptations.ts +6 -9
- package/src/utils/themeProps.test.ts +29 -10
- package/src/utils/themeProps.ts +36 -17
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
import {readFileSync} from 'node:fs';
|
|
4
|
+
import {act, render, screen} from '@testing-library/react';
|
|
5
|
+
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
|
|
6
|
+
import {ScrollableArea} from './ScrollableArea';
|
|
7
|
+
|
|
8
|
+
function setGeometry(
|
|
9
|
+
element: HTMLElement,
|
|
10
|
+
values: Partial<
|
|
11
|
+
Pick<
|
|
12
|
+
HTMLElement,
|
|
13
|
+
'clientWidth' | 'clientHeight' | 'scrollWidth' | 'scrollHeight'
|
|
14
|
+
>
|
|
15
|
+
>,
|
|
16
|
+
) {
|
|
17
|
+
for (const [key, value] of Object.entries(values)) {
|
|
18
|
+
Object.defineProperty(element, key, {
|
|
19
|
+
configurable: true,
|
|
20
|
+
value,
|
|
21
|
+
writable: true,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('ScrollableArea', () => {
|
|
27
|
+
let frames: FrameRequestCallback[];
|
|
28
|
+
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
frames = [];
|
|
31
|
+
vi.stubGlobal(
|
|
32
|
+
'requestAnimationFrame',
|
|
33
|
+
vi.fn((callback: FrameRequestCallback) => {
|
|
34
|
+
frames.push(callback);
|
|
35
|
+
return frames.length;
|
|
36
|
+
}),
|
|
37
|
+
);
|
|
38
|
+
vi.stubGlobal('cancelAnimationFrame', vi.fn());
|
|
39
|
+
vi.stubGlobal(
|
|
40
|
+
'ResizeObserver',
|
|
41
|
+
vi.fn(function () {
|
|
42
|
+
return {observe: vi.fn(), unobserve: vi.fn(), disconnect: vi.fn()};
|
|
43
|
+
}),
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
afterEach(() => vi.unstubAllGlobals());
|
|
48
|
+
|
|
49
|
+
function flushFrame() {
|
|
50
|
+
void act(() => {
|
|
51
|
+
const pending = frames.splice(0);
|
|
52
|
+
pending.forEach(callback => callback(performance.now()));
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
it('renders one native viewport and one real content box', () => {
|
|
57
|
+
render(
|
|
58
|
+
<ScrollableArea label="Messages" data-testid="viewport">
|
|
59
|
+
<p>Message history</p>
|
|
60
|
+
</ScrollableArea>,
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const viewport = screen.getByTestId('viewport');
|
|
64
|
+
expect(viewport).toHaveAccessibleName('Messages');
|
|
65
|
+
expect(viewport).toHaveAttribute('role', 'group');
|
|
66
|
+
expect(viewport).toHaveAttribute('data-scroll-axis', 'block');
|
|
67
|
+
expect(viewport).not.toHaveAttribute('tabindex');
|
|
68
|
+
expect(viewport.children).toHaveLength(1);
|
|
69
|
+
expect(viewport.firstElementChild).toHaveAttribute('data-scroll-content');
|
|
70
|
+
expect(viewport).toHaveTextContent('Message history');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('becomes keyboard reachable only when the requested axis is effective', () => {
|
|
74
|
+
render(
|
|
75
|
+
<ScrollableArea
|
|
76
|
+
axis="block"
|
|
77
|
+
label="Messages"
|
|
78
|
+
data-testid="viewport"
|
|
79
|
+
style={{height: 100}}>
|
|
80
|
+
<p>Message history</p>
|
|
81
|
+
</ScrollableArea>,
|
|
82
|
+
);
|
|
83
|
+
const viewport = screen.getByTestId('viewport');
|
|
84
|
+
// jsdom does not resolve StyleX's dynamic CSS variables. Mirror the
|
|
85
|
+
// generated class values so this integration test can exercise measurement.
|
|
86
|
+
viewport.style.overflowX = 'hidden';
|
|
87
|
+
viewport.style.overflowY = 'auto';
|
|
88
|
+
setGeometry(viewport, {
|
|
89
|
+
clientWidth: 100,
|
|
90
|
+
clientHeight: 100,
|
|
91
|
+
scrollWidth: 100,
|
|
92
|
+
scrollHeight: 180,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
void act(() => viewport.dispatchEvent(new Event('scroll')));
|
|
96
|
+
flushFrame();
|
|
97
|
+
expect(viewport).toHaveAttribute('tabindex', '0');
|
|
98
|
+
expect(viewport).toHaveAttribute('data-scrollable-block', 'true');
|
|
99
|
+
|
|
100
|
+
setGeometry(viewport, {scrollHeight: 100});
|
|
101
|
+
viewport.focus();
|
|
102
|
+
void act(() => viewport.dispatchEvent(new Event('scroll')));
|
|
103
|
+
flushFrame();
|
|
104
|
+
expect(viewport).toHaveAttribute('tabindex', '-1');
|
|
105
|
+
expect(document.activeElement).toBe(viewport);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('keeps viewport props, styles, class names, events, and refs', () => {
|
|
109
|
+
const onClick = vi.fn();
|
|
110
|
+
const ref = vi.fn();
|
|
111
|
+
render(
|
|
112
|
+
<ScrollableArea
|
|
113
|
+
label="Results"
|
|
114
|
+
role="region"
|
|
115
|
+
ref={ref}
|
|
116
|
+
onClick={onClick}
|
|
117
|
+
className="consumer-class"
|
|
118
|
+
style={{maxHeight: 200}}
|
|
119
|
+
data-testid="viewport">
|
|
120
|
+
Results
|
|
121
|
+
</ScrollableArea>,
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const viewport = screen.getByTestId('viewport');
|
|
125
|
+
viewport.click();
|
|
126
|
+
expect(onClick).toHaveBeenCalledTimes(1);
|
|
127
|
+
expect(ref).toHaveBeenCalledWith(viewport);
|
|
128
|
+
expect(viewport).toHaveClass('astryx-scrollable-area', 'consumer-class');
|
|
129
|
+
expect(viewport.style.maxHeight).toBe('200px');
|
|
130
|
+
expect(viewport).toHaveAttribute('role', 'region');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('reflects logical axis and chaining intent without containing a fitting area', () => {
|
|
134
|
+
render(
|
|
135
|
+
<ScrollableArea
|
|
136
|
+
axis="both"
|
|
137
|
+
label="Canvas"
|
|
138
|
+
overscroll="contain"
|
|
139
|
+
data-testid="viewport">
|
|
140
|
+
Canvas
|
|
141
|
+
</ScrollableArea>,
|
|
142
|
+
);
|
|
143
|
+
const viewport = screen.getByTestId('viewport');
|
|
144
|
+
expect(viewport).toHaveAttribute('data-axis', 'both');
|
|
145
|
+
expect(viewport).toHaveAttribute('data-scroll-axis', 'both');
|
|
146
|
+
expect(viewport.style.overscrollBehaviorX).toBe('auto');
|
|
147
|
+
expect(viewport.style.overscrollBehaviorY).toBe('auto');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('keeps logical overflow and content sizing for writing-mode changes', () => {
|
|
151
|
+
const {rerender} = render(
|
|
152
|
+
<ScrollableArea axis="inline" label="Timeline" data-testid="viewport">
|
|
153
|
+
Timeline
|
|
154
|
+
</ScrollableArea>,
|
|
155
|
+
);
|
|
156
|
+
const viewport = screen.getByTestId('viewport');
|
|
157
|
+
const content = viewport.firstElementChild as HTMLElement;
|
|
158
|
+
const inlineViewportClass = viewport.className;
|
|
159
|
+
expect(viewport.style.overflowInline).toBe('');
|
|
160
|
+
expect(viewport.style.overflowBlock).toBe('');
|
|
161
|
+
const inlineContentClass = content.className;
|
|
162
|
+
expect(content.style.inlineSize).toBe('');
|
|
163
|
+
expect(content.style.minInlineSize).toBe('');
|
|
164
|
+
|
|
165
|
+
rerender(
|
|
166
|
+
<ScrollableArea axis="block" label="Timeline" data-testid="viewport">
|
|
167
|
+
Timeline
|
|
168
|
+
</ScrollableArea>,
|
|
169
|
+
);
|
|
170
|
+
expect(viewport.className).not.toBe(inlineViewportClass);
|
|
171
|
+
expect(viewport.style.overflowInline).toBe('');
|
|
172
|
+
expect(viewport.style.overflowBlock).toBe('');
|
|
173
|
+
expect(content.className).not.toBe(inlineContentClass);
|
|
174
|
+
expect(content.style.inlineSize).toBe('');
|
|
175
|
+
const source = stylesSource();
|
|
176
|
+
expect(source).not.toContain('dynamicStyles.overflow(');
|
|
177
|
+
expect(source).not.toContain('logicalOverflowStyle');
|
|
178
|
+
expect(hookSource()).toContain('styles.overflow(');
|
|
179
|
+
expect(source).toContain("minInlineSize: '100%'");
|
|
180
|
+
expect(source).toContain("inlineSize: 'max-content'");
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('uses clip while fitting and makes fitting Sticky containment explicit', () => {
|
|
184
|
+
const {rerender} = render(
|
|
185
|
+
<ScrollableArea label="Messages" data-testid="viewport">
|
|
186
|
+
Messages
|
|
187
|
+
</ScrollableArea>,
|
|
188
|
+
);
|
|
189
|
+
const viewport = screen.getByTestId('viewport');
|
|
190
|
+
expect(viewport.getAttribute('style')).toContain('clip');
|
|
191
|
+
expect(viewport).not.toHaveAttribute('tabindex');
|
|
192
|
+
|
|
193
|
+
rerender(
|
|
194
|
+
<ScrollableArea
|
|
195
|
+
label="Messages"
|
|
196
|
+
stickyContainment="always"
|
|
197
|
+
data-testid="viewport">
|
|
198
|
+
Messages
|
|
199
|
+
</ScrollableArea>,
|
|
200
|
+
);
|
|
201
|
+
expect(viewport.getAttribute('style')).toContain('auto');
|
|
202
|
+
expect(viewport.getAttribute('style')).toContain('hidden');
|
|
203
|
+
expect(viewport).not.toHaveAttribute('tabindex');
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('accepts standard container sizing props on the viewport', () => {
|
|
207
|
+
render(
|
|
208
|
+
<ScrollableArea
|
|
209
|
+
label="Messages"
|
|
210
|
+
width={320}
|
|
211
|
+
height="50vh"
|
|
212
|
+
maxWidth="100%"
|
|
213
|
+
minHeight={120}
|
|
214
|
+
data-testid="viewport">
|
|
215
|
+
Messages
|
|
216
|
+
</ScrollableArea>,
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
expect(screen.getByTestId('viewport')).toBeInTheDocument();
|
|
220
|
+
expect(stylesSource()).toContain('dynamicStyles.sizing(');
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('publishes content padding without changing the viewport default', () => {
|
|
224
|
+
const {rerender} = render(
|
|
225
|
+
<ScrollableArea label="Messages" data-testid="viewport">
|
|
226
|
+
Messages
|
|
227
|
+
</ScrollableArea>,
|
|
228
|
+
);
|
|
229
|
+
const viewport = screen.getByTestId('viewport');
|
|
230
|
+
const content = viewport.firstElementChild as HTMLElement;
|
|
231
|
+
const defaultViewportClass = viewport.className;
|
|
232
|
+
const defaultContentClass = content.className;
|
|
233
|
+
|
|
234
|
+
rerender(
|
|
235
|
+
<ScrollableArea
|
|
236
|
+
label="Messages"
|
|
237
|
+
padding={4}
|
|
238
|
+
paddingInlineEnd={2}
|
|
239
|
+
data-testid="viewport">
|
|
240
|
+
Messages
|
|
241
|
+
</ScrollableArea>,
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
expect(viewport.className).toBe(defaultViewportClass);
|
|
245
|
+
expect(content.className).not.toBe(defaultContentClass);
|
|
246
|
+
const source = stylesSource();
|
|
247
|
+
expect(source).toContain('containerPaddingInlineVarStyles[padding]');
|
|
248
|
+
expect(source).toContain(
|
|
249
|
+
'containerPaddingInlineEndVarStyles[paddingInlineEnd]',
|
|
250
|
+
);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('keeps inherited container bleed opt-in on the viewport', () => {
|
|
254
|
+
const {rerender} = render(
|
|
255
|
+
<ScrollableArea label="Messages" data-testid="viewport">
|
|
256
|
+
Messages
|
|
257
|
+
</ScrollableArea>,
|
|
258
|
+
);
|
|
259
|
+
const viewport = screen.getByTestId('viewport');
|
|
260
|
+
const containedClass = viewport.className;
|
|
261
|
+
|
|
262
|
+
rerender(
|
|
263
|
+
<ScrollableArea label="Messages" isFullBleed data-testid="viewport">
|
|
264
|
+
Messages
|
|
265
|
+
</ScrollableArea>,
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
expect(viewport.className).not.toBe(containedClass);
|
|
269
|
+
const source = stylesSource();
|
|
270
|
+
expect(source).toContain('var(--container-padding-inline-start, 0px)');
|
|
271
|
+
expect(source).toContain('isFullBleed && styles.fullBleed');
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('uses the neutral token for native scrollbar color with a transparent track', () => {
|
|
275
|
+
const source = stylesSource();
|
|
276
|
+
expect(source).toContain("colorVars['--color-neutral']");
|
|
277
|
+
expect(source).toContain('transparent');
|
|
278
|
+
expect(source).toContain("'@media (forced-colors: active)': 'auto'");
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('guards scroll-state query containment as a progressive enhancement', () => {
|
|
282
|
+
const source = stylesSource();
|
|
283
|
+
expect(source).toContain("'@supports (container-type: scroll-state)'");
|
|
284
|
+
expect(source).toContain("'scroll-state'");
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
function hookSource(): string {
|
|
289
|
+
return readFileSync('packages/core/src/hooks/useScrollableArea.ts', 'utf8');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function stylesSource(): string {
|
|
293
|
+
// This focused source assertion complements the built-CSS browser probe: it
|
|
294
|
+
// keeps the capability guard and token reference reviewable at unit-test speed.
|
|
295
|
+
return readFileSync(
|
|
296
|
+
'packages/core/src/ScrollableArea/ScrollableArea.tsx',
|
|
297
|
+
'utf8',
|
|
298
|
+
);
|
|
299
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @file ScrollableArea.tsx
|
|
7
|
+
* @input Logical scroll intent, accessible name, overscroll policy, viewport sizing, content padding, and content
|
|
8
|
+
* @output Native scroll viewport with an observed real content box
|
|
9
|
+
* @position Reference composition over useScrollableArea
|
|
10
|
+
*
|
|
11
|
+
* SYNC: When modified, update:
|
|
12
|
+
* - /packages/core/src/ScrollableArea/ScrollableArea.test.tsx
|
|
13
|
+
* - /packages/core/src/ScrollableArea/ScrollableArea.doc.mjs
|
|
14
|
+
* - /apps/storybook/stories/ScrollableArea.stories.tsx
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type {ReactNode} from 'react';
|
|
18
|
+
import * as stylex from '@stylexjs/stylex';
|
|
19
|
+
import type {BaseProps} from '../BaseProps';
|
|
20
|
+
import {
|
|
21
|
+
paddingStyles,
|
|
22
|
+
paddingInlineStyles,
|
|
23
|
+
paddingInlineStartStyles,
|
|
24
|
+
paddingInlineEndStyles,
|
|
25
|
+
paddingBlockStyles,
|
|
26
|
+
paddingBlockStartStyles,
|
|
27
|
+
paddingBlockEndStyles,
|
|
28
|
+
containerPaddingInlineVarStyles,
|
|
29
|
+
containerPaddingInlineStartVarStyles,
|
|
30
|
+
containerPaddingInlineEndVarStyles,
|
|
31
|
+
containerPaddingBlockStartVarStyles,
|
|
32
|
+
containerPaddingBlockEndVarStyles,
|
|
33
|
+
} from '../Layout/padding.stylex';
|
|
34
|
+
import type {SizeValue, SpacingStep} from '../utils/types';
|
|
35
|
+
import {
|
|
36
|
+
useScrollableArea,
|
|
37
|
+
type ScrollAxis,
|
|
38
|
+
type ScrollOverscroll,
|
|
39
|
+
type ScrollStickyContainment,
|
|
40
|
+
} from '../hooks/useScrollableArea';
|
|
41
|
+
import {colorVars} from '../theme/tokens.stylex';
|
|
42
|
+
import {focusOutlineStyles} from '../utils/focusOutline.stylex';
|
|
43
|
+
import {mergeProps} from '../utils/mergeProps';
|
|
44
|
+
import {themeProps} from '../utils/themeProps';
|
|
45
|
+
|
|
46
|
+
const styles = stylex.create({
|
|
47
|
+
viewport: {
|
|
48
|
+
boxSizing: 'border-box',
|
|
49
|
+
maxInlineSize: '100%',
|
|
50
|
+
maxBlockSize: '100%',
|
|
51
|
+
WebkitOverflowScrolling: 'touch',
|
|
52
|
+
scrollbarColor: {
|
|
53
|
+
default: `${colorVars['--color-neutral']} transparent`,
|
|
54
|
+
'@media (forced-colors: active)': 'auto',
|
|
55
|
+
},
|
|
56
|
+
scrollbarWidth: 'auto',
|
|
57
|
+
scrollbarGutter: 'auto',
|
|
58
|
+
// Chromium can resolve CSS-only stuck/edge presentation for descendants.
|
|
59
|
+
// Unsupported engines keep the hook state as the behavior source of truth.
|
|
60
|
+
containerType: {
|
|
61
|
+
default: null,
|
|
62
|
+
'@supports (container-type: scroll-state)': 'scroll-state',
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
fullBleed: {
|
|
66
|
+
marginInlineStart: 'calc(-1 * var(--container-padding-inline-start, 0px))',
|
|
67
|
+
marginInlineEnd: 'calc(-1 * var(--container-padding-inline-end, 0px))',
|
|
68
|
+
maxInlineSize:
|
|
69
|
+
'calc(100% + var(--container-padding-inline-start, 0px) + var(--container-padding-inline-end, 0px))',
|
|
70
|
+
marginBlockStart: {
|
|
71
|
+
default: null,
|
|
72
|
+
':first-child': 'calc(-1 * var(--container-padding-block-start, 0px))',
|
|
73
|
+
},
|
|
74
|
+
marginBlockEnd: {
|
|
75
|
+
default: null,
|
|
76
|
+
':last-child': 'calc(-1 * var(--container-padding-block-end, 0px))',
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
content: {
|
|
80
|
+
boxSizing: 'border-box',
|
|
81
|
+
minInlineSize: '100%',
|
|
82
|
+
minBlockSize: '100%',
|
|
83
|
+
},
|
|
84
|
+
contentWithInlineOverflow: {
|
|
85
|
+
inlineSize: 'max-content',
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const dynamicStyles = stylex.create({
|
|
90
|
+
sizing: (
|
|
91
|
+
width: SizeValue | null,
|
|
92
|
+
height: SizeValue | null,
|
|
93
|
+
maxWidth: SizeValue | null,
|
|
94
|
+
minHeight: SizeValue | null,
|
|
95
|
+
) => ({width, height, maxWidth, minHeight}),
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
export type ScrollableAreaStickyContainment = ScrollStickyContainment;
|
|
99
|
+
|
|
100
|
+
export interface ScrollableAreaProps extends Omit<
|
|
101
|
+
BaseProps<HTMLDivElement>,
|
|
102
|
+
'aria-label' | 'children' | 'role' | 'tabIndex'
|
|
103
|
+
> {
|
|
104
|
+
/** Content rendered inside the observed content box. */
|
|
105
|
+
children?: ReactNode;
|
|
106
|
+
/** Logical axis or axes where scrolling is allowed. @default 'block' */
|
|
107
|
+
axis?: ScrollAxis;
|
|
108
|
+
/** Accessible name used when an effective viewport enters the tab order. */
|
|
109
|
+
label: string;
|
|
110
|
+
/** Semantics for the named scroll viewport. @default 'group' */
|
|
111
|
+
role?: 'group' | 'region';
|
|
112
|
+
/** Whether effective axes pass scroll gestures to ancestors at an edge. @default 'allow' */
|
|
113
|
+
overscroll?: ScrollOverscroll;
|
|
114
|
+
/** Width of the viewport; numbers are interpreted as pixels. */
|
|
115
|
+
width?: SizeValue;
|
|
116
|
+
/** Height of the viewport; numbers are interpreted as pixels. */
|
|
117
|
+
height?: SizeValue;
|
|
118
|
+
/** Maximum width of the viewport. */
|
|
119
|
+
maxWidth?: SizeValue;
|
|
120
|
+
/** Minimum height of the viewport. */
|
|
121
|
+
minHeight?: SizeValue;
|
|
122
|
+
/** Whether a fitting viewport deliberately remains a Sticky containing boundary. @default 'whenScrollable' */
|
|
123
|
+
stickyContainment?: ScrollableAreaStickyContainment;
|
|
124
|
+
/** Content padding using the shared spacing scale. @default 0 */
|
|
125
|
+
padding?: SpacingStep;
|
|
126
|
+
/** Logical inline-axis content padding; overrides `padding` on that axis. */
|
|
127
|
+
paddingInline?: SpacingStep;
|
|
128
|
+
/** Logical inline-start content padding; overrides broader padding values. */
|
|
129
|
+
paddingInlineStart?: SpacingStep;
|
|
130
|
+
/** Logical inline-end content padding; overrides broader padding values. */
|
|
131
|
+
paddingInlineEnd?: SpacingStep;
|
|
132
|
+
/** Logical block-axis content padding; overrides `padding` on that axis. */
|
|
133
|
+
paddingBlock?: SpacingStep;
|
|
134
|
+
/** Logical block-start content padding; overrides broader padding values. */
|
|
135
|
+
paddingBlockStart?: SpacingStep;
|
|
136
|
+
/** Logical block-end content padding; overrides broader padding values. */
|
|
137
|
+
paddingBlockEnd?: SpacingStep;
|
|
138
|
+
/** Let the viewport escape inherited container padding. @default false */
|
|
139
|
+
isFullBleed?: boolean;
|
|
140
|
+
/** Ref connected to the native scroll viewport. */
|
|
141
|
+
ref?: React.Ref<HTMLDivElement>;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* A native scroll viewport with axis-aware accessibility and chaining.
|
|
146
|
+
*
|
|
147
|
+
* The root is the viewport. Its single child is a normal block content box with
|
|
148
|
+
* a 100% minimum size; children participate in that box rather than directly in
|
|
149
|
+
* viewport flex/grid layout. Inline scrolling gives the box max-content inline
|
|
150
|
+
* sizing, and its minimum block size deliberately does not create a definite
|
|
151
|
+
* percentage-height basis. Use the hook on owned structure when those formatting
|
|
152
|
+
* semantics must stay unchanged. Native scrolling and platform scrollbars remain
|
|
153
|
+
* authoritative.
|
|
154
|
+
*
|
|
155
|
+
* @example
|
|
156
|
+
* ```
|
|
157
|
+
* <ScrollableArea axis="block" label="Activity history">
|
|
158
|
+
* <ActivityList />
|
|
159
|
+
* </ScrollableArea>
|
|
160
|
+
* ```
|
|
161
|
+
*/
|
|
162
|
+
export function ScrollableArea({
|
|
163
|
+
children,
|
|
164
|
+
axis = 'block',
|
|
165
|
+
label,
|
|
166
|
+
role = 'group',
|
|
167
|
+
overscroll = 'allow',
|
|
168
|
+
width,
|
|
169
|
+
height,
|
|
170
|
+
maxWidth,
|
|
171
|
+
minHeight,
|
|
172
|
+
stickyContainment = 'whenScrollable',
|
|
173
|
+
padding = 0,
|
|
174
|
+
paddingInline,
|
|
175
|
+
paddingInlineStart,
|
|
176
|
+
paddingInlineEnd,
|
|
177
|
+
paddingBlock,
|
|
178
|
+
paddingBlockStart,
|
|
179
|
+
paddingBlockEnd,
|
|
180
|
+
isFullBleed = false,
|
|
181
|
+
ref,
|
|
182
|
+
xstyle,
|
|
183
|
+
className,
|
|
184
|
+
style,
|
|
185
|
+
...props
|
|
186
|
+
}: ScrollableAreaProps) {
|
|
187
|
+
const {getViewportProps, getContentProps} = useScrollableArea({
|
|
188
|
+
axis,
|
|
189
|
+
keyboardAccess: {owner: 'viewport', label, role},
|
|
190
|
+
overscroll,
|
|
191
|
+
stickyContainment,
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const mergedViewportProps = mergeProps(
|
|
195
|
+
themeProps('scrollable-area', {axis}),
|
|
196
|
+
stylex.props(
|
|
197
|
+
styles.viewport,
|
|
198
|
+
isFullBleed && styles.fullBleed,
|
|
199
|
+
focusOutlineStyles.focusVisible,
|
|
200
|
+
dynamicStyles.sizing(
|
|
201
|
+
width ?? null,
|
|
202
|
+
height ?? null,
|
|
203
|
+
maxWidth ?? null,
|
|
204
|
+
minHeight ?? null,
|
|
205
|
+
),
|
|
206
|
+
xstyle,
|
|
207
|
+
),
|
|
208
|
+
className,
|
|
209
|
+
style,
|
|
210
|
+
);
|
|
211
|
+
const viewportProps = getViewportProps<HTMLDivElement>({
|
|
212
|
+
...props,
|
|
213
|
+
ref,
|
|
214
|
+
...mergedViewportProps,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
return (
|
|
218
|
+
<div {...viewportProps}>
|
|
219
|
+
<div
|
|
220
|
+
{...getContentProps<HTMLDivElement>({
|
|
221
|
+
...stylex.props(
|
|
222
|
+
styles.content,
|
|
223
|
+
axis !== 'block' && styles.contentWithInlineOverflow,
|
|
224
|
+
paddingStyles[padding],
|
|
225
|
+
containerPaddingInlineVarStyles[padding],
|
|
226
|
+
containerPaddingBlockStartVarStyles[padding],
|
|
227
|
+
containerPaddingBlockEndVarStyles[padding],
|
|
228
|
+
paddingInline != null && paddingInlineStyles[paddingInline],
|
|
229
|
+
paddingInline != null &&
|
|
230
|
+
containerPaddingInlineVarStyles[paddingInline],
|
|
231
|
+
paddingBlock != null && paddingBlockStyles[paddingBlock],
|
|
232
|
+
paddingBlock != null &&
|
|
233
|
+
containerPaddingBlockStartVarStyles[paddingBlock],
|
|
234
|
+
paddingBlock != null &&
|
|
235
|
+
containerPaddingBlockEndVarStyles[paddingBlock],
|
|
236
|
+
paddingInlineStart != null &&
|
|
237
|
+
paddingInlineStartStyles[paddingInlineStart],
|
|
238
|
+
paddingInlineStart != null &&
|
|
239
|
+
containerPaddingInlineStartVarStyles[paddingInlineStart],
|
|
240
|
+
paddingInlineEnd != null &&
|
|
241
|
+
paddingInlineEndStyles[paddingInlineEnd],
|
|
242
|
+
paddingInlineEnd != null &&
|
|
243
|
+
containerPaddingInlineEndVarStyles[paddingInlineEnd],
|
|
244
|
+
paddingBlockStart != null &&
|
|
245
|
+
paddingBlockStartStyles[paddingBlockStart],
|
|
246
|
+
paddingBlockStart != null &&
|
|
247
|
+
containerPaddingBlockStartVarStyles[paddingBlockStart],
|
|
248
|
+
paddingBlockEnd != null && paddingBlockEndStyles[paddingBlockEnd],
|
|
249
|
+
paddingBlockEnd != null &&
|
|
250
|
+
containerPaddingBlockEndVarStyles[paddingBlockEnd],
|
|
251
|
+
),
|
|
252
|
+
})}>
|
|
253
|
+
{children}
|
|
254
|
+
</div>
|
|
255
|
+
</div>
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
ScrollableArea.displayName = 'ScrollableArea';
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file index.ts
|
|
5
|
+
* @input ScrollableArea implementation and shared hook types
|
|
6
|
+
* @output Public ScrollableArea package surface
|
|
7
|
+
* @position Package entry point for ScrollableArea
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export {ScrollableArea} from './ScrollableArea';
|
|
11
|
+
export type {
|
|
12
|
+
ScrollableAreaProps,
|
|
13
|
+
ScrollableAreaStickyContainment,
|
|
14
|
+
} from './ScrollableArea';
|
|
15
|
+
export {useScrollableArea} from '../hooks/useScrollableArea';
|
|
16
|
+
export type {
|
|
17
|
+
ScrollAxis,
|
|
18
|
+
ScrollAxisState,
|
|
19
|
+
ScrollOverscroll,
|
|
20
|
+
ScrollStickyContainment,
|
|
21
|
+
ScrollKeyboardAccess,
|
|
22
|
+
ScrollableAreaState,
|
|
23
|
+
ScrollableElementProps,
|
|
24
|
+
UseScrollableAreaOptions,
|
|
25
|
+
UseScrollableAreaResult,
|
|
26
|
+
} from '../hooks/useScrollableArea';
|