@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.
- package/CHANGELOG.md +186 -0
- package/dist/BottomSheet/BottomSheet.d.ts +5 -4
- package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetPanel.d.ts +1 -1
- package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetPanel.js +1 -1
- package/dist/Calendar/styles.d.ts +6 -6
- package/dist/Chat/ChatComposerInput.d.ts +0 -11
- package/dist/Chat/ChatComposerInput.d.ts.map +1 -1
- package/dist/Chat/ChatComposerInput.js +0 -11
- package/dist/Chat/useTriggerMenu.d.ts.map +1 -1
- package/dist/Chat/useTriggerMenu.js +1 -1
- package/dist/CommandPalette/CommandPaletteList.d.ts +3 -2
- package/dist/CommandPalette/CommandPaletteList.d.ts.map +1 -1
- package/dist/CommandPalette/CommandPaletteList.js +3 -2
- package/dist/DateInput/TouchDateField.d.ts.map +1 -1
- package/dist/DateInput/TouchDateField.js +4 -1
- package/dist/DateTimeInput/TouchDateTimeField.d.ts.map +1 -1
- package/dist/DateTimeInput/TouchDateTimeField.js +29 -13
- package/dist/DropdownMenu/DropdownMenu.d.ts +11 -0
- package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenu.js +173 -11
- package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuSubMenu.js +13 -2
- package/dist/DropdownMenu/menuRegionsContext.d.ts +2 -0
- package/dist/DropdownMenu/menuRegionsContext.d.ts.map +1 -0
- package/dist/DropdownMenu/menuRegionsContext.js +17 -0
- package/dist/Field/inputStyles.stylex.d.ts +3 -3
- package/dist/Layout/container.stylex.d.ts +18 -20
- package/dist/Layout/container.stylex.d.ts.map +1 -1
- package/dist/Layout/container.stylex.js +15 -25
- package/dist/Layout/padding.stylex.d.ts +231 -231
- package/dist/Lightbox/Lightbox.d.ts.map +1 -1
- package/dist/Lightbox/Lightbox.js +15 -6
- package/dist/Markdown/plugins/protocol.d.ts.map +1 -1
- package/dist/Markdown/plugins/protocol.js +33 -8
- package/dist/NavItem/navItemStyles.stylex.d.ts +5 -5
- package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearch.js +21 -1
- package/dist/PowerSearch/PowerSearchEditPopover.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearchEditPopover.js +30 -15
- package/dist/Stack/stack.stylex.d.ts +22 -22
- package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts.map +1 -1
- package/dist/Table/plugins/rowExpansion/useTableRowExpansion.js +89 -14
- package/dist/Table/table.stylex.d.ts +6 -6
- package/dist/TimeInput/TouchTimeField.d.ts.map +1 -1
- package/dist/TimeInput/TouchTimeField.js +4 -1
- package/dist/Tokenizer/Tokenizer.d.ts +1 -12
- package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
- package/dist/Tokenizer/Tokenizer.js +1 -13
- package/dist/Typeahead/BaseTypeahead.d.ts +0 -11
- package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
- package/dist/Typeahead/BaseTypeahead.js +1 -9
- package/dist/Typeahead/Typeahead.d.ts +1 -12
- package/dist/Typeahead/Typeahead.d.ts.map +1 -1
- package/dist/Typeahead/Typeahead.js +1 -13
- package/dist/astryx.css +17 -12
- package/dist/hooks/useFocusTrap.d.ts +8 -0
- package/dist/hooks/useFocusTrap.d.ts.map +1 -1
- package/dist/hooks/useFocusTrap.js +5 -1
- package/dist/hooks/useRenamedProp.d.ts +6 -6
- package/dist/hooks/useRenamedProp.js +6 -6
- package/dist/reset.a11y.chromium.spec.d.ts +2 -0
- package/dist/reset.a11y.chromium.spec.d.ts.map +1 -0
- package/dist/reset.a11y.chromium.spec.js +176 -0
- package/dist/theme/tokens.stylex.d.ts +30 -30
- package/dist/theme/tokens.stylex.d.ts.map +1 -1
- package/dist/theme/tokens.stylex.js +18 -15
- package/dist/theme/useTheme.d.ts +1 -1
- package/package.json +4 -4
- package/src/BottomSheet/BottomSheet.doc.mjs +4 -4
- package/src/BottomSheet/BottomSheet.tsx +5 -4
- package/src/BottomSheet/BottomSheetPanel.test.tsx +6 -9
- package/src/BottomSheet/BottomSheetPanel.tsx +2 -3
- package/src/Chat/ChatComposerInput.doc.mjs +3 -3
- package/src/Chat/ChatComposerInput.test.tsx +7 -55
- package/src/Chat/ChatComposerInput.tsx +0 -36
- package/src/Chat/useTriggerMenu.tsx +1 -4
- package/src/CommandPalette/CommandPalette.spec.md +2 -0
- package/src/CommandPalette/CommandPaletteList.spec.md +189 -0
- package/src/CommandPalette/CommandPaletteList.test.tsx +47 -0
- package/src/CommandPalette/CommandPaletteList.tsx +3 -2
- package/src/DateInput/TouchDateField.tsx +3 -1
- package/src/DateTimeInput/DateTimeInputTouch.test.tsx +60 -1
- package/src/DateTimeInput/TouchDateTimeField.tsx +31 -14
- package/src/DropdownMenu/DropdownMenu.doc.mjs +12 -0
- package/src/DropdownMenu/DropdownMenu.spec.md +65 -40
- package/src/DropdownMenu/DropdownMenu.test.tsx +457 -0
- package/src/DropdownMenu/DropdownMenu.tsx +278 -49
- package/src/DropdownMenu/DropdownMenuCheckboxItem.doc.mjs +6 -3
- package/src/DropdownMenu/DropdownMenuRadioGroup.doc.mjs +2 -1
- package/src/DropdownMenu/DropdownMenuSubMenu.tsx +11 -1
- package/src/DropdownMenu/__tests__/MenuHeaderFooter.a11y.chromium.spec.ts +195 -0
- package/src/DropdownMenu/menuRegionsContext.ts +17 -0
- package/src/Layout/container.stylex.ts +11 -21
- package/src/Lightbox/Lightbox.test.tsx +58 -0
- package/src/Lightbox/Lightbox.tsx +12 -6
- package/src/Markdown/plugins/protocol.ts +45 -12
- package/src/Markdown/plugins/sourceLines.test.tsx +121 -0
- package/src/PowerSearch/PowerSearch.tsx +28 -1
- package/src/PowerSearch/PowerSearchEditPopover.test.tsx +284 -0
- package/src/PowerSearch/PowerSearchEditPopover.tsx +41 -13
- package/src/Table/plugins/rowExpansion/useTableRowExpansion.test.tsx +33 -9
- package/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx +81 -18
- package/src/TimeInput/TouchTimeField.tsx +3 -1
- package/src/Tokenizer/Tokenizer.doc.mjs +0 -16
- package/src/Tokenizer/Tokenizer.test.tsx +12 -63
- package/src/Tokenizer/Tokenizer.tsx +1 -25
- package/src/Toolbar/Toolbar.test.tsx +29 -0
- package/src/Typeahead/BaseTypeahead.doc.mjs +0 -16
- package/src/Typeahead/BaseTypeahead.test.tsx +1 -1
- package/src/Typeahead/BaseTypeahead.tsx +1 -21
- package/src/Typeahead/Typeahead.doc.mjs +0 -7
- package/src/Typeahead/Typeahead.test.tsx +24 -42
- package/src/Typeahead/Typeahead.tsx +1 -25
- package/src/hooks/useFocusTrap.ts +5 -1
- package/src/hooks/useRenamedProp.ts +6 -6
- package/src/reset.a11y.chromium.spec.ts +178 -0
- package/src/reset.css +8 -8
- package/src/reset.test.ts +112 -0
- package/src/theme/__tests__/SpacingRootFontSize.a11y.chromium.spec.ts +198 -0
- package/src/theme/tokens.stylex.ts +18 -15
- package/src/theme/tokens.test.ts +1 -1
- package/src/theme/useTheme.test.tsx +4 -4
- 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
|
+
});
|