@astryxdesign/core 0.1.4-canary.39ffd21 → 0.1.4-canary.3d64ef2
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/dist/Banner/Banner.d.ts.map +1 -1
- package/dist/Banner/Banner.js +8 -1
- package/dist/Calendar/Calendar.d.ts.map +1 -1
- package/dist/Calendar/Calendar.js +31 -11
- package/dist/Calendar/styles.d.ts +2 -2
- package/dist/Calendar/styles.d.ts.map +1 -1
- package/dist/Calendar/styles.js +2 -2
- package/dist/Card/Card.d.ts +4 -2
- package/dist/Card/Card.d.ts.map +1 -1
- package/dist/Card/Card.js +15 -7
- package/dist/Carousel/Carousel.d.ts.map +1 -1
- package/dist/Carousel/Carousel.js +1 -0
- package/dist/Chat/ChatToolCalls.d.ts.map +1 -1
- package/dist/Chat/ChatToolCalls.js +8 -1
- package/dist/CheckboxInput/CheckboxInput.d.ts +1 -1
- package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
- package/dist/CheckboxInput/CheckboxInput.js +3 -1
- package/dist/ClickableCard/ClickableCard.d.ts.map +1 -1
- package/dist/ClickableCard/ClickableCard.js +26 -3
- package/dist/CodeBlock/CodeBlock.d.ts +10 -1
- package/dist/CodeBlock/CodeBlock.d.ts.map +1 -1
- package/dist/CodeBlock/CodeBlock.js +23 -5
- package/dist/Collapsible/Collapsible.d.ts +9 -3
- package/dist/Collapsible/Collapsible.d.ts.map +1 -1
- package/dist/Collapsible/Collapsible.js +101 -13
- package/dist/Collapsible/CollapsibleGroup.d.ts +39 -7
- package/dist/Collapsible/CollapsibleGroup.d.ts.map +1 -1
- package/dist/Collapsible/CollapsibleGroup.js +70 -9
- package/dist/Collapsible/CollapsibleGroupContext.d.ts +31 -0
- package/dist/Collapsible/CollapsibleGroupContext.d.ts.map +1 -1
- package/dist/Collapsible/CollapsibleGroupContext.js +33 -3
- package/dist/Collapsible/index.d.ts +1 -0
- package/dist/Collapsible/index.d.ts.map +1 -1
- package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
- package/dist/ContextMenu/ContextMenu.js +2 -0
- package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
- package/dist/DateTimeInput/DateTimeInput.js +2 -0
- package/dist/Grid/Grid.d.ts +4 -3
- package/dist/Grid/Grid.d.ts.map +1 -1
- package/dist/Grid/Grid.js +26 -9
- package/dist/Icon/Icon.d.ts.map +1 -1
- package/dist/Icon/Icon.js +5 -1
- package/dist/InputGroup/InputGroup.js +2 -2
- package/dist/Item/Item.js +1 -1
- package/dist/Lightbox/Lightbox.d.ts.map +1 -1
- package/dist/Lightbox/Lightbox.js +26 -2
- package/dist/NumberInput/NumberInput.d.ts.map +1 -1
- package/dist/NumberInput/NumberInput.js +32 -16
- package/dist/Pagination/Pagination.d.ts.map +1 -1
- package/dist/Pagination/Pagination.js +87 -36
- package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearch.js +39 -21
- package/dist/ProgressBar/ProgressBar.d.ts.map +1 -1
- package/dist/ProgressBar/ProgressBar.js +2 -1
- package/dist/Resizable/ResizeHandle.d.ts.map +1 -1
- package/dist/Resizable/ResizeHandle.js +10 -2
- package/dist/SideNav/SideNav.d.ts.map +1 -1
- package/dist/SideNav/SideNav.js +6 -2
- package/dist/TabList/TabMenu.d.ts.map +1 -1
- package/dist/TabList/TabMenu.js +34 -7
- package/dist/Table/BaseTable.d.ts.map +1 -1
- package/dist/Table/BaseTable.js +14 -3
- package/dist/Table/plugins/columnResize/useTableColumnResize.d.ts.map +1 -1
- package/dist/Table/plugins/columnResize/useTableColumnResize.js +5 -2
- package/dist/Table/plugins/pagination/paginateData.d.ts.map +1 -1
- package/dist/Table/plugins/pagination/paginateData.js +5 -1
- package/dist/Table/plugins/selection/useTableSelectionState.d.ts.map +1 -1
- package/dist/Table/plugins/selection/useTableSelectionState.js +11 -1
- package/dist/Table/plugins/sortable/useTableSortableState.d.ts.map +1 -1
- package/dist/Table/plugins/sortable/useTableSortableState.js +8 -4
- package/dist/Thumbnail/Thumbnail.d.ts +3 -2
- package/dist/Thumbnail/Thumbnail.d.ts.map +1 -1
- package/dist/Thumbnail/Thumbnail.js +1 -0
- package/dist/TimeInput/TimeInput.d.ts.map +1 -1
- package/dist/TimeInput/TimeInput.js +6 -1
- package/dist/Toast/Toast.d.ts.map +1 -1
- package/dist/Toast/Toast.js +11 -3
- package/dist/Toast/ToastViewport.d.ts.map +1 -1
- package/dist/Toast/ToastViewport.js +12 -0
- package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
- package/dist/Typeahead/BaseTypeahead.js +5 -2
- package/dist/astryx.css +7 -0
- package/dist/astryx.umd.js +48 -48
- package/dist/astryx.umd.js.map +4 -4
- package/dist/hooks/focusableSelector.d.ts +1 -1
- package/dist/hooks/focusableSelector.d.ts.map +1 -1
- package/dist/hooks/focusableSelector.js +1 -1
- package/dist/hooks/useFocusTrap.d.ts +3 -0
- package/dist/hooks/useFocusTrap.d.ts.map +1 -1
- package/dist/hooks/useFocusTrap.js +38 -1
- package/dist/theme/defineTheme.d.ts +1 -1
- package/dist/theme/defineTheme.d.ts.map +1 -1
- package/dist/theme/expandColorScale.d.ts.map +1 -1
- package/dist/theme/expandColorScale.js +10 -3
- package/dist/theme/hct.d.ts +0 -15
- package/dist/theme/hct.d.ts.map +1 -1
- package/dist/theme/hct.js +9 -9
- package/dist/theme/tokens.d.ts +10 -2
- package/dist/theme/tokens.d.ts.map +1 -1
- package/dist/theme/tokens.js +220 -4
- package/dist/utils/color.d.ts +42 -0
- package/dist/utils/color.d.ts.map +1 -0
- package/dist/utils/color.js +150 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +2 -1
- package/dist/utils/timeParser.d.ts.map +1 -1
- package/dist/utils/timeParser.js +8 -5
- package/package.json +1 -1
- package/src/Avatar/Avatar.doc.mjs +11 -0
- package/src/Banner/Banner.doc.mjs +8 -0
- package/src/Banner/Banner.test.tsx +38 -0
- package/src/Banner/Banner.tsx +8 -1
- package/src/Calendar/Calendar.doc.mjs +1 -1
- package/src/Calendar/Calendar.test.tsx +112 -4
- package/src/Calendar/Calendar.tsx +26 -5
- package/src/Calendar/styles.ts +12 -2
- package/src/Card/Card.tsx +16 -6
- package/src/Carousel/Carousel.doc.mjs +12 -12
- package/src/Carousel/Carousel.test.tsx +13 -0
- package/src/Carousel/Carousel.tsx +1 -0
- package/src/Chat/Chat.doc.mjs +1 -1
- package/src/Chat/ChatToolCalls.test.tsx +55 -0
- package/src/Chat/ChatToolCalls.tsx +10 -12
- package/src/CheckboxInput/CheckboxInput.doc.mjs +3 -3
- package/src/CheckboxInput/CheckboxInput.test.tsx +71 -4
- package/src/CheckboxInput/CheckboxInput.tsx +2 -0
- package/src/Citation/Citation.doc.mjs +5 -0
- package/src/ClickableCard/ClickableCard.tsx +45 -2
- package/src/CodeBlock/CodeBlock.doc.mjs +7 -2
- package/src/CodeBlock/CodeBlock.test.tsx +87 -3
- package/src/CodeBlock/CodeBlock.tsx +43 -3
- package/src/Collapsible/Collapsible.doc.mjs +8 -4
- package/src/Collapsible/Collapsible.tsx +86 -10
- package/src/Collapsible/CollapsibleGroup.doc.mjs +27 -3
- package/src/Collapsible/CollapsibleGroup.test.tsx +288 -0
- package/src/Collapsible/CollapsibleGroup.tsx +107 -9
- package/src/Collapsible/CollapsibleGroupContext.tsx +42 -2
- package/src/Collapsible/index.ts +4 -0
- package/src/CommandPalette/CommandPaletteEmpty.doc.mjs +5 -0
- package/src/CommandPalette/CommandPaletteGroup.doc.mjs +10 -0
- package/src/CommandPalette/CommandPaletteInput.doc.mjs +6 -0
- package/src/CommandPalette/CommandPaletteItem.doc.mjs +6 -0
- package/src/CommandPalette/CommandPaletteList.doc.mjs +9 -0
- package/src/ContextMenu/ContextMenu.doc.mjs +15 -0
- package/src/ContextMenu/ContextMenu.test.tsx +15 -0
- package/src/ContextMenu/ContextMenu.tsx +2 -0
- package/src/ContextMenu/ContextMenuItem.doc.mjs +6 -0
- package/src/DateTimeInput/DateTimeInput.test.tsx +70 -0
- package/src/DateTimeInput/DateTimeInput.tsx +2 -0
- package/src/Dialog/DialogHeader.doc.mjs +45 -0
- package/src/DropdownMenu/DropdownMenu.doc.mjs +1 -1
- package/src/Field/FieldLabel.test.tsx +2 -2
- package/src/FormLayout/__snapshots__/FormLayout.test.tsx.snap +0 -3
- package/src/Grid/Grid.test.tsx +43 -18
- package/src/Grid/Grid.tsx +26 -9
- package/src/Icon/Icon.test.tsx +41 -29
- package/src/Icon/Icon.tsx +6 -2
- package/src/InputGroup/InputGroup.tsx +2 -2
- package/src/Item/Item.tsx +1 -1
- package/src/Layout/LayoutContent.doc.mjs +12 -0
- package/src/Layout/LayoutPanel.doc.mjs +40 -0
- package/src/Lightbox/Lightbox.doc.mjs +5 -0
- package/src/Lightbox/Lightbox.test.tsx +123 -2
- package/src/Lightbox/Lightbox.tsx +29 -1
- package/src/NavMenu/NavMenu.doc.mjs +15 -0
- package/src/NumberInput/NumberInput.test.tsx +48 -3
- package/src/NumberInput/NumberInput.tsx +32 -15
- package/src/Overlay/Overlay.doc.mjs +6 -0
- package/src/Pagination/Pagination.test.tsx +143 -0
- package/src/Pagination/Pagination.tsx +75 -23
- package/src/Popover/Popover.test.tsx +65 -0
- package/src/PowerSearch/PowerSearch.doc.mjs +2 -2
- package/src/PowerSearch/PowerSearch.test.tsx +109 -1
- package/src/PowerSearch/PowerSearch.tsx +37 -16
- package/src/ProgressBar/ProgressBar.test.tsx +10 -0
- package/src/ProgressBar/ProgressBar.tsx +2 -1
- package/src/Resizable/ResizeHandle.test.tsx +180 -0
- package/src/Resizable/ResizeHandle.tsx +10 -2
- package/src/SegmentedControl/SegmentedControl.doc.mjs +1 -1
- package/src/SelectableCard/SelectableCard.doc.mjs +1 -1
- package/src/SideNav/SideNav.doc.mjs +7 -1
- package/src/SideNav/SideNav.test.tsx +20 -0
- package/src/SideNav/SideNav.tsx +6 -2
- package/src/TabList/TabList.test.tsx +185 -1
- package/src/TabList/TabMenu.tsx +45 -9
- package/src/Table/BaseTable.tsx +13 -2
- package/src/Table/Table.test.tsx +51 -0
- package/src/Table/plugins/columnResize/useTableColumnResize.test.tsx +13 -0
- package/src/Table/plugins/columnResize/useTableColumnResize.tsx +3 -1
- package/src/Table/plugins/pagination/paginateData.ts +5 -1
- package/src/Table/plugins/pagination/useTablePagination.test.tsx +13 -0
- package/src/Table/plugins/selection/useTableSelectionState.test.tsx +23 -0
- package/src/Table/plugins/selection/useTableSelectionState.tsx +11 -6
- package/src/Table/plugins/sortable/useTableSortableState.test.tsx +35 -1
- package/src/Table/plugins/sortable/useTableSortableState.tsx +8 -4
- package/src/TextArea/TextArea.test.tsx +3 -3
- package/src/TextInput/TextInput.test.tsx +3 -3
- package/src/Thumbnail/Thumbnail.test.tsx +36 -2
- package/src/Thumbnail/Thumbnail.tsx +4 -2
- package/src/TimeInput/TimeInput.test.tsx +37 -0
- package/src/TimeInput/TimeInput.tsx +11 -1
- package/src/Timestamp/Timestamp.doc.mjs +1 -1
- package/src/Toast/Toast.tsx +11 -3
- package/src/Toast/ToastViewport.test.tsx +64 -0
- package/src/Toast/ToastViewport.tsx +12 -0
- package/src/ToggleButton/ToggleButton.doc.mjs +1 -0
- package/src/Tokenizer/Tokenizer.doc.mjs +2 -2
- package/src/TopNav/TopNav.doc.mjs +1 -1
- package/src/TreeList/TreeList.doc.mjs +2 -2
- package/src/Typeahead/BaseTypeahead.doc.mjs +19 -0
- package/src/Typeahead/BaseTypeahead.tsx +5 -2
- package/src/Typeahead/Typeahead.doc.mjs +1 -1
- package/src/Typeahead/Typeahead.test.tsx +45 -1
- package/src/__tests__/TestIcon.tsx +26 -0
- package/src/docPropReferences.test.ts +178 -0
- package/src/hooks/focusableSelector.ts +1 -1
- package/src/hooks/useFocusTrap.doc.mjs +4 -2
- package/src/hooks/useFocusTrap.test.tsx +149 -1
- package/src/hooks/useFocusTrap.ts +50 -1
- package/src/theme/SyntaxTheme.doc.mjs +4 -4
- package/src/theme/defineTheme.ts +4 -13
- package/src/theme/derivedVarRegistry.test.ts +2 -2
- package/src/theme/expandColorScale.test.ts +29 -0
- package/src/theme/expandColorScale.ts +11 -6
- package/src/theme/hct.ts +9 -17
- package/src/theme/tokens.test.ts +182 -9
- package/src/theme/tokens.ts +239 -4
- package/src/theme/useTheme.test.tsx +20 -0
- package/src/utils/color.test.ts +133 -0
- package/src/utils/color.ts +148 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/timeParser.test.ts +13 -0
- package/src/utils/timeParser.ts +8 -5
|
@@ -35,6 +35,7 @@ import {Icon} from '../Icon';
|
|
|
35
35
|
import {Selector} from '../Selector';
|
|
36
36
|
import {Text} from '../Text';
|
|
37
37
|
import {useAnnounce} from '../hooks/useAnnounce';
|
|
38
|
+
import {useListFocus} from '../hooks/useListFocus';
|
|
38
39
|
import {mergeProps} from '../utils';
|
|
39
40
|
import type {BaseProps} from '../BaseProps';
|
|
40
41
|
import {themeProps} from '../utils/themeProps';
|
|
@@ -370,6 +371,21 @@ export function Pagination({
|
|
|
370
371
|
// in-flight target instead of stalling on the last committed page.
|
|
371
372
|
const [optimisticPage, setOptimisticPage] = useOptimistic(page);
|
|
372
373
|
|
|
374
|
+
// Roving-tabindex + arrow/Home/End keyboard nav for the dots variant, owned
|
|
375
|
+
// by the shared useListFocus primitive (mirrors SegmentedControl). It stamps a
|
|
376
|
+
// single tab stop across the dots and moves focus horizontally; selection
|
|
377
|
+
// follows focus via handleDotsFocus so arrow keys move the active page.
|
|
378
|
+
const {
|
|
379
|
+
listRef: dotsListRef,
|
|
380
|
+
handleKeyDown: handleDotsKeyDown,
|
|
381
|
+
handleFocus: handleDotsRovingFocus,
|
|
382
|
+
} = useListFocus<HTMLDivElement>({
|
|
383
|
+
itemSelector: 'button',
|
|
384
|
+
hasRovingTabIndex: true,
|
|
385
|
+
wrap: true,
|
|
386
|
+
orientation: 'horizontal',
|
|
387
|
+
});
|
|
388
|
+
|
|
373
389
|
// Compute pagination state
|
|
374
390
|
const computedTotalPages =
|
|
375
391
|
totalPagesProp ??
|
|
@@ -410,6 +426,28 @@ export function Pagination({
|
|
|
410
426
|
});
|
|
411
427
|
};
|
|
412
428
|
|
|
429
|
+
// Selection-follows-focus for the dots (APG radiogroup pattern): useListFocus
|
|
430
|
+
// only *moves* focus, so when focus lands on a dot -- via arrow/Home/End, or a
|
|
431
|
+
// click that focuses it -- we select that dot's page. handleDotsRovingFocus
|
|
432
|
+
// keeps the roving tab stop in sync. The current page is skipped so tabbing
|
|
433
|
+
// into the group is a no-op.
|
|
434
|
+
const handleDotsFocus = (e: React.FocusEvent) => {
|
|
435
|
+
handleDotsRovingFocus(e);
|
|
436
|
+
if (isDisabled) {
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
const focused = (e.target as HTMLElement | null)?.closest<HTMLElement>(
|
|
440
|
+
'button[data-page]',
|
|
441
|
+
);
|
|
442
|
+
if (!focused) {
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
const nextPage = Number(focused.dataset.page);
|
|
446
|
+
if (Number.isFinite(nextPage) && nextPage !== optimisticPage) {
|
|
447
|
+
handlePageChange(nextPage);
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
|
|
413
451
|
const handlePrevious = () => {
|
|
414
452
|
if (hasPrevious) {
|
|
415
453
|
handlePageChange(optimisticPage - 1);
|
|
@@ -518,33 +556,47 @@ export function Pagination({
|
|
|
518
556
|
if (computedTotalPages == null) {
|
|
519
557
|
return null;
|
|
520
558
|
}
|
|
559
|
+
|
|
521
560
|
return (
|
|
522
561
|
<div
|
|
562
|
+
ref={dotsListRef}
|
|
523
563
|
{...stylex.props(styles.dotsContainer)}
|
|
524
564
|
role="group"
|
|
525
|
-
aria-label="Page indicators"
|
|
526
|
-
{
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
565
|
+
aria-label="Page indicators"
|
|
566
|
+
onKeyDown={handleDotsKeyDown}
|
|
567
|
+
onFocus={handleDotsFocus}>
|
|
568
|
+
{Array.from({length: computedTotalPages}, (_, i) => {
|
|
569
|
+
const isActive = i + 1 === optimisticPage;
|
|
570
|
+
return (
|
|
571
|
+
<button
|
|
572
|
+
key={i + 1}
|
|
573
|
+
type="button"
|
|
574
|
+
data-page={i + 1}
|
|
575
|
+
aria-label={`Go to page ${i + 1}`}
|
|
576
|
+
aria-current={isActive ? 'page' : undefined}
|
|
577
|
+
// The active dot is the single roving tab stop; useListFocus
|
|
578
|
+
// maintains it as focus and the active page move.
|
|
579
|
+
tabIndex={isActive ? 0 : -1}
|
|
580
|
+
// Selection is driven by focus (handleDotsFocus); clicking only
|
|
581
|
+
// needs to focus the dot, which some browsers (Safari) skip for
|
|
582
|
+
// buttons, so focus it explicitly.
|
|
583
|
+
onClick={e => e.currentTarget.focus()}
|
|
584
|
+
disabled={isDisabled}
|
|
585
|
+
{...mergeProps(
|
|
586
|
+
themeProps('pagination-dot', {
|
|
587
|
+
active: isActive ? 'active' : null,
|
|
588
|
+
size,
|
|
589
|
+
}),
|
|
590
|
+
stylex.props(
|
|
591
|
+
styles.dot,
|
|
592
|
+
isSm && styles.dotSm,
|
|
593
|
+
isActive && styles.dotActive,
|
|
594
|
+
isDisabled && styles.dotDisabled,
|
|
595
|
+
),
|
|
596
|
+
)}
|
|
597
|
+
/>
|
|
598
|
+
);
|
|
599
|
+
})}
|
|
548
600
|
</div>
|
|
549
601
|
);
|
|
550
602
|
}
|
|
@@ -354,4 +354,69 @@ describe('Popover', () => {
|
|
|
354
354
|
expect(trigger).toHaveAttribute('aria-expanded', 'true');
|
|
355
355
|
});
|
|
356
356
|
});
|
|
357
|
+
|
|
358
|
+
describe('focus restoration', () => {
|
|
359
|
+
it('returns focus to the trigger when closed via Escape', () => {
|
|
360
|
+
render(
|
|
361
|
+
<Popover
|
|
362
|
+
content={
|
|
363
|
+
<button type="button" data-testid="inside-content">
|
|
364
|
+
Inside
|
|
365
|
+
</button>
|
|
366
|
+
}
|
|
367
|
+
label="Test">
|
|
368
|
+
<button type="button">Open</button>
|
|
369
|
+
</Popover>,
|
|
370
|
+
);
|
|
371
|
+
const trigger = screen.getByRole('button', {name: 'Open'});
|
|
372
|
+
trigger.focus();
|
|
373
|
+
expect(trigger).toHaveFocus();
|
|
374
|
+
|
|
375
|
+
fireEvent.click(trigger);
|
|
376
|
+
expect(trigger).toHaveAttribute('aria-expanded', 'true');
|
|
377
|
+
|
|
378
|
+
// Move focus into the open popover, as a keyboard user would.
|
|
379
|
+
const inside = screen.getByTestId('inside-content');
|
|
380
|
+
inside.focus();
|
|
381
|
+
expect(inside).toHaveFocus();
|
|
382
|
+
|
|
383
|
+
fireEvent.keyDown(document, {key: 'Escape'});
|
|
384
|
+
expect(trigger).toHaveAttribute('aria-expanded', 'false');
|
|
385
|
+
expect(trigger).toHaveFocus();
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it('returns focus to the trigger on light dismiss', () => {
|
|
389
|
+
render(
|
|
390
|
+
<Popover
|
|
391
|
+
content={
|
|
392
|
+
<button type="button" data-testid="inside-content">
|
|
393
|
+
Inside
|
|
394
|
+
</button>
|
|
395
|
+
}
|
|
396
|
+
label="Test">
|
|
397
|
+
<button type="button">Open</button>
|
|
398
|
+
</Popover>,
|
|
399
|
+
);
|
|
400
|
+
const trigger = screen.getByRole('button', {name: 'Open'});
|
|
401
|
+
trigger.focus();
|
|
402
|
+
|
|
403
|
+
fireEvent.click(trigger);
|
|
404
|
+
expect(trigger).toHaveAttribute('aria-expanded', 'true');
|
|
405
|
+
|
|
406
|
+
const inside = screen.getByTestId('inside-content');
|
|
407
|
+
inside.focus();
|
|
408
|
+
expect(inside).toHaveFocus();
|
|
409
|
+
|
|
410
|
+
// Simulate the browser's light dismiss for popover="auto": clicking
|
|
411
|
+
// outside fires a `toggle` event with newState "closed".
|
|
412
|
+
const popoverEl = document.querySelector('[popover]');
|
|
413
|
+
expect(popoverEl).not.toBeNull();
|
|
414
|
+
const toggleEvent = new Event('toggle');
|
|
415
|
+
Object.defineProperty(toggleEvent, 'newState', {value: 'closed'});
|
|
416
|
+
fireEvent(popoverEl as HTMLElement, toggleEvent);
|
|
417
|
+
|
|
418
|
+
expect(trigger).toHaveAttribute('aria-expanded', 'false');
|
|
419
|
+
expect(trigger).toHaveFocus();
|
|
420
|
+
});
|
|
421
|
+
});
|
|
357
422
|
});
|
|
@@ -120,7 +120,7 @@ export const docs = {
|
|
|
120
120
|
name: 'resultCount',
|
|
121
121
|
type: 'number | string',
|
|
122
122
|
description:
|
|
123
|
-
'Number of results matching the current filters. When a number, formatted as "N results". When a string, displayed as-is.',
|
|
123
|
+
'Number of results matching the current filters. When a number, formatted as "N results". When a string, displayed as-is. Changes are announced to screen readers via a polite live region.',
|
|
124
124
|
},
|
|
125
125
|
{
|
|
126
126
|
name: 'size',
|
|
@@ -261,7 +261,7 @@ export const docsZh = {
|
|
|
261
261
|
name: 'resultCount',
|
|
262
262
|
type: 'number | string',
|
|
263
263
|
description:
|
|
264
|
-
'匹配当前过滤器的结果数量。数字类型时格式化为"N results"
|
|
264
|
+
'匹配当前过滤器的结果数量。数字类型时格式化为"N results"。字符串类型时按原样显示。数量变化会通过 polite 实时区域向屏幕阅读器播报。',
|
|
265
265
|
},
|
|
266
266
|
{
|
|
267
267
|
name: 'size',
|
|
@@ -10,10 +10,11 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import {useState} from 'react';
|
|
13
|
-
import {describe, it, expect, vi, beforeAll, afterAll} from 'vitest';
|
|
13
|
+
import {describe, it, expect, vi, beforeAll, afterAll, afterEach} from 'vitest';
|
|
14
14
|
import {render, screen, act, fireEvent, waitFor} from '@testing-library/react';
|
|
15
15
|
import userEvent from '@testing-library/user-event';
|
|
16
16
|
import {PowerSearch} from './PowerSearch';
|
|
17
|
+
import {__resetLiveRegionsForTest} from '../hooks/useAnnounce';
|
|
17
18
|
import type {PowerSearchConfig, PowerSearchFilter} from './types';
|
|
18
19
|
|
|
19
20
|
// =============================================================================
|
|
@@ -59,6 +60,12 @@ afterAll(() => {
|
|
|
59
60
|
(HTMLElement.prototype as any).matches = originalMatches;
|
|
60
61
|
});
|
|
61
62
|
|
|
63
|
+
// Reset the singleton live regions between tests so result-count
|
|
64
|
+
// announcements from one test don't leak into the next.
|
|
65
|
+
afterEach(() => {
|
|
66
|
+
__resetLiveRegionsForTest();
|
|
67
|
+
});
|
|
68
|
+
|
|
62
69
|
// =============================================================================
|
|
63
70
|
// Fixtures
|
|
64
71
|
// =============================================================================
|
|
@@ -295,4 +302,105 @@ describe('PowerSearch', () => {
|
|
|
295
302
|
expect(screen.getByRole('combobox')).toBeDisabled();
|
|
296
303
|
});
|
|
297
304
|
});
|
|
305
|
+
|
|
306
|
+
describe('result count announcements', () => {
|
|
307
|
+
const politeRegion = () =>
|
|
308
|
+
document.querySelector('[data-astryx-live-region="polite"]');
|
|
309
|
+
|
|
310
|
+
it('announces the result count to a polite live region when it changes', async () => {
|
|
311
|
+
const {rerender} = render(
|
|
312
|
+
<PowerSearch
|
|
313
|
+
config={config}
|
|
314
|
+
filters={[]}
|
|
315
|
+
onChange={() => {}}
|
|
316
|
+
resultCount={0}
|
|
317
|
+
/>,
|
|
318
|
+
);
|
|
319
|
+
rerender(
|
|
320
|
+
<PowerSearch
|
|
321
|
+
config={config}
|
|
322
|
+
filters={[]}
|
|
323
|
+
onChange={() => {}}
|
|
324
|
+
resultCount={5}
|
|
325
|
+
/>,
|
|
326
|
+
);
|
|
327
|
+
await waitFor(() => {
|
|
328
|
+
expect(politeRegion()).toHaveTextContent('5 results');
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('announces "1 result" (singular) for a single match', async () => {
|
|
333
|
+
const {rerender} = render(
|
|
334
|
+
<PowerSearch
|
|
335
|
+
config={config}
|
|
336
|
+
filters={[]}
|
|
337
|
+
onChange={() => {}}
|
|
338
|
+
resultCount={0}
|
|
339
|
+
/>,
|
|
340
|
+
);
|
|
341
|
+
rerender(
|
|
342
|
+
<PowerSearch
|
|
343
|
+
config={config}
|
|
344
|
+
filters={[]}
|
|
345
|
+
onChange={() => {}}
|
|
346
|
+
resultCount={1}
|
|
347
|
+
/>,
|
|
348
|
+
);
|
|
349
|
+
await waitFor(() => {
|
|
350
|
+
expect(politeRegion()).toHaveTextContent('1 result');
|
|
351
|
+
});
|
|
352
|
+
expect(politeRegion()?.textContent).not.toMatch(/results/);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('announces a string result count verbatim', async () => {
|
|
356
|
+
const {rerender} = render(
|
|
357
|
+
<PowerSearch
|
|
358
|
+
config={config}
|
|
359
|
+
filters={[]}
|
|
360
|
+
onChange={() => {}}
|
|
361
|
+
resultCount="0 items"
|
|
362
|
+
/>,
|
|
363
|
+
);
|
|
364
|
+
rerender(
|
|
365
|
+
<PowerSearch
|
|
366
|
+
config={config}
|
|
367
|
+
filters={[]}
|
|
368
|
+
onChange={() => {}}
|
|
369
|
+
resultCount="Showing 1.2k matches"
|
|
370
|
+
/>,
|
|
371
|
+
);
|
|
372
|
+
await waitFor(() => {
|
|
373
|
+
expect(politeRegion()).toHaveTextContent('Showing 1.2k matches');
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it('does not announce the result count present on initial mount', async () => {
|
|
378
|
+
render(
|
|
379
|
+
<PowerSearch
|
|
380
|
+
config={config}
|
|
381
|
+
filters={[]}
|
|
382
|
+
onChange={() => {}}
|
|
383
|
+
resultCount={42}
|
|
384
|
+
/>,
|
|
385
|
+
);
|
|
386
|
+
// Flush effects and any pending live-region rAF writes.
|
|
387
|
+
await act(async () => {
|
|
388
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
389
|
+
});
|
|
390
|
+
expect(politeRegion()?.textContent ?? '').not.toContain('42');
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it('leaves Typeahead dropdown announcements intact and stays silent when no resultCount is set', async () => {
|
|
394
|
+
const user = userEvent.setup();
|
|
395
|
+
render(<PowerSearchWrapper config={config} />);
|
|
396
|
+
const input = screen.getByRole('combobox');
|
|
397
|
+
await user.click(input);
|
|
398
|
+
await user.type(input, 'Status');
|
|
399
|
+
// BaseTypeahead announces the dropdown suggestion count; PowerSearch adds
|
|
400
|
+
// no result-count announcement because resultCount is unset.
|
|
401
|
+
await waitFor(() => {
|
|
402
|
+
expect(politeRegion()?.textContent).toMatch(/\d+ results?/);
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
});
|
|
298
406
|
});
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
import React, {
|
|
18
18
|
useCallback,
|
|
19
|
+
useEffect,
|
|
19
20
|
useImperativeHandle,
|
|
20
21
|
useMemo,
|
|
21
22
|
useRef,
|
|
@@ -38,6 +39,7 @@ import type {IconType} from '../Icon';
|
|
|
38
39
|
import type {IconName} from '../Icon/globalIconRegistry';
|
|
39
40
|
import type {InputStatus} from '../Field';
|
|
40
41
|
import {usePopover} from '../Popover/usePopover';
|
|
42
|
+
import {useAnnounce} from '../hooks/useAnnounce';
|
|
41
43
|
import {
|
|
42
44
|
spacingVars,
|
|
43
45
|
colorVars,
|
|
@@ -945,23 +947,42 @@ export function PowerSearch({
|
|
|
945
947
|
isReadOnly,
|
|
946
948
|
]);
|
|
947
949
|
|
|
950
|
+
// Plain-text form of the result count, shared by the visible label and the
|
|
951
|
+
// screen-reader announcement so the two never drift.
|
|
952
|
+
const resultCountText = useMemo((): string | null => {
|
|
953
|
+
if (resultCount == null) {
|
|
954
|
+
return null;
|
|
955
|
+
}
|
|
956
|
+
if (typeof resultCount === 'number') {
|
|
957
|
+
const formatted = new Intl.NumberFormat().format(resultCount);
|
|
958
|
+
return `${formatted} ${resultCount === 1 ? 'result' : 'results'}`;
|
|
959
|
+
}
|
|
960
|
+
return resultCount;
|
|
961
|
+
}, [resultCount]);
|
|
962
|
+
|
|
963
|
+
// Announce result-count changes to screen readers through a polite live
|
|
964
|
+
// region, mirroring the way Typeahead announces its dropdown result count.
|
|
965
|
+
// The count is otherwise only shown visually and stays silent to assistive
|
|
966
|
+
// tech. Skip the first run so the count already present on mount isn't
|
|
967
|
+
// announced unprompted — only user-driven changes are spoken.
|
|
968
|
+
const announce = useAnnounce();
|
|
969
|
+
const hasMountedRef = useRef(false);
|
|
970
|
+
useEffect(() => {
|
|
971
|
+
if (!hasMountedRef.current) {
|
|
972
|
+
hasMountedRef.current = true;
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
if (resultCountText != null) {
|
|
976
|
+
announce(resultCountText);
|
|
977
|
+
}
|
|
978
|
+
}, [resultCountText, announce]);
|
|
979
|
+
|
|
948
980
|
// Build combined endContent from resultCount + endContent props
|
|
949
981
|
const combinedEndContent = useMemo((): React.ReactNode => {
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
resultCountNode = (
|
|
955
|
-
<span {...stylex.props(resultCountStyles.text)}>
|
|
956
|
-
{formatted} {resultCount === 1 ? 'result' : 'results'}
|
|
957
|
-
</span>
|
|
958
|
-
);
|
|
959
|
-
} else {
|
|
960
|
-
resultCountNode = (
|
|
961
|
-
<span {...stylex.props(resultCountStyles.text)}>{resultCount}</span>
|
|
962
|
-
);
|
|
963
|
-
}
|
|
964
|
-
}
|
|
982
|
+
const resultCountNode =
|
|
983
|
+
resultCountText != null ? (
|
|
984
|
+
<span {...stylex.props(resultCountStyles.text)}>{resultCountText}</span>
|
|
985
|
+
) : null;
|
|
965
986
|
|
|
966
987
|
if (resultCountNode && endContent) {
|
|
967
988
|
return (
|
|
@@ -972,7 +993,7 @@ export function PowerSearch({
|
|
|
972
993
|
);
|
|
973
994
|
}
|
|
974
995
|
return resultCountNode || endContent || undefined;
|
|
975
|
-
}, [
|
|
996
|
+
}, [resultCountText, endContent]);
|
|
976
997
|
|
|
977
998
|
return (
|
|
978
999
|
<>
|
|
@@ -140,6 +140,16 @@ describe('ProgressBar', () => {
|
|
|
140
140
|
expect(progressbar).toHaveAttribute('aria-valuemax', '0');
|
|
141
141
|
});
|
|
142
142
|
|
|
143
|
+
it('does not render NaN in the value label when max is zero', () => {
|
|
144
|
+
render(<ProgressBar value={0} max={0} label="Empty" hasValueLabel />);
|
|
145
|
+
expect(screen.queryByText(/NaN|Infinity/)).not.toBeInTheDocument();
|
|
146
|
+
const progressbar = screen.getByRole('progressbar');
|
|
147
|
+
expect(progressbar.getAttribute('aria-valuetext') ?? '').not.toMatch(
|
|
148
|
+
/NaN|Infinity/,
|
|
149
|
+
);
|
|
150
|
+
expect(screen.getByText('0%')).toBeInTheDocument();
|
|
151
|
+
});
|
|
152
|
+
|
|
143
153
|
// Disabled state
|
|
144
154
|
describe('disabled state', () => {
|
|
145
155
|
it('renders with isDisabled', () => {
|
|
@@ -228,7 +228,8 @@ const variantStyles = stylex.create({
|
|
|
228
228
|
});
|
|
229
229
|
|
|
230
230
|
function defaultFormatValueLabel(value: number, max: number): string {
|
|
231
|
-
|
|
231
|
+
const pct = max > 0 ? Math.round((value / max) * 100) : 0;
|
|
232
|
+
return `${pct}%`;
|
|
232
233
|
}
|
|
233
234
|
|
|
234
235
|
/**
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file ResizeHandle.test.tsx
|
|
5
|
+
* @input Uses vitest, @testing-library/react, useResizable, ResizeHandle
|
|
6
|
+
* @output Unit tests for ResizeHandle keyboard operability and ARIA state
|
|
7
|
+
* @position Testing; validates ResizeHandle.tsx implementation
|
|
8
|
+
*
|
|
9
|
+
* SYNC: When ResizeHandle.tsx changes, update tests to match new behavior
|
|
10
|
+
*
|
|
11
|
+
* These tests guard the WAI-ARIA window-splitter keyboard contract: the
|
|
12
|
+
* keydown handler must live on the focusable `role="separator"` element.
|
|
13
|
+
* Keyboard resizing is pure state math in useResizable (no layout
|
|
14
|
+
* measurement), so the observable effect asserted here is aria-valuenow,
|
|
15
|
+
* which is bound to the region's `_size`.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import {describe, it, expect, vi} from 'vitest';
|
|
19
|
+
import {render, screen, fireEvent, act} from '@testing-library/react';
|
|
20
|
+
import {ResizeHandle} from './ResizeHandle';
|
|
21
|
+
import type {ResizeHandleProps} from './ResizeHandle';
|
|
22
|
+
import {useResizable} from './useResizable';
|
|
23
|
+
import type {UseResizableSingleConfig} from './useResizable';
|
|
24
|
+
|
|
25
|
+
const KEYBOARD_STEP = 10;
|
|
26
|
+
const KEYBOARD_LARGE_STEP = 50;
|
|
27
|
+
|
|
28
|
+
function Harness({
|
|
29
|
+
config,
|
|
30
|
+
handleProps,
|
|
31
|
+
}: {
|
|
32
|
+
config?: UseResizableSingleConfig;
|
|
33
|
+
handleProps?: Partial<ResizeHandleProps>;
|
|
34
|
+
}) {
|
|
35
|
+
const region = useResizable(
|
|
36
|
+
config ?? {defaultSize: 200, minSizePx: 100, maxSizePx: 400},
|
|
37
|
+
);
|
|
38
|
+
return (
|
|
39
|
+
<ResizeHandle resizable={region.props} label="Resize" {...handleProps} />
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getSeparator(): HTMLElement {
|
|
44
|
+
return screen.getByRole('separator');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe('ResizeHandle', () => {
|
|
48
|
+
// --- ARIA wiring ---
|
|
49
|
+
|
|
50
|
+
it('exposes the region size and bounds via ARIA', () => {
|
|
51
|
+
render(<Harness />);
|
|
52
|
+
const separator = getSeparator();
|
|
53
|
+
expect(separator).toHaveAttribute('aria-valuenow', '200');
|
|
54
|
+
expect(separator).toHaveAttribute('aria-valuemin', '100');
|
|
55
|
+
expect(separator).toHaveAttribute('aria-valuemax', '400');
|
|
56
|
+
// Horizontal layout splits along the vertical axis.
|
|
57
|
+
expect(separator).toHaveAttribute('aria-orientation', 'vertical');
|
|
58
|
+
expect(separator).toHaveAttribute('aria-label', 'Resize');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('makes the separator focusable', () => {
|
|
62
|
+
render(<Harness />);
|
|
63
|
+
const separator = getSeparator();
|
|
64
|
+
expect(separator).toHaveAttribute('tabindex', '0');
|
|
65
|
+
act(() => separator.focus());
|
|
66
|
+
expect(separator).toHaveFocus();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// --- Keyboard resizing (the fix: handler lives on the focused separator) ---
|
|
70
|
+
|
|
71
|
+
it('grows the panel by a step on ArrowRight', () => {
|
|
72
|
+
render(<Harness />);
|
|
73
|
+
const separator = getSeparator();
|
|
74
|
+
act(() => separator.focus());
|
|
75
|
+
fireEvent.keyDown(separator, {key: 'ArrowRight'});
|
|
76
|
+
expect(separator).toHaveAttribute(
|
|
77
|
+
'aria-valuenow',
|
|
78
|
+
String(200 + KEYBOARD_STEP),
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('shrinks the panel by a step on ArrowLeft', () => {
|
|
83
|
+
render(<Harness />);
|
|
84
|
+
const separator = getSeparator();
|
|
85
|
+
act(() => separator.focus());
|
|
86
|
+
fireEvent.keyDown(separator, {key: 'ArrowLeft'});
|
|
87
|
+
expect(separator).toHaveAttribute(
|
|
88
|
+
'aria-valuenow',
|
|
89
|
+
String(200 - KEYBOARD_STEP),
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('uses the large step when Shift is held', () => {
|
|
94
|
+
render(<Harness />);
|
|
95
|
+
const separator = getSeparator();
|
|
96
|
+
act(() => separator.focus());
|
|
97
|
+
fireEvent.keyDown(separator, {key: 'ArrowRight', shiftKey: true});
|
|
98
|
+
expect(separator).toHaveAttribute(
|
|
99
|
+
'aria-valuenow',
|
|
100
|
+
String(200 + KEYBOARD_LARGE_STEP),
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('jumps to the minimum on Home', () => {
|
|
105
|
+
render(<Harness />);
|
|
106
|
+
const separator = getSeparator();
|
|
107
|
+
act(() => separator.focus());
|
|
108
|
+
fireEvent.keyDown(separator, {key: 'Home'});
|
|
109
|
+
expect(separator).toHaveAttribute('aria-valuenow', '100');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('jumps to the maximum on End', () => {
|
|
113
|
+
render(<Harness />);
|
|
114
|
+
const separator = getSeparator();
|
|
115
|
+
act(() => separator.focus());
|
|
116
|
+
fireEvent.keyDown(separator, {key: 'End'});
|
|
117
|
+
expect(separator).toHaveAttribute('aria-valuenow', '400');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('resizes along the block axis for a vertical handle', () => {
|
|
121
|
+
render(
|
|
122
|
+
<Harness
|
|
123
|
+
config={{defaultSize: 200, minSizePx: 100, maxSizePx: 400}}
|
|
124
|
+
handleProps={{direction: 'vertical'}}
|
|
125
|
+
/>,
|
|
126
|
+
);
|
|
127
|
+
const separator = getSeparator();
|
|
128
|
+
expect(separator).toHaveAttribute('aria-orientation', 'horizontal');
|
|
129
|
+
act(() => separator.focus());
|
|
130
|
+
fireEvent.keyDown(separator, {key: 'ArrowDown'});
|
|
131
|
+
expect(separator).toHaveAttribute(
|
|
132
|
+
'aria-valuenow',
|
|
133
|
+
String(200 + KEYBOARD_STEP),
|
|
134
|
+
);
|
|
135
|
+
fireEvent.keyDown(separator, {key: 'ArrowUp'});
|
|
136
|
+
expect(separator).toHaveAttribute('aria-valuenow', '200');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('collapses on Enter when the region is collapsible', () => {
|
|
140
|
+
render(
|
|
141
|
+
<Harness
|
|
142
|
+
config={{
|
|
143
|
+
defaultSize: 200,
|
|
144
|
+
minSizePx: 100,
|
|
145
|
+
maxSizePx: 400,
|
|
146
|
+
collapsible: true,
|
|
147
|
+
}}
|
|
148
|
+
/>,
|
|
149
|
+
);
|
|
150
|
+
const separator = getSeparator();
|
|
151
|
+
act(() => separator.focus());
|
|
152
|
+
fireEvent.keyDown(separator, {key: 'Enter'});
|
|
153
|
+
expect(separator).toHaveAttribute('aria-valuenow', '0');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// --- Disabled guard ---
|
|
157
|
+
|
|
158
|
+
it('ignores keyboard input when disabled', () => {
|
|
159
|
+
render(<Harness handleProps={{isDisabled: true}} />);
|
|
160
|
+
const separator = getSeparator();
|
|
161
|
+
expect(separator).toHaveAttribute('tabindex', '-1');
|
|
162
|
+
fireEvent.keyDown(separator, {key: 'ArrowRight'});
|
|
163
|
+
expect(separator).toHaveAttribute('aria-valuenow', '200');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// --- Prop composition (ordering choice: handler sits after {...props}) ---
|
|
167
|
+
|
|
168
|
+
it('runs a consumer onKeyDown alongside keyboard resizing', () => {
|
|
169
|
+
const onKeyDown = vi.fn();
|
|
170
|
+
render(<Harness handleProps={{onKeyDown}} />);
|
|
171
|
+
const separator = getSeparator();
|
|
172
|
+
act(() => separator.focus());
|
|
173
|
+
fireEvent.keyDown(separator, {key: 'ArrowRight'});
|
|
174
|
+
expect(onKeyDown).toHaveBeenCalledTimes(1);
|
|
175
|
+
expect(separator).toHaveAttribute(
|
|
176
|
+
'aria-valuenow',
|
|
177
|
+
String(200 + KEYBOARD_STEP),
|
|
178
|
+
);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
@@ -510,7 +510,16 @@ export function ResizeHandle({
|
|
|
510
510
|
),
|
|
511
511
|
className,
|
|
512
512
|
)}
|
|
513
|
-
{...props}
|
|
513
|
+
{...props}
|
|
514
|
+
// Keyboard resizing must fire from the focusable separator, not a child:
|
|
515
|
+
// keydown fires on the focused element and bubbles up, so a handler on a
|
|
516
|
+
// descendant never runs (per the WAI-ARIA window-splitter pattern).
|
|
517
|
+
// Placed after {...props} and composed with any consumer onKeyDown so
|
|
518
|
+
// this accessibility-critical handler can't be clobbered by a passed prop.
|
|
519
|
+
onKeyDown={e => {
|
|
520
|
+
props.onKeyDown?.(e);
|
|
521
|
+
handleKeyDown(e);
|
|
522
|
+
}}>
|
|
514
523
|
{/* Wider invisible hit area for pointer interaction */}
|
|
515
524
|
<div
|
|
516
525
|
{...stylex.props(
|
|
@@ -528,7 +537,6 @@ export function ResizeHandle({
|
|
|
528
537
|
setIsHovered(false);
|
|
529
538
|
}
|
|
530
539
|
}}
|
|
531
|
-
onKeyDown={handleKeyDown}
|
|
532
540
|
/>
|
|
533
541
|
{/* Pill grip indicator — themed via .astryx-resize-handle-pill */}
|
|
534
542
|
{children ?? (
|
|
@@ -16,7 +16,7 @@ export const docs = {
|
|
|
16
16
|
theming: {
|
|
17
17
|
targets: [
|
|
18
18
|
{className: 'astryx-segmented-control', visualProps: ['size']},
|
|
19
|
-
{className: 'astryx-segmented-control-item'},
|
|
19
|
+
{className: 'astryx-segmented-control-item', visualProps: ['size'], states: ['selected', 'disabled']},
|
|
20
20
|
],
|
|
21
21
|
vars: [
|
|
22
22
|
{name: '--_segmented-control-radius', description: 'Border radius of the segmented control', default: 'var(--radius-element)', private: true},
|