@astryxdesign/core 0.7.0-canary.fe57f54 → 0.7.0

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.
Files changed (125) hide show
  1. package/CHANGELOG.md +186 -0
  2. package/dist/BottomSheet/BottomSheet.d.ts +5 -4
  3. package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
  4. package/dist/BottomSheet/BottomSheetPanel.d.ts +1 -1
  5. package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
  6. package/dist/BottomSheet/BottomSheetPanel.js +1 -1
  7. package/dist/Calendar/styles.d.ts +6 -6
  8. package/dist/Chat/ChatComposerInput.d.ts +0 -11
  9. package/dist/Chat/ChatComposerInput.d.ts.map +1 -1
  10. package/dist/Chat/ChatComposerInput.js +0 -11
  11. package/dist/Chat/useTriggerMenu.d.ts.map +1 -1
  12. package/dist/Chat/useTriggerMenu.js +1 -1
  13. package/dist/CommandPalette/CommandPaletteList.d.ts +3 -2
  14. package/dist/CommandPalette/CommandPaletteList.d.ts.map +1 -1
  15. package/dist/CommandPalette/CommandPaletteList.js +3 -2
  16. package/dist/DateInput/TouchDateField.d.ts.map +1 -1
  17. package/dist/DateInput/TouchDateField.js +4 -1
  18. package/dist/DateTimeInput/TouchDateTimeField.d.ts.map +1 -1
  19. package/dist/DateTimeInput/TouchDateTimeField.js +29 -13
  20. package/dist/DropdownMenu/DropdownMenu.d.ts +11 -0
  21. package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
  22. package/dist/DropdownMenu/DropdownMenu.js +173 -11
  23. package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
  24. package/dist/DropdownMenu/DropdownMenuSubMenu.js +13 -2
  25. package/dist/DropdownMenu/menuRegionsContext.d.ts +2 -0
  26. package/dist/DropdownMenu/menuRegionsContext.d.ts.map +1 -0
  27. package/dist/DropdownMenu/menuRegionsContext.js +17 -0
  28. package/dist/Field/inputStyles.stylex.d.ts +3 -3
  29. package/dist/Layout/container.stylex.d.ts +18 -20
  30. package/dist/Layout/container.stylex.d.ts.map +1 -1
  31. package/dist/Layout/container.stylex.js +15 -25
  32. package/dist/Layout/padding.stylex.d.ts +231 -231
  33. package/dist/Lightbox/Lightbox.d.ts.map +1 -1
  34. package/dist/Lightbox/Lightbox.js +15 -6
  35. package/dist/Markdown/plugins/protocol.d.ts.map +1 -1
  36. package/dist/Markdown/plugins/protocol.js +33 -8
  37. package/dist/NavItem/navItemStyles.stylex.d.ts +5 -5
  38. package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
  39. package/dist/PowerSearch/PowerSearch.js +21 -1
  40. package/dist/PowerSearch/PowerSearchEditPopover.d.ts.map +1 -1
  41. package/dist/PowerSearch/PowerSearchEditPopover.js +30 -15
  42. package/dist/Stack/stack.stylex.d.ts +22 -22
  43. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts.map +1 -1
  44. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.js +89 -14
  45. package/dist/Table/table.stylex.d.ts +6 -6
  46. package/dist/TimeInput/TouchTimeField.d.ts.map +1 -1
  47. package/dist/TimeInput/TouchTimeField.js +4 -1
  48. package/dist/Tokenizer/Tokenizer.d.ts +1 -12
  49. package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
  50. package/dist/Tokenizer/Tokenizer.js +1 -13
  51. package/dist/Typeahead/BaseTypeahead.d.ts +0 -11
  52. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  53. package/dist/Typeahead/BaseTypeahead.js +1 -9
  54. package/dist/Typeahead/Typeahead.d.ts +1 -12
  55. package/dist/Typeahead/Typeahead.d.ts.map +1 -1
  56. package/dist/Typeahead/Typeahead.js +1 -13
  57. package/dist/astryx.css +17 -12
  58. package/dist/hooks/useFocusTrap.d.ts +8 -0
  59. package/dist/hooks/useFocusTrap.d.ts.map +1 -1
  60. package/dist/hooks/useFocusTrap.js +5 -1
  61. package/dist/hooks/useRenamedProp.d.ts +6 -6
  62. package/dist/hooks/useRenamedProp.js +6 -6
  63. package/dist/reset.a11y.chromium.spec.d.ts +2 -0
  64. package/dist/reset.a11y.chromium.spec.d.ts.map +1 -0
  65. package/dist/reset.a11y.chromium.spec.js +176 -0
  66. package/dist/theme/tokens.stylex.d.ts +30 -30
  67. package/dist/theme/tokens.stylex.d.ts.map +1 -1
  68. package/dist/theme/tokens.stylex.js +18 -15
  69. package/dist/theme/useTheme.d.ts +1 -1
  70. package/package.json +4 -4
  71. package/src/BottomSheet/BottomSheet.doc.mjs +4 -4
  72. package/src/BottomSheet/BottomSheet.tsx +5 -4
  73. package/src/BottomSheet/BottomSheetPanel.test.tsx +6 -9
  74. package/src/BottomSheet/BottomSheetPanel.tsx +2 -3
  75. package/src/Chat/ChatComposerInput.doc.mjs +3 -3
  76. package/src/Chat/ChatComposerInput.test.tsx +7 -55
  77. package/src/Chat/ChatComposerInput.tsx +0 -36
  78. package/src/Chat/useTriggerMenu.tsx +1 -4
  79. package/src/CommandPalette/CommandPalette.spec.md +2 -0
  80. package/src/CommandPalette/CommandPaletteList.spec.md +189 -0
  81. package/src/CommandPalette/CommandPaletteList.test.tsx +47 -0
  82. package/src/CommandPalette/CommandPaletteList.tsx +3 -2
  83. package/src/DateInput/TouchDateField.tsx +3 -1
  84. package/src/DateTimeInput/DateTimeInputTouch.test.tsx +60 -1
  85. package/src/DateTimeInput/TouchDateTimeField.tsx +31 -14
  86. package/src/DropdownMenu/DropdownMenu.doc.mjs +12 -0
  87. package/src/DropdownMenu/DropdownMenu.spec.md +65 -40
  88. package/src/DropdownMenu/DropdownMenu.test.tsx +457 -0
  89. package/src/DropdownMenu/DropdownMenu.tsx +278 -49
  90. package/src/DropdownMenu/DropdownMenuCheckboxItem.doc.mjs +6 -3
  91. package/src/DropdownMenu/DropdownMenuRadioGroup.doc.mjs +2 -1
  92. package/src/DropdownMenu/DropdownMenuSubMenu.tsx +11 -1
  93. package/src/DropdownMenu/__tests__/MenuHeaderFooter.a11y.chromium.spec.ts +195 -0
  94. package/src/DropdownMenu/menuRegionsContext.ts +17 -0
  95. package/src/Layout/container.stylex.ts +11 -21
  96. package/src/Lightbox/Lightbox.test.tsx +58 -0
  97. package/src/Lightbox/Lightbox.tsx +12 -6
  98. package/src/Markdown/plugins/protocol.ts +45 -12
  99. package/src/Markdown/plugins/sourceLines.test.tsx +121 -0
  100. package/src/PowerSearch/PowerSearch.tsx +28 -1
  101. package/src/PowerSearch/PowerSearchEditPopover.test.tsx +284 -0
  102. package/src/PowerSearch/PowerSearchEditPopover.tsx +41 -13
  103. package/src/Table/plugins/rowExpansion/useTableRowExpansion.test.tsx +33 -9
  104. package/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx +81 -18
  105. package/src/TimeInput/TouchTimeField.tsx +3 -1
  106. package/src/Tokenizer/Tokenizer.doc.mjs +0 -16
  107. package/src/Tokenizer/Tokenizer.test.tsx +12 -63
  108. package/src/Tokenizer/Tokenizer.tsx +1 -25
  109. package/src/Toolbar/Toolbar.test.tsx +29 -0
  110. package/src/Typeahead/BaseTypeahead.doc.mjs +0 -16
  111. package/src/Typeahead/BaseTypeahead.test.tsx +1 -1
  112. package/src/Typeahead/BaseTypeahead.tsx +1 -21
  113. package/src/Typeahead/Typeahead.doc.mjs +0 -7
  114. package/src/Typeahead/Typeahead.test.tsx +24 -42
  115. package/src/Typeahead/Typeahead.tsx +1 -25
  116. package/src/hooks/useFocusTrap.ts +5 -1
  117. package/src/hooks/useRenamedProp.ts +6 -6
  118. package/src/reset.a11y.chromium.spec.ts +178 -0
  119. package/src/reset.css +8 -8
  120. package/src/reset.test.ts +112 -0
  121. package/src/theme/__tests__/SpacingRootFontSize.a11y.chromium.spec.ts +198 -0
  122. package/src/theme/tokens.stylex.ts +18 -15
  123. package/src/theme/tokens.test.ts +1 -1
  124. package/src/theme/useTheme.test.tsx +4 -4
  125. package/src/theme/useTheme.ts +1 -1
