@astryxdesign/core 0.6.1 → 0.6.2-canary.176dfa6
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 +33 -0
- package/dist/BottomSheet/BottomSheet.d.ts +1 -1
- package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheet.js +3 -1
- package/dist/BottomSheet/BottomSheetPanel.d.ts +7 -5
- package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetPanel.js +56 -19
- package/dist/DateRangeInput/DateRangeInput.d.ts +3 -0
- package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
- package/dist/DateRangeInput/DateRangeInput.js +16 -9
- package/dist/Dialog/DialogHeader.d.ts +1 -1
- package/dist/Dialog/DialogHeader.d.ts.map +1 -1
- package/dist/Dialog/DialogHeader.js +10 -7
- package/dist/FileInput/FileInput.d.ts.map +1 -1
- package/dist/FileInput/FileInput.js +9 -3
- package/dist/Markdown/Markdown.d.ts +10 -2
- package/dist/Markdown/Markdown.d.ts.map +1 -1
- package/dist/Markdown/Markdown.js +58 -14
- package/dist/Markdown/index.d.ts +1 -1
- package/dist/Markdown/index.d.ts.map +1 -1
- package/dist/Markdown/parser.d.ts +126 -12
- package/dist/Markdown/parser.d.ts.map +1 -1
- package/dist/Markdown/parser.js +369 -34
- package/dist/Markdown/utils.d.ts +1 -1
- package/dist/Markdown/utils.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearchEditPopover.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearchEditPopover.js +46 -30
- package/dist/Slider/Slider.d.ts.map +1 -1
- package/dist/Slider/Slider.js +5 -2
- package/dist/Spinner/Spinner.d.ts +1 -1
- package/dist/Spinner/Spinner.d.ts.map +1 -1
- package/dist/Spinner/Spinner.js +23 -15
- package/dist/ToggleButton/ToggleButton.d.ts +2 -1
- package/dist/ToggleButton/ToggleButton.d.ts.map +1 -1
- package/dist/ToggleButton/ToggleButton.js +7 -1
- package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
- package/dist/Typeahead/BaseTypeahead.js +15 -6
- package/dist/astryx.css +5 -2
- package/dist/hooks/scrollKeyboardDelegation.d.ts +3 -0
- package/dist/hooks/scrollKeyboardDelegation.d.ts.map +1 -0
- package/dist/hooks/scrollKeyboardDelegation.js +146 -0
- package/dist/hooks/useScrollableArea.d.ts +6 -2
- package/dist/hooks/useScrollableArea.d.ts.map +1 -1
- package/dist/hooks/useScrollableArea.js +17 -5
- package/locales/en.json +16 -0
- package/locales/pseudo.json +12 -0
- package/package.json +7 -5
- package/scripts/agent-doc-state.mjs +1 -1
- package/src/BottomSheet/BottomSheet.doc.mjs +8 -1
- package/src/BottomSheet/BottomSheet.spec.md +46 -20
- package/src/BottomSheet/BottomSheet.test.tsx +6 -3
- package/src/BottomSheet/BottomSheet.tsx +3 -1
- package/src/BottomSheet/BottomSheetKeyboard.test.tsx +195 -0
- package/src/BottomSheet/BottomSheetPanel.test.tsx +11 -1
- package/src/BottomSheet/BottomSheetPanel.tsx +49 -16
- package/src/BottomSheet/__tests__/BottomSheetKeyboard.a11y.browser.spec.ts +344 -0
- package/src/DateRangeInput/DateRangeInput.doc.mjs +35 -7
- package/src/DateRangeInput/DateRangeInput.spec.md +203 -0
- package/src/DateRangeInput/DateRangeInput.test.tsx +100 -4
- package/src/DateRangeInput/DateRangeInput.tsx +29 -20
- package/src/Dialog/Dialog.doc.mjs +3 -0
- package/src/Dialog/Dialog.spec.md +1 -1
- package/src/Dialog/DialogHeader.doc.mjs +38 -0
- package/src/Dialog/DialogHeader.test.tsx +49 -0
- package/src/Dialog/DialogHeader.tsx +23 -4
- package/src/Dialog/modules/DialogHeader.spec.md +152 -0
- package/src/FileInput/FileInput.doc.mjs +2 -0
- package/src/FileInput/FileInput.spec.md +199 -0
- package/src/FileInput/FileInput.test.tsx +14 -0
- package/src/FileInput/FileInput.tsx +13 -3
- package/src/Markdown/Markdown.doc.mjs +167 -42
- package/src/Markdown/Markdown.public.test.ts +157 -0
- package/src/Markdown/Markdown.spec.md +149 -70
- package/src/Markdown/Markdown.test.tsx +107 -3
- package/src/Markdown/Markdown.tsx +116 -35
- package/src/Markdown/incremental.test.ts +175 -7
- package/src/Markdown/index.ts +6 -0
- package/src/Markdown/parser.perf.test.ts +3 -1
- package/src/Markdown/parser.test.ts +122 -0
- package/src/Markdown/parser.ts +609 -81
- package/src/Markdown/utils.ts +6 -0
- package/src/PowerSearch/PowerSearchEditPopover.test.tsx +150 -1
- package/src/PowerSearch/PowerSearchEditPopover.tsx +51 -28
- package/src/ScrollableArea/modules/useScrollableArea.spec.md +50 -22
- package/src/Slider/Slider.doc.mjs +16 -0
- package/src/Slider/Slider.spec.md +61 -47
- package/src/Slider/Slider.test.tsx +18 -0
- package/src/Slider/Slider.tsx +12 -6
- package/src/Spinner/Spinner.doc.mjs +6 -3
- package/src/Spinner/Spinner.test.tsx +37 -0
- package/src/Spinner/Spinner.tsx +31 -14
- package/src/ToggleButton/ToggleButton.test.tsx +133 -0
- package/src/ToggleButton/ToggleButton.tsx +9 -2
- package/src/ToggleButton/__tests__/ToggleButton.a11y.chromium.spec.ts +209 -0
- package/src/Tokenizer/Tokenizer.spec.md +142 -75
- package/src/Typeahead/BaseTypeahead.spec.md +4 -3
- package/src/Typeahead/BaseTypeahead.tsx +15 -6
- package/src/Typeahead/Typeahead.test.tsx +53 -0
- package/src/hooks/scrollKeyboardDelegation.test.ts +155 -0
- package/src/hooks/scrollKeyboardDelegation.ts +233 -0
- package/src/hooks/useScrollableArea.doc.mjs +15 -3
- package/src/hooks/useScrollableArea.test.tsx +59 -1
- package/src/hooks/useScrollableArea.ts +34 -10
- package/src/theme/derivedVarRegistry.test.ts +6 -4
package/src/Markdown/utils.ts
CHANGED
|
@@ -16,9 +16,15 @@ export {
|
|
|
16
16
|
|
|
17
17
|
export type {
|
|
18
18
|
InlineNode,
|
|
19
|
+
InlineNodeWithMath,
|
|
20
|
+
MathInlineNode,
|
|
19
21
|
BlockNode,
|
|
22
|
+
BlockNodeWithMath,
|
|
23
|
+
MathBlockNode,
|
|
20
24
|
ListItemNode,
|
|
21
25
|
TableCellNode,
|
|
22
26
|
TableAlignment,
|
|
27
|
+
ParseOptions,
|
|
28
|
+
MathParseOptions,
|
|
23
29
|
IncrementalState,
|
|
24
30
|
} from './parser';
|
|
@@ -10,7 +10,14 @@ import {
|
|
|
10
10
|
afterAll,
|
|
11
11
|
afterEach,
|
|
12
12
|
} from 'vitest';
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
render,
|
|
15
|
+
screen,
|
|
16
|
+
fireEvent,
|
|
17
|
+
act,
|
|
18
|
+
waitFor,
|
|
19
|
+
within,
|
|
20
|
+
} from '@testing-library/react';
|
|
14
21
|
import React, {useState} from 'react';
|
|
15
22
|
import * as stylex from '@stylexjs/stylex';
|
|
16
23
|
import {spacingVars} from '../theme/tokens.stylex';
|
|
@@ -170,6 +177,148 @@ describe('PowerSearch', () => {
|
|
|
170
177
|
expect(popoverText).toContain('equals');
|
|
171
178
|
});
|
|
172
179
|
|
|
180
|
+
describe('value editor identity follows the field and operator', () => {
|
|
181
|
+
// Two fields with the same value type, so an unkeyed editor would be
|
|
182
|
+
// reused across the switch and keep the first field's menu contents.
|
|
183
|
+
const sameTypeConfig: PowerSearchConfig = {
|
|
184
|
+
name: 'test-same-type',
|
|
185
|
+
fields: [
|
|
186
|
+
{
|
|
187
|
+
key: 'creator',
|
|
188
|
+
label: 'Creator',
|
|
189
|
+
operators: [
|
|
190
|
+
{
|
|
191
|
+
key: 'is_any_of',
|
|
192
|
+
label: 'is any of',
|
|
193
|
+
value: {
|
|
194
|
+
type: 'enum_list',
|
|
195
|
+
values: [{value: 'creator-1', label: 'Creator Person'}],
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
key: 'owner',
|
|
202
|
+
label: 'Owner',
|
|
203
|
+
operators: [
|
|
204
|
+
{
|
|
205
|
+
key: 'is_any_of',
|
|
206
|
+
label: 'is any of',
|
|
207
|
+
value: {
|
|
208
|
+
type: 'enum_list',
|
|
209
|
+
values: [{value: 'owner-1', label: 'Owner Person'}],
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
],
|
|
213
|
+
},
|
|
214
|
+
],
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const creatorOption = () =>
|
|
218
|
+
screen.queryByRole('option', {name: 'Creator Person', hidden: true});
|
|
219
|
+
const ownerOption = () =>
|
|
220
|
+
screen.queryByRole('option', {name: 'Owner Person', hidden: true});
|
|
221
|
+
|
|
222
|
+
it('replaces an open value menu when the field changes', async () => {
|
|
223
|
+
function Harness() {
|
|
224
|
+
const internalConfig = useInternalConfig(sameTypeConfig);
|
|
225
|
+
return (
|
|
226
|
+
<PowerSearchEditPopover
|
|
227
|
+
config={internalConfig}
|
|
228
|
+
filter={{
|
|
229
|
+
field: 'creator',
|
|
230
|
+
operator: 'is_any_of',
|
|
231
|
+
value: {type: 'enum_list', value: []},
|
|
232
|
+
}}
|
|
233
|
+
mode="edit"
|
|
234
|
+
onSave={() => {}}
|
|
235
|
+
onCancel={() => {}}
|
|
236
|
+
/>
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
render(<Harness />);
|
|
241
|
+
|
|
242
|
+
// Open the Creator value menu and leave it open.
|
|
243
|
+
fireEvent.focus(screen.getByRole('combobox', {name: 'Values'}));
|
|
244
|
+
await waitFor(() => expect(creatorOption()).toBeInTheDocument());
|
|
245
|
+
|
|
246
|
+
// Switch the field underneath the open menu.
|
|
247
|
+
fireEvent.click(screen.getByRole('combobox', {name: 'Field'}));
|
|
248
|
+
fireEvent.click(
|
|
249
|
+
screen.getByRole('option', {name: 'Owner', hidden: true}),
|
|
250
|
+
);
|
|
251
|
+
act(() => flushRAF());
|
|
252
|
+
|
|
253
|
+
// The old field's options are gone, the new field's are shown.
|
|
254
|
+
expect(creatorOption()).not.toBeInTheDocument();
|
|
255
|
+
await waitFor(() => expect(ownerOption()).toBeInTheDocument());
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('replaces an open value menu on a nested sub-filter row', async () => {
|
|
259
|
+
const nestedConfig: PowerSearchConfig = {
|
|
260
|
+
name: 'test-nested-same-type',
|
|
261
|
+
fields: [
|
|
262
|
+
{
|
|
263
|
+
key: 'group',
|
|
264
|
+
label: 'Group',
|
|
265
|
+
operators: [
|
|
266
|
+
{key: 'all_of', label: 'all of', value: {type: 'nested'}},
|
|
267
|
+
],
|
|
268
|
+
},
|
|
269
|
+
...sameTypeConfig.fields,
|
|
270
|
+
],
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
function Harness() {
|
|
274
|
+
const internalConfig = useInternalConfig(nestedConfig);
|
|
275
|
+
return (
|
|
276
|
+
<PowerSearchEditPopover
|
|
277
|
+
config={internalConfig}
|
|
278
|
+
filter={{
|
|
279
|
+
field: 'group',
|
|
280
|
+
operator: 'all_of',
|
|
281
|
+
value: {
|
|
282
|
+
type: 'nested',
|
|
283
|
+
value: [
|
|
284
|
+
{
|
|
285
|
+
field: 'creator',
|
|
286
|
+
operator: 'is_any_of',
|
|
287
|
+
value: {type: 'enum_list', value: []},
|
|
288
|
+
},
|
|
289
|
+
],
|
|
290
|
+
},
|
|
291
|
+
}}
|
|
292
|
+
mode="edit"
|
|
293
|
+
onSave={() => {}}
|
|
294
|
+
onCancel={() => {}}
|
|
295
|
+
/>
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
render(<Harness />);
|
|
300
|
+
|
|
301
|
+
fireEvent.focus(screen.getByRole('combobox', {name: 'Values'}));
|
|
302
|
+
await waitFor(() => expect(creatorOption()).toBeInTheDocument());
|
|
303
|
+
|
|
304
|
+
// The nested row's own field selector is the one currently on
|
|
305
|
+
// "Creator"; the root selector stays on "Group".
|
|
306
|
+
const rowField = screen
|
|
307
|
+
.getAllByRole('combobox', {name: 'Field'})
|
|
308
|
+
.find(el => el.textContent === 'Creator');
|
|
309
|
+
expect(rowField).toBeDefined();
|
|
310
|
+
fireEvent.click(rowField!);
|
|
311
|
+
const rowListbox = document.getElementById(
|
|
312
|
+
rowField!.getAttribute('aria-controls')!,
|
|
313
|
+
)!;
|
|
314
|
+
fireEvent.click(
|
|
315
|
+
within(rowListbox).getByRole('option', {name: 'Owner', hidden: true}),
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
expect(creatorOption()).not.toBeInTheDocument();
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
173
322
|
it('edit popover shows correct filter after removing a preceding filter', () => {
|
|
174
323
|
const filters: PowerSearchFilter[] = [
|
|
175
324
|
{field: 'status', operator: 'is', value: {type: 'string', value: 'open'}},
|
|
@@ -22,7 +22,10 @@ import {TreeList, type TreeListItemData} from '../TreeList';
|
|
|
22
22
|
import {useTranslator} from '../i18n';
|
|
23
23
|
import {isImeKeyEvent} from '../utils/ime';
|
|
24
24
|
import {spacingVars, typeScaleVars} from '../theme/tokens.stylex';
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
PowerSearchValueEditor,
|
|
27
|
+
type PowerSearchValueEditorProps,
|
|
28
|
+
} from './PowerSearchValueEditor';
|
|
26
29
|
import {resolveOperatorLabel} from './resolveOperatorLabel';
|
|
27
30
|
import type {InternalConfig} from './useInternalConfig';
|
|
28
31
|
import type {
|
|
@@ -356,6 +359,7 @@ function NestedSubFilterRow({
|
|
|
356
359
|
{operatorValue && !isEmptyType && !isNestedType && (
|
|
357
360
|
<div {...stylex.props(styles.nestedRowValueEditor)}>
|
|
358
361
|
<PowerSearchValueEditor
|
|
362
|
+
key={valueEditorKey(subFilter)}
|
|
359
363
|
operatorValue={operatorValue}
|
|
360
364
|
filterValue={subFilter.value}
|
|
361
365
|
onChange={handleValueChange}
|
|
@@ -606,6 +610,42 @@ function NestedEditor({
|
|
|
606
610
|
return <TreeList items={items} density="balanced" />;
|
|
607
611
|
}
|
|
608
612
|
|
|
613
|
+
// =============================================================================
|
|
614
|
+
// Value editor cell
|
|
615
|
+
// =============================================================================
|
|
616
|
+
|
|
617
|
+
// A field switch must replace the editor, not update it in place: a reused
|
|
618
|
+
// editor keeps its open menu and old results.
|
|
619
|
+
function valueEditorKey(filter: {field: string; operator?: string}): string {
|
|
620
|
+
return `${filter.field}\u0000${filter.operator ?? ''}`;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Keyed on field + operator, so this also mounts on a field switch, not just
|
|
624
|
+
// when the popover opens; the focus handoff below runs for both.
|
|
625
|
+
function ValueEditorCell(props: PowerSearchValueEditorProps) {
|
|
626
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
627
|
+
|
|
628
|
+
useEffect(() => {
|
|
629
|
+
const frame = requestAnimationFrame(() => {
|
|
630
|
+
const container = ref.current;
|
|
631
|
+
if (!container) {
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
const focusable = container.querySelector<HTMLElement>(
|
|
635
|
+
'input:not([disabled]), button:not([disabled]), textarea:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])',
|
|
636
|
+
);
|
|
637
|
+
focusable?.focus();
|
|
638
|
+
});
|
|
639
|
+
return () => cancelAnimationFrame(frame);
|
|
640
|
+
}, []);
|
|
641
|
+
|
|
642
|
+
return (
|
|
643
|
+
<div ref={ref} {...stylex.props(styles.valueEditor)}>
|
|
644
|
+
<PowerSearchValueEditor {...props} />
|
|
645
|
+
</div>
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
|
|
609
649
|
// =============================================================================
|
|
610
650
|
// Main popover
|
|
611
651
|
// =============================================================================
|
|
@@ -625,22 +665,6 @@ export function PowerSearchEditPopover({
|
|
|
625
665
|
saveButtonLabelFromProps ?? t('@astryx.powersearch.editor.apply');
|
|
626
666
|
const [partialFilter, setPartialFilter] =
|
|
627
667
|
useState<PartialFilter>(initialFilter);
|
|
628
|
-
const valueEditorRef = useRef<HTMLDivElement>(null);
|
|
629
|
-
|
|
630
|
-
// Focus the first focusable element inside the value editor after mount
|
|
631
|
-
useEffect(() => {
|
|
632
|
-
const frame = requestAnimationFrame(() => {
|
|
633
|
-
const container = valueEditorRef.current;
|
|
634
|
-
if (!container) {
|
|
635
|
-
return;
|
|
636
|
-
}
|
|
637
|
-
const focusable = container.querySelector<HTMLElement>(
|
|
638
|
-
'input:not([disabled]), button:not([disabled]), textarea:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])',
|
|
639
|
-
);
|
|
640
|
-
focusable?.focus();
|
|
641
|
-
});
|
|
642
|
-
return () => cancelAnimationFrame(frame);
|
|
643
|
-
}, []);
|
|
644
668
|
|
|
645
669
|
const currentOperator = partialFilter.operator
|
|
646
670
|
? config.getOperator(partialFilter.field, partialFilter.operator)
|
|
@@ -854,17 +878,16 @@ export function PowerSearchEditPopover({
|
|
|
854
878
|
</div>
|
|
855
879
|
)}
|
|
856
880
|
{operatorValue && !isEmptyType && (
|
|
857
|
-
<
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
</div>
|
|
881
|
+
<ValueEditorCell
|
|
882
|
+
key={valueEditorKey(partialFilter)}
|
|
883
|
+
operatorValue={operatorValue}
|
|
884
|
+
filterValue={partialFilter.value}
|
|
885
|
+
onChange={handleValueChange}
|
|
886
|
+
onEnter={handleSave}
|
|
887
|
+
config={config}
|
|
888
|
+
maxMenuItems={maxMenuItems}
|
|
889
|
+
isDisabled={isReadOnly}
|
|
890
|
+
/>
|
|
868
891
|
)}
|
|
869
892
|
</HStack>
|
|
870
893
|
</div>
|
|
@@ -10,7 +10,12 @@ approved_by: cixzhang
|
|
|
10
10
|
approved_at: 2026-09-11
|
|
11
11
|
owners: [cixzhang]
|
|
12
12
|
review_triggers: [public-api, behavior, accessibility]
|
|
13
|
-
verified_by:
|
|
13
|
+
verified_by:
|
|
14
|
+
[
|
|
15
|
+
packages/core/src/hooks/useScrollableArea.test.tsx,
|
|
16
|
+
packages/core/src/hooks/scrollKeyboardDelegation.test.ts,
|
|
17
|
+
packages/core/src/BottomSheet/__tests__/BottomSheetKeyboard.a11y.browser.spec.ts,
|
|
18
|
+
]
|
|
14
19
|
parent_component: component:ScrollableArea
|
|
15
20
|
references: [architecture:public-component-api, spec:AST-025/DEC-1]
|
|
16
21
|
---
|
|
@@ -47,24 +52,25 @@ and scroll chaining without inserting structure.
|
|
|
47
52
|
|
|
48
53
|
## Public API and concepts
|
|
49
54
|
|
|
50
|
-
| Concept | Closed values or states
|
|
51
|
-
| -------------- |
|
|
52
|
-
| axis | `inline`, `block`, `both`
|
|
53
|
-
| keyboard owner | `content`, named `viewport`
|
|
54
|
-
| overscroll | `allow`, `contain`
|
|
55
|
-
| axis state | `isScrollable`, `atStart`, `atEnd`
|
|
55
|
+
| Concept | Closed values or states | Meaning | Default | Owner | Stability |
|
|
56
|
+
| -------------- | ------------------------------------------------------ | --------------------------------------- | ------------------------- | ----------------------------------------- | --------- |
|
|
57
|
+
| axis | `inline`, `block`, `both` | requested logical scroll intent | required | `spec:AST-025` | stable |
|
|
58
|
+
| keyboard owner | `content`, named `viewport`, named `contentOrViewport` | fixed or automatic keyboard scroll path | required | `spec:AST-025` | stable |
|
|
59
|
+
| overscroll | `allow`, `contain` | edge propagation on effective axes | `allow` | `spec:AST-025` | stable |
|
|
60
|
+
| axis state | `isScrollable`, `atStart`, `atEnd` | effective ownership and logical edges | inactive, both edges true | `module:ScrollableArea/useScrollableArea` | stable |
|
|
56
61
|
|
|
57
62
|
## Behavioral contract
|
|
58
63
|
|
|
59
|
-
| ID | Invariant
|
|
60
|
-
| --- |
|
|
61
|
-
| FR1 | An axis MUST be effective only when requested, computed overflow is scroll-capable, and geometry exceeds 1px.
|
|
62
|
-
| FR2 | Viewport and content geometry MUST be observed; invalidations MUST coalesce and preserve the last valid state while unmeasurable.
|
|
63
|
-
| FR3 | Logical edges and behavior-owned physical overflow MUST map through direction and horizontal, vertical, or sideways writing modes without requiring adopters to recreate the mapping.
|
|
64
|
-
| FR4 | The nearest registered effective owner MUST win independently by axis.
|
|
65
|
-
| FR5 | Viewport keyboard props and overscroll containment MUST appear only while applicable requested axes are effective.
|
|
66
|
-
| FR6 | Prop getters MUST compose refs and preserve caller handlers/classes/styles/ARIA while behavior-owned accessibility and non-cancellable behavior win conflicts.
|
|
67
|
-
| FR7 | The viewport prop getter MUST consume caller `xstyle` and internally compose geometry-driven `clip`/`auto`/`hidden` overflow plus explicit fitting Sticky containment.
|
|
64
|
+
| ID | Invariant | Basis | Acceptance and implementation state |
|
|
65
|
+
| --- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------- | --------------------------------------- |
|
|
66
|
+
| FR1 | An axis MUST be effective only when requested, computed overflow is scroll-capable, and geometry exceeds 1px. | `spec:AST-025` FR1–FR4 | implemented |
|
|
67
|
+
| FR2 | Viewport and content geometry MUST be observed; invalidations MUST coalesce and preserve the last valid state while unmeasurable. | `spec:AST-025` FR5–FR9 | implemented |
|
|
68
|
+
| FR3 | Logical edges and behavior-owned physical overflow MUST map through direction and horizontal, vertical, or sideways writing modes without requiring adopters to recreate the mapping. | `spec:AST-025` FR1, FR3–FR4 | implemented |
|
|
69
|
+
| FR4 | The nearest registered effective owner MUST win independently by axis. | `spec:AST-025` FR10, FR18 | implemented privately for future Sticky |
|
|
70
|
+
| FR5 | Viewport keyboard props and overscroll containment MUST appear only while applicable requested axes are effective. | `spec:AST-025` FR12–FR14 | implemented |
|
|
71
|
+
| FR6 | Prop getters MUST compose refs and preserve caller handlers/classes/styles/ARIA while behavior-owned accessibility and non-cancellable behavior win conflicts. | `spec:AST-025` IR4 | implemented |
|
|
72
|
+
| FR7 | The viewport prop getter MUST consume caller `xstyle` and internally compose geometry-driven `clip`/`auto`/`hidden` overflow plus explicit fitting Sticky containment. | `spec:AST-025` FR21 | implemented |
|
|
73
|
+
| FR8 | `contentOrViewport` MUST retain the named overflowing viewport, inspect the first sequential descendant on forward Tab entry, and delegate only to a native link/button outside navigation-key-owning surfaces. Reverse traversal from the delegated first child skips the viewport. Pointer/programmatic focus never delegates; eligibility is checked again on the next entry. | `spec:AST-025` FR12–FR13 | implemented |
|
|
68
74
|
|
|
69
75
|
### Transformation and precedence order
|
|
70
76
|
|
|
@@ -82,9 +88,20 @@ and scroll chaining without inserting structure.
|
|
|
82
88
|
|
|
83
89
|
## Accessibility contract
|
|
84
90
|
|
|
85
|
-
- **AR1 — Named viewport owner.**
|
|
86
|
-
|
|
87
|
-
|
|
91
|
+
- **AR1 — Named viewport owner.** Fixed viewport and automatic access require a
|
|
92
|
+
label and retain `tabIndex=0` while effectively scrollable. Automatic access
|
|
93
|
+
delegates only on forward keyboard entry; the descendant retains its own name
|
|
94
|
+
inside the named viewport group/region.
|
|
95
|
+
- **AR2 — Focus continuity.** Content, eligibility, and geometry changes never
|
|
96
|
+
move or blur focus. A viewport losing overflow while focused remains
|
|
97
|
+
programmatically focusable. Reverse traversal from the delegated first child
|
|
98
|
+
skips the viewport, including after visiting later descendants.
|
|
99
|
+
- **AR3 — Conservative eligibility.** Inspect the first sequential target; do not
|
|
100
|
+
search past an excluded one for a link/button. Composite roles, editable
|
|
101
|
+
surfaces, native or registered nested scroll owners, positive tab ordering,
|
|
102
|
+
popup triggers, and unproven interactive roles retain the viewport path.
|
|
103
|
+
- **AR4 — Native scrolling.** Delegation MUST preserve Arrow/Page access to both
|
|
104
|
+
scroll edges without implementing JavaScript scrolling.
|
|
88
105
|
|
|
89
106
|
## Design relationships
|
|
90
107
|
|
|
@@ -97,9 +114,9 @@ No visual representation is owned by this hook.
|
|
|
97
114
|
|
|
98
115
|
## Verification map
|
|
99
116
|
|
|
100
|
-
| Contract | Verification
|
|
101
|
-
| ---------------- |
|
|
102
|
-
| FR1–
|
|
117
|
+
| Contract | Verification | Representative states | Mutation or failure expectation |
|
|
118
|
+
| ---------------- | ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
119
|
+
| FR1–FR8, AR1–AR4 | `useScrollableArea.test.tsx`, `scrollKeyboardDelegation.test.ts`, and `BottomSheetKeyboard.a11y.browser.spec.ts` | horizontal/vertical/sideways, LTR/RTL, fit/overflow/edge, fixed/automatic keyboard owners, dynamic descendant eligibility, nested owners, focused loss | style-only or geometry-only ownership, physical-edge leakage, duplicate/missing keyboard path, stale state, ref loss, or dead containment fails |
|
|
103
120
|
|
|
104
121
|
## Decision log
|
|
105
122
|
|
|
@@ -111,6 +128,17 @@ No visual representation is owned by this hook.
|
|
|
111
128
|
One returned prop object per owned element keeps refs, accessibility, handlers,
|
|
112
129
|
and behavior together without making consumer spread order part of correctness.
|
|
113
130
|
|
|
131
|
+
### DEC-2 — Automatic ownership is shared behavior
|
|
132
|
+
|
|
133
|
+
**Reference:** `module:ScrollableArea/useScrollableArea/DEC-2`
|
|
134
|
+
**Decider:** cixzhang, 2026-09-13
|
|
135
|
+
|
|
136
|
+
`contentOrViewport` retains the named overflowing viewport and delegates only on
|
|
137
|
+
forward Tab entry to a safe first sequential native link/button. This replaces
|
|
138
|
+
continuous keyboard-eligibility observation. Geometry observation is unchanged;
|
|
139
|
+
keyboard eligibility adds no subtree or ancestor observers and is inspected anew
|
|
140
|
+
at each entry. BottomSheet is a proving composition of this shared behavior.
|
|
141
|
+
|
|
114
142
|
## Open questions
|
|
115
143
|
|
|
116
144
|
None.
|
|
@@ -19,6 +19,12 @@ const anatomy = [
|
|
|
19
19
|
description:
|
|
20
20
|
'Control row containing the track, thumb or thumbs, and optional text value.',
|
|
21
21
|
},
|
|
22
|
+
{
|
|
23
|
+
name: 'Interactive control',
|
|
24
|
+
required: true,
|
|
25
|
+
description:
|
|
26
|
+
'Pointer and keyboard interaction surface containing the rail, fill, marks, and thumbs.',
|
|
27
|
+
},
|
|
22
28
|
{
|
|
23
29
|
name: 'Track',
|
|
24
30
|
required: true,
|
|
@@ -235,6 +241,11 @@ export const docs = {
|
|
|
235
241
|
visualProps: ['orientation'],
|
|
236
242
|
states: ['disabled'],
|
|
237
243
|
},
|
|
244
|
+
{
|
|
245
|
+
className: 'astryx-slider-control',
|
|
246
|
+
visualProps: ['orientation'],
|
|
247
|
+
states: ['disabled'],
|
|
248
|
+
},
|
|
238
249
|
{className: 'astryx-slider-track', visualProps: ['orientation']},
|
|
239
250
|
{
|
|
240
251
|
className: 'astryx-slider-thumb',
|
|
@@ -416,6 +427,11 @@ export const docsZh = {
|
|
|
416
427
|
visualProps: ['orientation'],
|
|
417
428
|
states: ['disabled'],
|
|
418
429
|
},
|
|
430
|
+
{
|
|
431
|
+
className: 'astryx-slider-control',
|
|
432
|
+
visualProps: ['orientation'],
|
|
433
|
+
states: ['disabled'],
|
|
434
|
+
},
|
|
419
435
|
{className: 'astryx-slider-track', visualProps: ['orientation']},
|
|
420
436
|
{
|
|
421
437
|
className: 'astryx-slider-thumb',
|