@@ -19,6 +19,7 @@ import {DropdownMenu} from './DropdownMenu';
19
19
  import {DropdownMenuItem} from './DropdownMenuItem';
20
20
  import {DropdownMenuDivider} from './DropdownMenuDivider';
21
21
  import {DropdownMenuGroup} from './DropdownMenuGroup';
22
+ import {DropdownMenuSubMenu} from './DropdownMenuSubMenu';
22
23
  import {Divider} from '../Divider';
23
24
  import {rtlStyles} from '../utils';
24
25
  import {__resetInteractionModalityForTest} from '../utils/interactionModality';
@@ -3357,3 +3358,459 @@ describe('DropdownMenu keyboard', () => {
3357
3358
  expect(onClick).toHaveBeenCalledTimes(1);
3358
3359
  });
3359
3360
  });
3361
+
3362
+ describe('DropdownMenu header and footer', () => {
3363
+ function LabelsMenu({onCreate}: {onCreate?: () => void}) {
3364
+ return (
3365
+ <DropdownMenu
3366
+ button={{label: 'Labels'}}
3367
+ header={<input aria-label="Filter labels" />}
3368
+ footer={
3369
+ <button type="button" onClick={onCreate}>
3370
+ Create label
3371
+ </button>
3372
+ }>
3373
+ <DropdownMenuItem label="Bug" />
3374
+ <DropdownMenuItem label="Feature" />
3375
+ </DropdownMenu>
3376
+ );
3377
+ }
3378
+
3379
+ async function open() {
3380
+ const user = userEvent.setup();
3381
+ render(<LabelsMenu />);
3382
+ await user.click(screen.getByRole('button', {name: /Labels/}));
3383
+ const menu = screen.getByRole('menu', {hidden: true});
3384
+ // A pointer open focuses the menu itself; let that land first.
3385
+ await waitFor(() => expect(menu).toHaveFocus());
3386
+ return {user, menu};
3387
+ }
3388
+
3389
+ it('renders the regions in the popover, outside the role="menu" element', async () => {
3390
+ const {menu} = await open();
3391
+ const field = screen.getByRole('textbox', {
3392
+ name: 'Filter labels',
3393
+ hidden: true,
3394
+ });
3395
+ const create = screen.getByRole('button', {
3396
+ name: 'Create label',
3397
+ hidden: true,
3398
+ });
3399
+ // The menu holds only its rows.
3400
+ expect(menu).not.toContainElement(field);
3401
+ expect(menu).not.toContainElement(create);
3402
+ expect(
3403
+ Array.from(menu.querySelectorAll('[role]')).map(el =>
3404
+ el.getAttribute('role'),
3405
+ ),
3406
+ ).toEqual(['menuitem', 'menuitem']);
3407
+ // Header before the menu, footer after it, in one popover.
3408
+ const popoverEl = menu.closest('[popover]');
3409
+ expect(popoverEl).toContainElement(field);
3410
+ expect(popoverEl).toContainElement(create);
3411
+ expect(
3412
+ field.compareDocumentPosition(menu) & Node.DOCUMENT_POSITION_FOLLOWING,
3413
+ ).toBeTruthy();
3414
+ expect(
3415
+ menu.compareDocumentPosition(create) & Node.DOCUMENT_POSITION_FOLLOWING,
3416
+ ).toBeTruthy();
3417
+ });
3418
+
3419
+ it('makes the menu one Tab stop between the regions', async () => {
3420
+ const {menu} = await open();
3421
+ expect(menu).toHaveAttribute('tabindex', '0');
3422
+ });
3423
+
3424
+ it('Tab from the menu moves on to the footer and keeps the menu open', async () => {
3425
+ const {menu} = await open();
3426
+ const hide = vi.mocked(HTMLElement.prototype.hidePopover);
3427
+ hide.mockClear();
3428
+ fireEvent.keyDown(menu, {key: 'Tab'});
3429
+ expect(hide).not.toHaveBeenCalled();
3430
+ fireEvent.keyDown(menu, {key: 'Tab', shiftKey: true});
3431
+ expect(hide).not.toHaveBeenCalled();
3432
+ });
3433
+
3434
+ it('Shift+Tab from a row moves straight to the header and keeps the menu open', async () => {
3435
+ const {menu} = await open();
3436
+ const hide = vi.mocked(HTMLElement.prototype.hidePopover);
3437
+ hide.mockClear();
3438
+ fireEvent.keyDown(menu, {key: 'ArrowDown'});
3439
+ const row = screen.getByRole('menuitem', {name: 'Bug', hidden: true});
3440
+ await waitFor(() => expect(row).toHaveFocus());
3441
+ fireEvent.keyDown(row, {key: 'Tab', shiftKey: true});
3442
+ expect(
3443
+ screen.getByRole('textbox', {name: 'Filter labels', hidden: true}),
3444
+ ).toHaveFocus();
3445
+ expect(hide).not.toHaveBeenCalled();
3446
+ });
3447
+
3448
+ it('a pointer open draws no focus ring on the menu; keyboard focus on it does', async () => {
3449
+ const {menu} = await open();
3450
+ // Opened with a pointer: the menu is focused but must not ring.
3451
+ const ringClasses = stylex
3452
+ .props(focusOutlineStyles.focusVisible)
3453
+ .className!.split(' ')
3454
+ .filter(Boolean);
3455
+ const hasRing = () => ringClasses.every(c => menu.classList.contains(c));
3456
+ expect(menu).toHaveFocus();
3457
+ expect(hasRing()).toBe(false);
3458
+ // Keyboard: leave to the header and come back by Tab.
3459
+ const field = screen.getByRole('textbox', {
3460
+ name: 'Filter labels',
3461
+ hidden: true,
3462
+ });
3463
+ fireEvent.keyDown(document, {key: 'Tab', shiftKey: true});
3464
+ act(() => field.focus());
3465
+ fireEvent.keyDown(document, {key: 'Tab'});
3466
+ act(() => menu.focus());
3467
+ await waitFor(() => expect(hasRing()).toBe(true));
3468
+ });
3469
+
3470
+ describe('with only hidden controls in a region', () => {
3471
+ async function openHidden(
3472
+ header: React.ReactNode,
3473
+ footer: React.ReactNode,
3474
+ ) {
3475
+ const user = userEvent.setup();
3476
+ render(
3477
+ <DropdownMenu
3478
+ button={{label: 'Labels'}}
3479
+ header={header}
3480
+ footer={footer}>
3481
+ <DropdownMenuItem label="Bug" />
3482
+ <DropdownMenuSubMenu label="Move to">
3483
+ <DropdownMenuItem label="Folder A" />
3484
+ </DropdownMenuSubMenu>
3485
+ </DropdownMenu>,
3486
+ );
3487
+ const trigger = screen.getByRole('button', {name: /Labels/});
3488
+ await user.click(trigger);
3489
+ const menu = screen.getByRole('menu', {name: 'Labels', hidden: true});
3490
+ await waitFor(() => expect(menu).toHaveFocus());
3491
+ const hide = vi.mocked(HTMLElement.prototype.hidePopover);
3492
+ hide.mockClear();
3493
+ return {user, trigger, menu, hide};
3494
+ }
3495
+
3496
+ it.each([
3497
+ [
3498
+ 'hidden',
3499
+ <button key="h" type="button" hidden>
3500
+ Gone
3501
+ </button>,
3502
+ ],
3503
+ [
3504
+ 'display: none',
3505
+ <button key="d" type="button" style={{display: 'none'}}>
3506
+ Gone
3507
+ </button>,
3508
+ ],
3509
+ [
3510
+ 'visibility: hidden',
3511
+ <button key="v" type="button" style={{visibility: 'hidden'}}>
3512
+ Gone
3513
+ </button>,
3514
+ ],
3515
+ ])(
3516
+ 'treats a %s footer control as unreachable: Tab from the menu closes it',
3517
+ async (_label, control) => {
3518
+ const {menu, hide} = await openHidden(null, control);
3519
+ fireEvent.keyDown(menu, {key: 'Tab'});
3520
+ expect(hide).toHaveBeenCalled();
3521
+ },
3522
+ );
3523
+
3524
+ it('treats a footer control inside a display: none wrapper as unreachable', async () => {
3525
+ const {menu, hide} = await openHidden(
3526
+ null,
3527
+ <div style={{display: 'none'}}>
3528
+ <button type="button">Create label</button>
3529
+ </div>,
3530
+ );
3531
+ fireEvent.keyDown(menu, {key: 'Tab'});
3532
+ expect(hide).toHaveBeenCalled();
3533
+ });
3534
+
3535
+ it('treats a header control inside a display: none wrapper as unreachable: Shift+Tab from a row closes', async () => {
3536
+ const {menu, hide} = await openHidden(
3537
+ <div style={{display: 'none'}}>
3538
+ <input aria-label="Filter labels" />
3539
+ </div>,
3540
+ null,
3541
+ );
3542
+ fireEvent.keyDown(menu, {key: 'ArrowDown'});
3543
+ const row = screen.getByRole('menuitem', {name: 'Bug', hidden: true});
3544
+ await waitFor(() => expect(row).toHaveFocus());
3545
+ fireEvent.keyDown(row, {key: 'Tab', shiftKey: true});
3546
+ expect(hide).toHaveBeenCalled();
3547
+ });
3548
+
3549
+ it('Tab past the last visible footer control closes, ignoring a wrapped hidden one after it', async () => {
3550
+ const {hide} = await openHidden(
3551
+ null,
3552
+ <>
3553
+ <button type="button">Create label</button>
3554
+ <div style={{display: 'none'}}>
3555
+ <button type="button">Gone</button>
3556
+ </div>
3557
+ </>,
3558
+ );
3559
+ const create = screen.getByRole('button', {
3560
+ name: 'Create label',
3561
+ hidden: true,
3562
+ });
3563
+ act(() => create.focus());
3564
+ fireEvent.keyDown(create, {key: 'Tab'});
3565
+ expect(hide).toHaveBeenCalled();
3566
+ });
3567
+
3568
+ it('Tab from an open sub-menu closes the menu when the footer control is in a hidden wrapper', async () => {
3569
+ const {user, menu} = await openHidden(
3570
+ null,
3571
+ <div style={{display: 'none'}}>
3572
+ <button type="button">Create label</button>
3573
+ </div>,
3574
+ );
3575
+ const subTrigger = screen.getByRole('menuitem', {
3576
+ name: /Move to/,
3577
+ hidden: true,
3578
+ });
3579
+ act(() => subTrigger.focus());
3580
+ await user.keyboard('{ArrowRight}');
3581
+ const folder = screen.getByRole('menuitem', {
3582
+ name: 'Folder A',
3583
+ hidden: true,
3584
+ });
3585
+ await waitFor(() => expect(folder).toHaveFocus());
3586
+ fireEvent.keyDown(folder, {key: 'Tab'});
3587
+ expect(subTrigger).toHaveAttribute('aria-expanded', 'false');
3588
+ // The flyout's own close also hides a popover, so check the root's.
3589
+ expect(menu.closest('[popover]')).not.toHaveAttribute('popover-open');
3590
+ });
3591
+
3592
+ it('treats a hidden header control as unreachable: Shift+Tab from a row closes the menu', async () => {
3593
+ const {menu, hide} = await openHidden(
3594
+ <input aria-label="Filter labels" hidden />,
3595
+ null,
3596
+ );
3597
+ fireEvent.keyDown(menu, {key: 'ArrowDown'});
3598
+ const row = screen.getByRole('menuitem', {name: 'Bug', hidden: true});
3599
+ await waitFor(() => expect(row).toHaveFocus());
3600
+ fireEvent.keyDown(row, {key: 'Tab', shiftKey: true});
3601
+ expect(hide).toHaveBeenCalled();
3602
+ });
3603
+
3604
+ it('Tab past the last visible footer control closes, ignoring a hidden one after it', async () => {
3605
+ const {hide} = await openHidden(
3606
+ null,
3607
+ <>
3608
+ <button type="button">Create label</button>
3609
+ <button type="button" style={{display: 'none'}}>
3610
+ Gone
3611
+ </button>
3612
+ </>,
3613
+ );
3614
+ const create = screen.getByRole('button', {
3615
+ name: 'Create label',
3616
+ hidden: true,
3617
+ });
3618
+ act(() => create.focus());
3619
+ fireEvent.keyDown(create, {key: 'Tab'});
3620
+ expect(hide).toHaveBeenCalled();
3621
+ });
3622
+
3623
+ it('Tab from an open sub-menu closes the menu when the footer holds only hidden controls', async () => {
3624
+ const {user, menu} = await openHidden(
3625
+ null,
3626
+ <button type="button" hidden>
3627
+ Gone
3628
+ </button>,
3629
+ );
3630
+ const subTrigger = screen.getByRole('menuitem', {
3631
+ name: /Move to/,
3632
+ hidden: true,
3633
+ });
3634
+ act(() => subTrigger.focus());
3635
+ await user.keyboard('{ArrowRight}');
3636
+ const folder = screen.getByRole('menuitem', {
3637
+ name: 'Folder A',
3638
+ hidden: true,
3639
+ });
3640
+ await waitFor(() => expect(folder).toHaveFocus());
3641
+ fireEvent.keyDown(folder, {key: 'Tab'});
3642
+ expect(subTrigger).toHaveAttribute('aria-expanded', 'false');
3643
+ // The flyout's own close also hides a popover, so check the root's.
3644
+ expect(menu.closest('[popover]')).not.toHaveAttribute('popover-open');
3645
+ });
3646
+ });
3647
+
3648
+ describe('with a sub-menu open', () => {
3649
+ async function openFlyout() {
3650
+ const user = userEvent.setup();
3651
+ render(
3652
+ <DropdownMenu
3653
+ button={{label: 'Labels'}}
3654
+ header={<input aria-label="Filter labels" />}
3655
+ footer={<button type="button">Create label</button>}>
3656
+ <DropdownMenuItem label="Bug" />
3657
+ <DropdownMenuSubMenu label="Move to">
3658
+ <DropdownMenuItem label="Folder A" />
3659
+ <DropdownMenuItem label="Folder B" />
3660
+ </DropdownMenuSubMenu>
3661
+ </DropdownMenu>,
3662
+ );
3663
+ await user.click(screen.getByRole('button', {name: /Labels/}));
3664
+ const root = screen.getByRole('menu', {name: 'Labels', hidden: true});
3665
+ await waitFor(() => expect(root).toHaveFocus());
3666
+ const subTrigger = screen.getByRole('menuitem', {
3667
+ name: /Move to/,
3668
+ hidden: true,
3669
+ });
3670
+ act(() => subTrigger.focus());
3671
+ await user.keyboard('{ArrowRight}');
3672
+ await waitFor(() =>
3673
+ expect(subTrigger).toHaveAttribute('aria-expanded', 'true'),
3674
+ );
3675
+ const folder = screen.getByRole('menuitem', {
3676
+ name: 'Folder A',
3677
+ hidden: true,
3678
+ });
3679
+ await waitFor(() => expect(folder).toHaveFocus());
3680
+ const hide = vi.mocked(HTMLElement.prototype.hidePopover);
3681
+ return {root, subTrigger, folder, hide};
3682
+ }
3683
+
3684
+ it('Tab from a flyout row closes the flyout and moves to the footer', async () => {
3685
+ const {root, subTrigger, folder} = await openFlyout();
3686
+ fireEvent.keyDown(folder, {key: 'Tab'});
3687
+ expect(subTrigger).toHaveAttribute('aria-expanded', 'false');
3688
+ expect(
3689
+ screen.getByRole('button', {name: 'Create label', hidden: true}),
3690
+ ).toHaveFocus();
3691
+ expect(root.closest('[popover]')).toHaveAttribute('popover-open');
3692
+ });
3693
+
3694
+ it('Shift+Tab from a flyout row closes the flyout and moves to the header', async () => {
3695
+ const {subTrigger, folder} = await openFlyout();
3696
+ fireEvent.keyDown(folder, {key: 'Tab', shiftKey: true});
3697
+ expect(subTrigger).toHaveAttribute('aria-expanded', 'false');
3698
+ expect(
3699
+ screen.getByRole('textbox', {name: 'Filter labels', hidden: true}),
3700
+ ).toHaveFocus();
3701
+ });
3702
+ });
3703
+
3704
+ it('Tab past the last footer control closes the menu', async () => {
3705
+ await open();
3706
+ const hide = vi.mocked(HTMLElement.prototype.hidePopover);
3707
+ hide.mockClear();
3708
+ const create = screen.getByRole('button', {
3709
+ name: 'Create label',
3710
+ hidden: true,
3711
+ });
3712
+ create.focus();
3713
+ fireEvent.keyDown(create, {key: 'Tab', shiftKey: true});
3714
+ expect(hide).not.toHaveBeenCalled();
3715
+ create.focus();
3716
+ expect(create).toHaveFocus();
3717
+ fireEvent.keyDown(create, {key: 'Tab'});
3718
+ expect(hide).toHaveBeenCalled();
3719
+ expect(screen.getByRole('button', {name: /Labels/})).toHaveFocus();
3720
+ });
3721
+
3722
+ it('Shift+Tab before the first header control closes the menu', async () => {
3723
+ await open();
3724
+ const hide = vi.mocked(HTMLElement.prototype.hidePopover);
3725
+ hide.mockClear();
3726
+ const field = screen.getByRole('textbox', {
3727
+ name: 'Filter labels',
3728
+ hidden: true,
3729
+ });
3730
+ field.focus();
3731
+ fireEvent.keyDown(field, {key: 'Tab'});
3732
+ expect(hide).not.toHaveBeenCalled();
3733
+ field.focus();
3734
+ expect(field).toHaveFocus();
3735
+ fireEvent.keyDown(field, {key: 'Tab', shiftKey: true});
3736
+ expect(hide).toHaveBeenCalled();
3737
+ });
3738
+
3739
+ it('leaves keys typed in the header to the field', async () => {
3740
+ const {user} = await open();
3741
+ const field = screen.getByRole('textbox', {
3742
+ name: 'Filter labels',
3743
+ hidden: true,
3744
+ });
3745
+ await user.click(field);
3746
+ await user.keyboard('bug');
3747
+ expect(field).toHaveValue('bug');
3748
+ expect(field).toHaveFocus();
3749
+ });
3750
+
3751
+ it('a footer button acts on its own click and the menu stays open', async () => {
3752
+ const user = userEvent.setup();
3753
+ const onCreate = vi.fn();
3754
+ render(<LabelsMenu onCreate={onCreate} />);
3755
+ const trigger = screen.getByRole('button', {name: /Labels/});
3756
+ await user.click(trigger);
3757
+ await user.click(
3758
+ screen.getByRole('button', {name: 'Create label', hidden: true}),
3759
+ );
3760
+ expect(onCreate).toHaveBeenCalledTimes(1);
3761
+ expect(trigger).toHaveAttribute('aria-expanded', 'true');
3762
+ });
3763
+
3764
+ it('without a header or footer, Tab still closes and the menu stays out of the Tab order', async () => {
3765
+ const user = userEvent.setup();
3766
+ render(
3767
+ <DropdownMenu button={{label: 'Actions'}}>
3768
+ <DropdownMenuItem label="Item 1" />
3769
+ </DropdownMenu>,
3770
+ );
3771
+ await user.click(screen.getByRole('button', {name: /Actions/}));
3772
+ const menu = screen.getByRole('menu', {hidden: true});
3773
+ expect(menu).toHaveAttribute('tabindex', '-1');
3774
+ expect(menu.parentElement?.closest('[role="menu"]')).toBeNull();
3775
+ fireEvent.keyDown(menu, {key: 'Tab'});
3776
+ expect(HTMLElement.prototype.hidePopover).toHaveBeenCalled();
3777
+ });
3778
+
3779
+ it('a false or empty region renders no wrapper', async () => {
3780
+ const user = userEvent.setup();
3781
+ render(
3782
+ <DropdownMenu button={{label: 'Actions'}} header={false} footer="">
3783
+ <DropdownMenuItem label="Item 1" />
3784
+ </DropdownMenu>,
3785
+ );
3786
+ await user.click(screen.getByRole('button', {name: /Actions/}));
3787
+ const menu = screen.getByRole('menu', {hidden: true});
3788
+ expect(menu).toHaveAttribute('tabindex', '-1');
3789
+ });
3790
+
3791
+ it('renders the regions above and below the actions in the bottom sheet', async () => {
3792
+ const user = userEvent.setup();
3793
+ render(
3794
+ <DropdownMenu
3795
+ button={{label: 'Labels'}}
3796
+ presentation="bottom-sheet"
3797
+ header={<input aria-label="Filter labels" />}
3798
+ footer={<button type="button">Create label</button>}
3799
+ items={[{label: 'Bug'}, {label: 'Feature'}]}
3800
+ />,
3801
+ );
3802
+ await user.click(screen.getByRole('button', {name: /Labels/}));
3803
+ const sheet = screen.getByRole('dialog', {name: 'Labels'});
3804
+ const field = screen.getByRole('textbox', {name: 'Filter labels'});
3805
+ const bug = screen.getByRole('button', {name: 'Bug'});
3806
+ const create = screen.getByRole('button', {name: 'Create label'});
3807
+ expect(sheet).toContainElement(field);
3808
+ expect(sheet).toContainElement(create);
3809
+ expect(
3810
+ field.compareDocumentPosition(bug) & Node.DOCUMENT_POSITION_FOLLOWING,
3811
+ ).toBeTruthy();
3812
+ expect(
3813
+ bug.compareDocumentPosition(create) & Node.DOCUMENT_POSITION_FOLLOWING,
3814
+ ).toBeTruthy();
3815
+ });
3816
+ });