@astryxdesign/core 0.7.2 → 0.7.3-canary.0bf23bd
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/BottomSheet/BottomSheet.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheet.js +8 -4
- package/dist/BottomSheet/BottomSheetSwitcher.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetSwitcher.js +7 -0
- package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
- package/dist/CheckboxInput/CheckboxInput.js +19 -24
- package/dist/ClickableCard/ClickableCard.d.ts +2 -2
- package/dist/ClickableCard/ClickableCard.d.ts.map +1 -1
- package/dist/ClickableCard/ClickableCard.js +11 -9
- package/dist/CodeBlock/CodeBlock.d.ts.map +1 -1
- package/dist/CodeBlock/CodeBlock.js +11 -1
- package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
- package/dist/ComplexSelector/ComplexSelector.js +23 -5
- package/dist/DateInput/DateInput.d.ts +4 -4
- package/dist/DateInput/DateInput.d.ts.map +1 -1
- package/dist/DateInput/DateInput.js +27 -20
- package/dist/DateInput/NativeDateField.d.ts.map +1 -1
- package/dist/DateInput/NativeDateField.js +14 -8
- package/dist/DateInput/TouchDateField.d.ts.map +1 -1
- package/dist/DateInput/TouchDateField.js +14 -10
- package/dist/Dialog/Dialog.d.ts.map +1 -1
- package/dist/Dialog/Dialog.js +9 -4
- package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenu.js +52 -10
- package/dist/DropdownMenu/DropdownMenuContext.d.ts +34 -1
- package/dist/DropdownMenu/DropdownMenuContext.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuContext.js +15 -0
- package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts +21 -2
- package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuSubMenu.js +375 -42
- package/dist/DropdownMenu/menuRegionStops.d.ts +2 -2
- package/dist/DropdownMenu/menuRegionStops.js +2 -2
- package/dist/FileInput/FileInput.d.ts.map +1 -1
- package/dist/FileInput/FileInput.js +14 -12
- package/dist/Link/Link.d.ts +2 -3
- package/dist/Link/Link.d.ts.map +1 -1
- package/dist/Link/Link.js +11 -19
- package/dist/Markdown/Markdown.d.ts.map +1 -1
- package/dist/Markdown/Markdown.js +48 -15
- package/dist/MultiSelector/MultiSelector.d.ts +4 -4
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.disabledFocus.a11y.chromium.spec.d.ts +2 -0
- package/dist/MultiSelector/MultiSelector.disabledFocus.a11y.chromium.spec.d.ts.map +1 -0
- package/dist/MultiSelector/MultiSelector.disabledFocus.a11y.chromium.spec.js +48 -0
- package/dist/MultiSelector/MultiSelector.js +25 -18
- package/dist/Popover/Popover.d.ts.map +1 -1
- package/dist/Popover/Popover.js +8 -4
- package/dist/Selector/Selector.d.ts +4 -4
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +23 -14
- package/dist/Selector/SelectorOption.d.ts.map +1 -1
- package/dist/Selector/SelectorOption.js +1 -0
- package/dist/Slider/Slider.d.ts.map +1 -1
- package/dist/Slider/Slider.js +7 -14
- package/dist/Switch/Switch.d.ts.map +1 -1
- package/dist/Switch/Switch.js +25 -17
- package/dist/Switch/switch.markers.stylex.d.ts +2 -0
- package/dist/Switch/switch.markers.stylex.d.ts.map +1 -1
- package/dist/Switch/switch.markers.stylex.js +6 -0
- package/dist/TimeInput/TimeInput.d.ts +2 -1
- package/dist/TimeInput/TimeInput.d.ts.map +1 -1
- package/dist/Token/Token.d.ts.map +1 -1
- package/dist/Token/Token.js +8 -3
- package/dist/Tooltip/Tooltip.d.ts.map +1 -1
- package/dist/Tooltip/Tooltip.js +36 -14
- package/dist/Tooltip/useTooltip.d.ts.map +1 -1
- package/dist/Tooltip/useTooltip.js +11 -2
- package/dist/TopNav/TopNav.d.ts.map +1 -1
- package/dist/TopNav/TopNav.js +2 -2
- package/dist/TopNav/TopNavHeading.d.ts.map +1 -1
- package/dist/TopNav/TopNavHeading.js +1 -0
- package/dist/Typeahead/Typeahead.d.ts.map +1 -1
- package/dist/Typeahead/Typeahead.js +14 -2
- package/dist/astryx.css +8 -2
- package/dist/hooks/useMenuPress.d.ts +11 -1
- package/dist/hooks/useMenuPress.d.ts.map +1 -1
- package/dist/hooks/useMenuPress.js +43 -5
- package/dist/utils/disabledInteraction.d.ts +21 -0
- package/dist/utils/disabledInteraction.d.ts.map +1 -0
- package/dist/utils/disabledInteraction.js +33 -0
- package/dist/utils/timeParser.d.ts +4 -0
- package/dist/utils/timeParser.d.ts.map +1 -1
- package/dist/utils/timeParser.js +9 -2
- package/package.json +4 -4
- package/scripts/agent-doc-state.mjs +35 -8
- package/src/Banner/Banner.spec.md +37 -32
- package/src/BottomSheet/BottomSheet.spec.md +13 -6
- package/src/BottomSheet/BottomSheet.test.tsx +29 -0
- package/src/BottomSheet/BottomSheet.tsx +8 -4
- package/src/BottomSheet/BottomSheetSwitcher.spec.md +121 -51
- package/src/BottomSheet/BottomSheetSwitcher.test.tsx +26 -0
- package/src/BottomSheet/BottomSheetSwitcher.tsx +6 -0
- package/src/CheckboxInput/CheckboxInput.test.tsx +44 -5
- package/src/CheckboxInput/CheckboxInput.tsx +16 -26
- package/src/CheckboxInput/__tests__/Checkbox.a11y.states.ts +3 -3
- package/src/CheckboxList/CheckboxList.test.tsx +9 -6
- package/src/ClickableCard/ClickableCard.doc.mjs +2 -2
- package/src/ClickableCard/ClickableCard.test.tsx +5 -4
- package/src/ClickableCard/ClickableCard.tsx +9 -9
- package/src/CodeBlock/CodeBlock.highlightStyles.test.tsx +87 -0
- package/src/CodeBlock/CodeBlock.tsx +11 -1
- package/src/ComplexSelector/ComplexSelector.focusableDisabled.test.tsx +76 -0
- package/src/ComplexSelector/ComplexSelector.tsx +21 -4
- package/src/ContextMenu/ContextMenu.test.tsx +41 -0
- package/src/DateInput/DateInput.doc.mjs +9 -8
- package/src/DateInput/DateInput.test.tsx +88 -13
- package/src/DateInput/DateInput.tsx +49 -21
- package/src/DateInput/DateInputTouch.test.tsx +22 -1
- package/src/DateInput/NativeDateField.test.tsx +29 -3
- package/src/DateInput/NativeDateField.tsx +20 -7
- package/src/DateInput/TouchDateField.tsx +24 -9
- package/src/DateRangeInput/DateRangeInput.spec.md +13 -13
- package/src/Dialog/Dialog.test.tsx +39 -0
- package/src/Dialog/Dialog.tsx +9 -4
- package/src/DropdownMenu/DropdownMenu.spec.md +33 -2
- package/src/DropdownMenu/DropdownMenu.tsx +79 -8
- package/src/DropdownMenu/DropdownMenuContext.public.test.ts +22 -0
- package/src/DropdownMenu/DropdownMenuContext.tsx +39 -1
- package/src/DropdownMenu/DropdownMenuSubMenu.doc.mjs +16 -1
- package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +623 -1
- package/src/DropdownMenu/DropdownMenuSubMenu.tsx +556 -99
- package/src/DropdownMenu/__tests__/SubMenuHeaderFooter.a11y.chromium.spec.ts +267 -0
- package/src/DropdownMenu/menuRegionStops.ts +2 -2
- package/src/DropdownMenu/modules/useMenuPress.spec.md +9 -0
- package/src/FieldStatus/FieldStatus.spec.md +3 -2
- package/src/FileInput/FileInput.test.tsx +30 -7
- package/src/FileInput/FileInput.tsx +21 -11
- package/src/Link/Link.test.tsx +21 -7
- package/src/Link/Link.tsx +16 -24
- package/src/Link/__tests__/Link.a11y.states.ts +1 -1
- package/src/Markdown/Markdown.rerender.test.tsx +143 -0
- package/src/Markdown/Markdown.tsx +89 -35
- package/src/MultiSelector/MultiSelector.disabledFocus.a11y.chromium.spec.ts +71 -0
- package/src/MultiSelector/MultiSelector.doc.mjs +5 -5
- package/src/MultiSelector/MultiSelector.focusableDisabled.test.tsx +121 -0
- package/src/MultiSelector/MultiSelector.test.tsx +49 -7
- package/src/MultiSelector/MultiSelector.tsx +53 -29
- package/src/Popover/Popover.test.tsx +15 -0
- package/src/Popover/Popover.tsx +19 -8
- package/src/PowerSearch/PowerSearchFilterEditor.test.tsx +3 -2
- package/src/PowerSearch/PowerSearchToken.test.tsx +7 -5
- package/src/Selector/Selector.doc.mjs +4 -4
- package/src/Selector/Selector.focusableDisabled.test.tsx +127 -0
- package/src/Selector/Selector.spec.md +11 -11
- package/src/Selector/Selector.test.tsx +64 -6
- package/src/Selector/Selector.tsx +51 -21
- package/src/Selector/SelectorOption.tsx +4 -0
- package/src/SideNav/SideNav.test.tsx +35 -0
- package/src/Slider/Slider.spec.md +11 -11
- package/src/Slider/Slider.test.tsx +3 -3
- package/src/Slider/Slider.tsx +8 -15
- package/src/Switch/Switch.test.tsx +85 -8
- package/src/Switch/Switch.tsx +26 -16
- package/src/Switch/__tests__/Switch.a11y.states.ts +2 -2
- package/src/Switch/switch.markers.stylex.ts +4 -0
- package/src/Table/Table.spec.md +2 -0
- package/src/Table/plugins/infiniteScroll/useTableInfiniteScroll.spec.md +244 -0
- package/src/Table/plugins/selection/useTableSelection.test.tsx +5 -3
- package/src/Table/plugins/virtualization/useTableVirtualization.spec.md +266 -0
- package/src/TimeInput/TimeInput.doc.mjs +5 -3
- package/src/TimeInput/TimeInput.test.tsx +20 -0
- package/src/TimeInput/TimeInput.tsx +2 -1
- package/src/Token/Token.test.tsx +26 -4
- package/src/Token/Token.tsx +6 -2
- package/src/Tokenizer/Tokenizer.spec.md +28 -28
- package/src/Tooltip/Tooltip.test.tsx +51 -6
- package/src/Tooltip/Tooltip.tsx +41 -18
- package/src/Tooltip/__tests__/Tooltip.a11y.chromium.spec.ts +129 -0
- package/src/Tooltip/__tests__/Tooltip.a11y.renders.tsx +86 -0
- package/src/Tooltip/__tests__/Tooltip.a11y.states.ts +132 -0
- package/src/Tooltip/__tests__/Tooltip.a11y.test.tsx +126 -0
- package/src/Tooltip/__tests__/TooltipHoverBridge.a11y.chromium.spec.ts +74 -0
- package/src/Tooltip/useTooltip.test.tsx +23 -0
- package/src/Tooltip/useTooltip.tsx +22 -9
- package/src/TopNav/TopNav.tsx +11 -1
- package/src/TopNav/TopNavHeading.tsx +1 -0
- package/src/TopNav/__tests__/TopNavMobileLongHeading.a11y.chromium.spec.ts +74 -0
- package/src/Typeahead/Typeahead.test.tsx +89 -0
- package/src/Typeahead/Typeahead.tsx +15 -2
- package/src/hooks/useMenuPress.ts +45 -7
- package/src/utils/disabledInteraction.ts +40 -0
- package/src/utils/timeParser.test.ts +11 -0
- package/src/utils/timeParser.ts +9 -2
|
@@ -6,7 +6,15 @@
|
|
|
6
6
|
* @output Unit tests for DropdownMenuSubMenu (#3829)
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
describe,
|
|
11
|
+
it,
|
|
12
|
+
expect,
|
|
13
|
+
vi,
|
|
14
|
+
beforeAll,
|
|
15
|
+
beforeEach,
|
|
16
|
+
afterEach,
|
|
17
|
+
} from 'vitest';
|
|
10
18
|
import {createRef, useState} from 'react';
|
|
11
19
|
import {render, screen, waitFor, fireEvent, act} from '@testing-library/react';
|
|
12
20
|
import userEvent from '@testing-library/user-event';
|
|
@@ -14,7 +22,9 @@ import * as stylex from '@stylexjs/stylex';
|
|
|
14
22
|
import {DropdownMenu} from './DropdownMenu';
|
|
15
23
|
import {DropdownMenuItem} from './DropdownMenuItem';
|
|
16
24
|
import {DropdownMenuSubMenu} from './DropdownMenuSubMenu';
|
|
25
|
+
import {warmMenuBottomSheet} from './__tests__/warmMenuBottomSheet';
|
|
17
26
|
import {IconButton} from '../IconButton';
|
|
27
|
+
import {RadioList, RadioListItem} from '../RadioList';
|
|
18
28
|
import {COMPACT_TOUCH_PRESENTATION_QUERY} from '../hooks/useAdaptivePresentation';
|
|
19
29
|
import {rtlStyles} from '../utils';
|
|
20
30
|
|
|
@@ -1679,3 +1689,615 @@ describe('DropdownMenuSubMenu light dismiss of the root menu (#6893)', () => {
|
|
|
1679
1689
|
}
|
|
1680
1690
|
});
|
|
1681
1691
|
});
|
|
1692
|
+
|
|
1693
|
+
describe('DropdownMenuSubMenu header and footer', () => {
|
|
1694
|
+
const h = {hidden: true} as const;
|
|
1695
|
+
// The touch sheet is loaded lazily; resolve it once so the sheets below
|
|
1696
|
+
// render synchronously.
|
|
1697
|
+
beforeAll(warmMenuBottomSheet, 20000);
|
|
1698
|
+
// The bottom sheet is a native dialog, which jsdom does not open.
|
|
1699
|
+
beforeEach(() => {
|
|
1700
|
+
HTMLDialogElement.prototype.showModal = vi.fn(function (
|
|
1701
|
+
this: HTMLDialogElement,
|
|
1702
|
+
) {
|
|
1703
|
+
this.setAttribute('open', '');
|
|
1704
|
+
});
|
|
1705
|
+
HTMLDialogElement.prototype.close = vi.fn(function (
|
|
1706
|
+
this: HTMLDialogElement,
|
|
1707
|
+
) {
|
|
1708
|
+
this.removeAttribute('open');
|
|
1709
|
+
});
|
|
1710
|
+
});
|
|
1711
|
+
// Hidden by an attribute, or stepped aside by a style rule (the root's menu
|
|
1712
|
+
// while a view with regions shows). The walk stops at the popover, whose
|
|
1713
|
+
// own open state jsdom does not compute.
|
|
1714
|
+
const isShown = (el: HTMLElement) => {
|
|
1715
|
+
for (
|
|
1716
|
+
let node: HTMLElement | null = el;
|
|
1717
|
+
node != null && !node.hasAttribute('popover');
|
|
1718
|
+
node = node.parentElement
|
|
1719
|
+
) {
|
|
1720
|
+
if (node.hidden || window.getComputedStyle(node).display === 'none') {
|
|
1721
|
+
return false;
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
return true;
|
|
1725
|
+
};
|
|
1726
|
+
|
|
1727
|
+
function LabelsMenu({
|
|
1728
|
+
onCreate,
|
|
1729
|
+
onPick,
|
|
1730
|
+
rootHeader,
|
|
1731
|
+
menuMaxHeight,
|
|
1732
|
+
header = <input aria-label="Filter labels" />,
|
|
1733
|
+
}: {
|
|
1734
|
+
onCreate?: () => void;
|
|
1735
|
+
onPick?: (label: string) => void;
|
|
1736
|
+
rootHeader?: React.ReactNode;
|
|
1737
|
+
menuMaxHeight?: number;
|
|
1738
|
+
header?: React.ReactNode;
|
|
1739
|
+
} = {}) {
|
|
1740
|
+
return (
|
|
1741
|
+
<DropdownMenu
|
|
1742
|
+
button={{label: 'Session'}}
|
|
1743
|
+
header={rootHeader}
|
|
1744
|
+
menuMaxHeight={menuMaxHeight}>
|
|
1745
|
+
<DropdownMenuItem label="Rename" onClick={() => {}} />
|
|
1746
|
+
<DropdownMenuSubMenu
|
|
1747
|
+
label="Labels"
|
|
1748
|
+
header={header}
|
|
1749
|
+
footer={
|
|
1750
|
+
<button type="button" onClick={onCreate}>
|
|
1751
|
+
Create label
|
|
1752
|
+
</button>
|
|
1753
|
+
}>
|
|
1754
|
+
<DropdownMenuItem label="Bug" onClick={() => onPick?.('Bug')} />
|
|
1755
|
+
<DropdownMenuSubMenu label="More">
|
|
1756
|
+
<DropdownMenuItem label="Later" onClick={() => onPick?.('Later')} />
|
|
1757
|
+
</DropdownMenuSubMenu>
|
|
1758
|
+
</DropdownMenuSubMenu>
|
|
1759
|
+
</DropdownMenu>
|
|
1760
|
+
);
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
async function openLabels(props?: Parameters<typeof LabelsMenu>[0]) {
|
|
1764
|
+
const user = userEvent.setup();
|
|
1765
|
+
render(<LabelsMenu {...props} />);
|
|
1766
|
+
const trigger = screen.getByRole('button', {name: /Session/});
|
|
1767
|
+
await user.click(trigger);
|
|
1768
|
+
await waitFor(() =>
|
|
1769
|
+
expect(trigger).toHaveAttribute('aria-expanded', 'true'),
|
|
1770
|
+
);
|
|
1771
|
+
const rootMenu = screen.getByRole('menu', {name: 'Session', ...h});
|
|
1772
|
+
// A pointer open focuses the root menu itself; let that land first.
|
|
1773
|
+
await waitFor(() => expect(rootMenu).toHaveFocus());
|
|
1774
|
+
const row = screen.getByRole('menuitem', {name: /Labels/, ...h});
|
|
1775
|
+
fireEvent.click(row, {detail: 1});
|
|
1776
|
+
const dialog = await screen.findByRole('dialog', {name: /Labels/, ...h});
|
|
1777
|
+
return {user, trigger, rootMenu, row, dialog};
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
const field = () =>
|
|
1781
|
+
screen.getByRole('textbox', {name: 'Filter labels', ...h});
|
|
1782
|
+
const create = () => screen.getByRole('button', {name: 'Create label', ...h});
|
|
1783
|
+
const labelsMenu = () => screen.getByRole('menu', {name: /Labels/, ...h});
|
|
1784
|
+
|
|
1785
|
+
it('holds the header, the rows and the footer in a dialog named by the row, outside every menu', async () => {
|
|
1786
|
+
const {rootMenu, row, dialog} = await openLabels();
|
|
1787
|
+
expect(dialog).toHaveAttribute('aria-modal', 'true');
|
|
1788
|
+
expect(row).toHaveAttribute('aria-haspopup', 'dialog');
|
|
1789
|
+
expect(row).toHaveAttribute('aria-controls', dialog.id);
|
|
1790
|
+
const menu = labelsMenu();
|
|
1791
|
+
for (const part of [field(), menu, create()]) {
|
|
1792
|
+
expect(dialog).toContainElement(part);
|
|
1793
|
+
}
|
|
1794
|
+
// The sub-menu's menu holds only its rows, and the dialog sits beside the
|
|
1795
|
+
// root's menu, not inside it.
|
|
1796
|
+
expect(menu).not.toContainElement(field());
|
|
1797
|
+
expect(menu).not.toContainElement(create());
|
|
1798
|
+
expect(rootMenu).not.toContainElement(dialog);
|
|
1799
|
+
expect(rootMenu.closest('[popover]')).toContainElement(dialog);
|
|
1800
|
+
expect(
|
|
1801
|
+
field().compareDocumentPosition(menu) & Node.DOCUMENT_POSITION_FOLLOWING,
|
|
1802
|
+
).toBeTruthy();
|
|
1803
|
+
expect(
|
|
1804
|
+
menu.compareDocumentPosition(create()) & Node.DOCUMENT_POSITION_FOLLOWING,
|
|
1805
|
+
).toBeTruthy();
|
|
1806
|
+
});
|
|
1807
|
+
|
|
1808
|
+
it('makes the rows one Tab stop, and Tab and Shift+Tab walk the regions and wrap', async () => {
|
|
1809
|
+
const {row} = await openLabels();
|
|
1810
|
+
const menu = labelsMenu();
|
|
1811
|
+
expect(menu).toHaveAttribute('tabindex', '0');
|
|
1812
|
+
// A pointer open lands on the first row.
|
|
1813
|
+
const bug = screen.getByRole('menuitem', {name: 'Bug', ...h});
|
|
1814
|
+
await waitFor(() => expect(bug).toHaveFocus());
|
|
1815
|
+
|
|
1816
|
+
fireEvent.keyDown(bug, {key: 'Tab'});
|
|
1817
|
+
expect(create()).toHaveFocus();
|
|
1818
|
+
fireEvent.keyDown(create(), {key: 'Tab'});
|
|
1819
|
+
expect(field()).toHaveFocus();
|
|
1820
|
+
fireEvent.keyDown(field(), {key: 'Tab'});
|
|
1821
|
+
expect(menu).toHaveFocus();
|
|
1822
|
+
fireEvent.keyDown(menu, {key: 'Tab', shiftKey: true});
|
|
1823
|
+
expect(field()).toHaveFocus();
|
|
1824
|
+
fireEvent.keyDown(field(), {key: 'Tab', shiftKey: true});
|
|
1825
|
+
expect(create()).toHaveFocus();
|
|
1826
|
+
expect(row).toHaveAttribute('aria-expanded', 'true');
|
|
1827
|
+
});
|
|
1828
|
+
|
|
1829
|
+
it('keeps arrows and letters in the field; Escape there returns to the row and leaves the menu open', async () => {
|
|
1830
|
+
const {trigger, row} = await openLabels();
|
|
1831
|
+
act(() => field().focus());
|
|
1832
|
+
fireEvent.keyDown(field(), {key: 'ArrowLeft'});
|
|
1833
|
+
fireEvent.keyDown(field(), {key: 'b'});
|
|
1834
|
+
expect(field()).toHaveFocus();
|
|
1835
|
+
expect(row).toHaveAttribute('aria-expanded', 'true');
|
|
1836
|
+
|
|
1837
|
+
fireEvent.keyDown(field(), {key: 'Escape'});
|
|
1838
|
+
await waitFor(() => expect(row).toHaveAttribute('aria-expanded', 'false'));
|
|
1839
|
+
expect(row).toHaveFocus();
|
|
1840
|
+
expect(trigger).toHaveAttribute('aria-expanded', 'true');
|
|
1841
|
+
});
|
|
1842
|
+
|
|
1843
|
+
it('an Escape a control in the region already handled closes only that control: a menu in the header, a field that clears itself', async () => {
|
|
1844
|
+
// A field that clears itself on Escape claims the key.
|
|
1845
|
+
function ClearingField() {
|
|
1846
|
+
const [query, setQuery] = useState('bu');
|
|
1847
|
+
return (
|
|
1848
|
+
<input
|
|
1849
|
+
aria-label="Filter labels"
|
|
1850
|
+
value={query}
|
|
1851
|
+
onChange={e => setQuery(e.target.value)}
|
|
1852
|
+
onKeyDown={e => {
|
|
1853
|
+
if (e.key === 'Escape' && query !== '') {
|
|
1854
|
+
e.preventDefault();
|
|
1855
|
+
setQuery('');
|
|
1856
|
+
}
|
|
1857
|
+
}}
|
|
1858
|
+
/>
|
|
1859
|
+
);
|
|
1860
|
+
}
|
|
1861
|
+
const {user, trigger, row} = await openLabels({
|
|
1862
|
+
header: (
|
|
1863
|
+
<>
|
|
1864
|
+
<ClearingField />
|
|
1865
|
+
<DropdownMenu button={{label: 'Sort'}}>
|
|
1866
|
+
<DropdownMenuItem label="By name" onClick={() => {}} />
|
|
1867
|
+
</DropdownMenu>
|
|
1868
|
+
</>
|
|
1869
|
+
),
|
|
1870
|
+
});
|
|
1871
|
+
|
|
1872
|
+
// The header's own menu takes the first Escape and closes; the sub-menu
|
|
1873
|
+
// and the root menu stay open.
|
|
1874
|
+
const sort = screen.getByRole('button', {name: /Sort/, ...h});
|
|
1875
|
+
await user.click(sort);
|
|
1876
|
+
await waitFor(() => expect(sort).toHaveAttribute('aria-expanded', 'true'));
|
|
1877
|
+
const sortMenu = screen.getByRole('menu', {name: /Sort/, ...h});
|
|
1878
|
+
await waitFor(() => expect(sortMenu).toHaveFocus());
|
|
1879
|
+
fireEvent.keyDown(sortMenu, {key: 'Escape'});
|
|
1880
|
+
await waitFor(() => expect(sort).toHaveAttribute('aria-expanded', 'false'));
|
|
1881
|
+
expect(row).toHaveAttribute('aria-expanded', 'true');
|
|
1882
|
+
expect(trigger).toHaveAttribute('aria-expanded', 'true');
|
|
1883
|
+
|
|
1884
|
+
// A field that clears itself on Escape keeps that Escape too.
|
|
1885
|
+
act(() => field().focus());
|
|
1886
|
+
fireEvent.keyDown(field(), {key: 'Escape'});
|
|
1887
|
+
expect(field()).toHaveValue('');
|
|
1888
|
+
expect(field()).toHaveFocus();
|
|
1889
|
+
expect(row).toHaveAttribute('aria-expanded', 'true');
|
|
1890
|
+
|
|
1891
|
+
// The next Escape, which nothing in the region claims, returns to the row.
|
|
1892
|
+
fireEvent.keyDown(field(), {key: 'Escape'});
|
|
1893
|
+
await waitFor(() => expect(row).toHaveAttribute('aria-expanded', 'false'));
|
|
1894
|
+
expect(trigger).toHaveAttribute('aria-expanded', 'true');
|
|
1895
|
+
});
|
|
1896
|
+
|
|
1897
|
+
it('an Escape that ends an IME composition in the field closes nothing', async () => {
|
|
1898
|
+
const {trigger, row} = await openLabels();
|
|
1899
|
+
act(() => field().focus());
|
|
1900
|
+
fireEvent.keyDown(field(), {key: 'Escape', isComposing: true});
|
|
1901
|
+
fireEvent.keyDown(field(), {key: 'Escape', keyCode: 229});
|
|
1902
|
+
expect(row).toHaveAttribute('aria-expanded', 'true');
|
|
1903
|
+
expect(field()).toHaveFocus();
|
|
1904
|
+
// The next Escape, outside the composition, returns to the row.
|
|
1905
|
+
fireEvent.keyDown(field(), {key: 'Escape'});
|
|
1906
|
+
await waitFor(() => expect(row).toHaveAttribute('aria-expanded', 'false'));
|
|
1907
|
+
expect(trigger).toHaveAttribute('aria-expanded', 'true');
|
|
1908
|
+
});
|
|
1909
|
+
|
|
1910
|
+
it('a keyboard open lands on the first row, not the field', async () => {
|
|
1911
|
+
const user = userEvent.setup();
|
|
1912
|
+
render(<LabelsMenu />);
|
|
1913
|
+
const trigger = screen.getByRole('button', {name: /Session/});
|
|
1914
|
+
await user.click(trigger);
|
|
1915
|
+
const row = screen.getByRole('menuitem', {name: /Labels/, ...h});
|
|
1916
|
+
act(() => row.focus());
|
|
1917
|
+
fireEvent.keyDown(row, {key: 'ArrowRight'});
|
|
1918
|
+
await waitFor(() =>
|
|
1919
|
+
expect(screen.getByRole('menuitem', {name: 'Bug', ...h})).toHaveFocus(),
|
|
1920
|
+
);
|
|
1921
|
+
});
|
|
1922
|
+
|
|
1923
|
+
it('a press in a region acts on nothing in the menus and keeps both open', async () => {
|
|
1924
|
+
const onCreate = vi.fn();
|
|
1925
|
+
const onPick = vi.fn();
|
|
1926
|
+
const {trigger, row} = await openLabels({onCreate, onPick});
|
|
1927
|
+
fireEvent.pointerDown(field(), {pointerType: 'mouse', button: 0});
|
|
1928
|
+
fireEvent.pointerUp(field(), {pointerType: 'mouse', button: 0});
|
|
1929
|
+
fireEvent.click(field());
|
|
1930
|
+
fireEvent.click(create());
|
|
1931
|
+
expect(onCreate).toHaveBeenCalledTimes(1);
|
|
1932
|
+
expect(onPick).not.toHaveBeenCalled();
|
|
1933
|
+
expect(row).toHaveAttribute('aria-expanded', 'true');
|
|
1934
|
+
expect(trigger).toHaveAttribute('aria-expanded', 'true');
|
|
1935
|
+
});
|
|
1936
|
+
|
|
1937
|
+
it('Tab from a deeper flyout closes it and walks on from the rows', async () => {
|
|
1938
|
+
await openLabels();
|
|
1939
|
+
const more = screen.getByRole('menuitem', {name: /More/, ...h});
|
|
1940
|
+
act(() => more.focus());
|
|
1941
|
+
fireEvent.keyDown(more, {key: 'ArrowRight'});
|
|
1942
|
+
const later = await waitFor(() => {
|
|
1943
|
+
const el = screen.getByRole('menuitem', {name: 'Later', ...h});
|
|
1944
|
+
expect(el).toHaveFocus();
|
|
1945
|
+
return el;
|
|
1946
|
+
});
|
|
1947
|
+
fireEvent.keyDown(later, {key: 'Tab'});
|
|
1948
|
+
await waitFor(() => expect(more).toHaveAttribute('aria-expanded', 'false'));
|
|
1949
|
+
expect(create()).toHaveFocus();
|
|
1950
|
+
});
|
|
1951
|
+
|
|
1952
|
+
// Opens the Labels flyout by hover (timers faked), puts focus on `focusIn`,
|
|
1953
|
+
// and moves the pointer off until the hide delay has run out: the flyout
|
|
1954
|
+
// stays open while focus is inside.
|
|
1955
|
+
function hoverOpenAndLeave(
|
|
1956
|
+
focusIn: () => HTMLElement,
|
|
1957
|
+
header?: React.ReactNode,
|
|
1958
|
+
) {
|
|
1959
|
+
render(<LabelsMenu header={header} />);
|
|
1960
|
+
fireEvent.click(screen.getByRole('button', {name: /Session/}));
|
|
1961
|
+
const row = screen.getByRole('menuitem', {name: /Labels/, ...h});
|
|
1962
|
+
fireEvent.mouseEnter(row);
|
|
1963
|
+
act(() => {
|
|
1964
|
+
vi.advanceTimersByTime(500);
|
|
1965
|
+
});
|
|
1966
|
+
act(() => focusIn().focus());
|
|
1967
|
+
fireEvent.mouseLeave(screen.getByRole('dialog', {name: /Labels/, ...h}));
|
|
1968
|
+
fireEvent.mouseLeave(row);
|
|
1969
|
+
act(() => {
|
|
1970
|
+
vi.advanceTimersByTime(1000);
|
|
1971
|
+
});
|
|
1972
|
+
expect(row).toHaveAttribute('aria-expanded', 'true');
|
|
1973
|
+
return row;
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
it('a flyout the pointer left stays open while focus moves inside it, and closes once focus leaves it', async () => {
|
|
1977
|
+
vi.useFakeTimers({toFake: ['setTimeout', 'clearTimeout']});
|
|
1978
|
+
try {
|
|
1979
|
+
const row = hoverOpenAndLeave(field);
|
|
1980
|
+
await act(async () => create().focus());
|
|
1981
|
+
act(() => {
|
|
1982
|
+
vi.advanceTimersByTime(0);
|
|
1983
|
+
});
|
|
1984
|
+
expect(row).toHaveAttribute('aria-expanded', 'true');
|
|
1985
|
+
// Focus leaving closes it, with no further pointer movement, once the
|
|
1986
|
+
// move has completed.
|
|
1987
|
+
await act(async () =>
|
|
1988
|
+
screen.getByRole('menuitem', {name: 'Rename', ...h}).focus(),
|
|
1989
|
+
);
|
|
1990
|
+
act(() => {
|
|
1991
|
+
vi.advanceTimersByTime(0);
|
|
1992
|
+
});
|
|
1993
|
+
expect(row).toHaveAttribute('aria-expanded', 'false');
|
|
1994
|
+
} finally {
|
|
1995
|
+
vi.useRealTimers();
|
|
1996
|
+
}
|
|
1997
|
+
});
|
|
1998
|
+
|
|
1999
|
+
it('a flyout the pointer left stays open while the browser moves focus between its header controls, as an arrow in a radio group does', async () => {
|
|
2000
|
+
vi.useFakeTimers({toFake: ['setTimeout', 'clearTimeout']});
|
|
2001
|
+
try {
|
|
2002
|
+
const radio = (name: string) => screen.getByRole('radio', {name, ...h});
|
|
2003
|
+
const row = hoverOpenAndLeave(
|
|
2004
|
+
() => radio('Any label'),
|
|
2005
|
+
<RadioList label="Match" value="any" onChange={() => {}}>
|
|
2006
|
+
<RadioListItem label="Any label" value="any" />
|
|
2007
|
+
<RadioListItem label="All labels" value="all" />
|
|
2008
|
+
</RadioList>,
|
|
2009
|
+
);
|
|
2010
|
+
// The browser's own move, in its order: the blur, a microtask
|
|
2011
|
+
// checkpoint, then the focus on the next radio.
|
|
2012
|
+
fireEvent.focusOut(radio('Any label'));
|
|
2013
|
+
await act(async () => {
|
|
2014
|
+
await Promise.resolve();
|
|
2015
|
+
});
|
|
2016
|
+
act(() => radio('All labels').focus());
|
|
2017
|
+
act(() => {
|
|
2018
|
+
vi.advanceTimersByTime(0);
|
|
2019
|
+
});
|
|
2020
|
+
expect(row).toHaveAttribute('aria-expanded', 'true');
|
|
2021
|
+
expect(radio('All labels')).toHaveFocus();
|
|
2022
|
+
} finally {
|
|
2023
|
+
vi.useRealTimers();
|
|
2024
|
+
}
|
|
2025
|
+
});
|
|
2026
|
+
|
|
2027
|
+
it('a flyout with regions stays in the popover when the root header comes or goes, which moves the host', async () => {
|
|
2028
|
+
const user = userEvent.setup();
|
|
2029
|
+
const {rerender} = render(<LabelsMenu />);
|
|
2030
|
+
const trigger = screen.getByRole('button', {name: /Session/});
|
|
2031
|
+
await user.click(trigger);
|
|
2032
|
+
await waitFor(() =>
|
|
2033
|
+
expect(trigger).toHaveAttribute('aria-expanded', 'true'),
|
|
2034
|
+
);
|
|
2035
|
+
const rootMenu = screen.getByRole('menu', {name: 'Session', ...h});
|
|
2036
|
+
fireEvent.click(screen.getByRole('menuitem', {name: /Labels/, ...h}), {
|
|
2037
|
+
detail: 1,
|
|
2038
|
+
});
|
|
2039
|
+
await screen.findByRole('dialog', {name: /Labels/, ...h});
|
|
2040
|
+
const popover = rootMenu.closest<HTMLElement>('[popover]');
|
|
2041
|
+
// The host moves into the regions frame and back out; the flyout follows
|
|
2042
|
+
// the host that is mounted, never a detached one.
|
|
2043
|
+
for (const rootHeader of [
|
|
2044
|
+
<input key="search" aria-label="Search actions" />,
|
|
2045
|
+
undefined,
|
|
2046
|
+
]) {
|
|
2047
|
+
rerender(<LabelsMenu rootHeader={rootHeader} />);
|
|
2048
|
+
const dialog = await screen.findByRole('dialog', {name: /Labels/, ...h});
|
|
2049
|
+
expect(popover).toContainElement(dialog);
|
|
2050
|
+
}
|
|
2051
|
+
});
|
|
2052
|
+
|
|
2053
|
+
it('a nested flyout with regions closes with the flyout that holds its row', async () => {
|
|
2054
|
+
const user = userEvent.setup();
|
|
2055
|
+
render(
|
|
2056
|
+
<DropdownMenu button={{label: 'Session'}}>
|
|
2057
|
+
<DropdownMenuSubMenu
|
|
2058
|
+
label="Labels"
|
|
2059
|
+
header={<input aria-label="Filter labels" />}>
|
|
2060
|
+
<DropdownMenuItem label="Bug" onClick={() => {}} />
|
|
2061
|
+
<DropdownMenuSubMenu
|
|
2062
|
+
label="More"
|
|
2063
|
+
header={<input aria-label="Filter more" />}>
|
|
2064
|
+
<DropdownMenuItem label="Later" onClick={() => {}} />
|
|
2065
|
+
</DropdownMenuSubMenu>
|
|
2066
|
+
</DropdownMenuSubMenu>
|
|
2067
|
+
</DropdownMenu>,
|
|
2068
|
+
);
|
|
2069
|
+
await user.click(screen.getByRole('button', {name: /Session/}));
|
|
2070
|
+
const labelsRow = screen.getByRole('menuitem', {name: /Labels/, ...h});
|
|
2071
|
+
fireEvent.click(labelsRow, {detail: 1});
|
|
2072
|
+
await screen.findByRole('dialog', {name: /Labels/, ...h});
|
|
2073
|
+
const moreRow = screen.getByRole('menuitem', {name: /More/, ...h});
|
|
2074
|
+
fireEvent.click(moreRow, {detail: 1});
|
|
2075
|
+
const moreDialog = await screen.findByRole('dialog', {name: /More/, ...h});
|
|
2076
|
+
const morePopover = moreDialog.closest<HTMLElement>('[popover]');
|
|
2077
|
+
expect(morePopover).toHaveAttribute('popover-open', '');
|
|
2078
|
+
// Escape in the outer flyout's field closes that flyout, and the nested
|
|
2079
|
+
// one, which renders beside the root's menu, with it.
|
|
2080
|
+
act(() => field().focus());
|
|
2081
|
+
fireEvent.keyDown(field(), {key: 'Escape'});
|
|
2082
|
+
await waitFor(() =>
|
|
2083
|
+
expect(labelsRow).toHaveAttribute('aria-expanded', 'false'),
|
|
2084
|
+
);
|
|
2085
|
+
await waitFor(() =>
|
|
2086
|
+
expect(morePopover).not.toHaveAttribute('popover-open'),
|
|
2087
|
+
);
|
|
2088
|
+
});
|
|
2089
|
+
|
|
2090
|
+
it('a menu placed in a region keeps its own press: a mouse release outside it closes it and acts on nothing', async () => {
|
|
2091
|
+
const user = userEvent.setup();
|
|
2092
|
+
const onBug = vi.fn();
|
|
2093
|
+
const onNewest = vi.fn();
|
|
2094
|
+
render(
|
|
2095
|
+
<DropdownMenu button={{label: 'Session'}}>
|
|
2096
|
+
<DropdownMenuItem label="Rename" onClick={() => {}} />
|
|
2097
|
+
<DropdownMenuSubMenu
|
|
2098
|
+
label="Labels"
|
|
2099
|
+
header={
|
|
2100
|
+
<DropdownMenu button={{label: 'Sort'}}>
|
|
2101
|
+
<DropdownMenuItem label="Newest" onClick={onNewest} />
|
|
2102
|
+
</DropdownMenu>
|
|
2103
|
+
}>
|
|
2104
|
+
<DropdownMenuItem label="Bug" onClick={onBug} />
|
|
2105
|
+
</DropdownMenuSubMenu>
|
|
2106
|
+
</DropdownMenu>,
|
|
2107
|
+
);
|
|
2108
|
+
await user.click(screen.getByRole('button', {name: /Session/}));
|
|
2109
|
+
fireEvent.click(screen.getByRole('menuitem', {name: /Labels/, ...h}), {
|
|
2110
|
+
detail: 1,
|
|
2111
|
+
});
|
|
2112
|
+
await screen.findByRole('dialog', {name: /Labels/, ...h});
|
|
2113
|
+
const sort = screen.getByRole('button', {name: /Sort/, ...h});
|
|
2114
|
+
await user.click(sort);
|
|
2115
|
+
await waitFor(() => expect(sort).toHaveAttribute('aria-expanded', 'true'));
|
|
2116
|
+
const newest = screen.getByRole('menuitem', {name: 'Newest', ...h});
|
|
2117
|
+
const bug = screen.getByRole('menuitem', {name: 'Bug', ...h});
|
|
2118
|
+
fireEvent.pointerDown(newest, {
|
|
2119
|
+
pointerType: 'mouse',
|
|
2120
|
+
pointerId: 1,
|
|
2121
|
+
button: 0,
|
|
2122
|
+
});
|
|
2123
|
+
fireEvent.pointerMove(bug, {pointerType: 'mouse', pointerId: 1});
|
|
2124
|
+
fireEvent.pointerUp(bug, {pointerType: 'mouse', pointerId: 1, button: 0});
|
|
2125
|
+
expect(onBug).not.toHaveBeenCalled();
|
|
2126
|
+
expect(onNewest).not.toHaveBeenCalled();
|
|
2127
|
+
await waitFor(() => expect(sort).toHaveAttribute('aria-expanded', 'false'));
|
|
2128
|
+
});
|
|
2129
|
+
|
|
2130
|
+
it('a press from a menu row released on a row of the flyout acts on it, as in any flyout', async () => {
|
|
2131
|
+
const onPick = vi.fn();
|
|
2132
|
+
const {trigger} = await openLabels({onPick});
|
|
2133
|
+
const rename = screen.getByRole('menuitem', {name: 'Rename', ...h});
|
|
2134
|
+
const bug = screen.getByRole('menuitem', {name: 'Bug', ...h});
|
|
2135
|
+
fireEvent.pointerDown(rename, {
|
|
2136
|
+
pointerType: 'mouse',
|
|
2137
|
+
pointerId: 1,
|
|
2138
|
+
button: 0,
|
|
2139
|
+
});
|
|
2140
|
+
fireEvent.pointerMove(bug, {pointerType: 'mouse', pointerId: 1});
|
|
2141
|
+
fireEvent.pointerUp(bug, {pointerType: 'mouse', pointerId: 1, button: 0});
|
|
2142
|
+
expect(onPick).toHaveBeenCalledWith('Bug');
|
|
2143
|
+
expect(trigger).toHaveAttribute('aria-expanded', 'false');
|
|
2144
|
+
});
|
|
2145
|
+
|
|
2146
|
+
it("a mouse press from a menu row released on the flyout's field acts on nothing and closes, as outside the menu", async () => {
|
|
2147
|
+
const onPick = vi.fn();
|
|
2148
|
+
const {trigger} = await openLabels({onPick});
|
|
2149
|
+
const rename = screen.getByRole('menuitem', {name: 'Rename', ...h});
|
|
2150
|
+
fireEvent.pointerDown(rename, {
|
|
2151
|
+
pointerType: 'mouse',
|
|
2152
|
+
pointerId: 1,
|
|
2153
|
+
button: 0,
|
|
2154
|
+
});
|
|
2155
|
+
fireEvent.pointerMove(field(), {pointerType: 'mouse', pointerId: 1});
|
|
2156
|
+
fireEvent.pointerUp(field(), {
|
|
2157
|
+
pointerType: 'mouse',
|
|
2158
|
+
pointerId: 1,
|
|
2159
|
+
button: 0,
|
|
2160
|
+
});
|
|
2161
|
+
expect(onPick).not.toHaveBeenCalled();
|
|
2162
|
+
expect(trigger).toHaveAttribute('aria-expanded', 'false');
|
|
2163
|
+
});
|
|
2164
|
+
|
|
2165
|
+
it('without a header or footer the sub-menu is a plain flyout menu', async () => {
|
|
2166
|
+
const user = userEvent.setup();
|
|
2167
|
+
render(<MoveMenu />);
|
|
2168
|
+
await user.click(screen.getByRole('button', {name: /Actions/}));
|
|
2169
|
+
const row = screen.getByRole('menuitem', {name: /Move to/, ...h});
|
|
2170
|
+
fireEvent.click(row, {detail: 1});
|
|
2171
|
+
await waitFor(() => expect(row).toHaveAttribute('aria-expanded', 'true'));
|
|
2172
|
+
expect(row).toHaveAttribute('aria-haspopup', 'menu');
|
|
2173
|
+
expect(screen.queryByRole('dialog', h)).toBeNull();
|
|
2174
|
+
});
|
|
2175
|
+
|
|
2176
|
+
describe('on a phone', () => {
|
|
2177
|
+
const originalMatchMedia = window.matchMedia;
|
|
2178
|
+
beforeEach(() => {
|
|
2179
|
+
vi.stubGlobal(
|
|
2180
|
+
'matchMedia',
|
|
2181
|
+
vi.fn().mockImplementation((query: string) => ({
|
|
2182
|
+
matches: query === COMPACT_TOUCH_PRESENTATION_QUERY,
|
|
2183
|
+
media: query,
|
|
2184
|
+
onchange: null,
|
|
2185
|
+
addEventListener: vi.fn(),
|
|
2186
|
+
removeEventListener: vi.fn(),
|
|
2187
|
+
addListener: vi.fn(),
|
|
2188
|
+
removeListener: vi.fn(),
|
|
2189
|
+
dispatchEvent: vi.fn(),
|
|
2190
|
+
})),
|
|
2191
|
+
);
|
|
2192
|
+
});
|
|
2193
|
+
afterEach(() => {
|
|
2194
|
+
vi.stubGlobal('matchMedia', originalMatchMedia);
|
|
2195
|
+
});
|
|
2196
|
+
|
|
2197
|
+
it('the drilled-in view holds the regions beside its menu while the root steps aside, and Escape in its field returns', async () => {
|
|
2198
|
+
const {rootMenu, row, dialog} = await openLabels({
|
|
2199
|
+
rootHeader: <input aria-label="Search actions" />,
|
|
2200
|
+
});
|
|
2201
|
+
const rootField = screen.getByRole('textbox', {
|
|
2202
|
+
name: 'Search actions',
|
|
2203
|
+
...h,
|
|
2204
|
+
});
|
|
2205
|
+
expect(isShown(dialog)).toBe(true);
|
|
2206
|
+
expect(dialog).toHaveAttribute('aria-modal', 'true');
|
|
2207
|
+
expect(row).toHaveAttribute('aria-haspopup', 'dialog');
|
|
2208
|
+
// The view sits beside the root's menu, which steps aside with its own
|
|
2209
|
+
// header; the view leads with a Back row.
|
|
2210
|
+
expect(rootMenu).not.toContainElement(dialog);
|
|
2211
|
+
expect(isShown(rootMenu)).toBe(false);
|
|
2212
|
+
expect(isShown(rootField)).toBe(false);
|
|
2213
|
+
expect(dialog.closest('[popover]')).toBe(rootMenu.closest('[popover]'));
|
|
2214
|
+
const viewRows = Array.from(
|
|
2215
|
+
labelsMenu().querySelectorAll('[role="menuitem"]'),
|
|
2216
|
+
)
|
|
2217
|
+
.filter(el => isShown(el as HTMLElement))
|
|
2218
|
+
.map(el => el.textContent);
|
|
2219
|
+
expect(viewRows[0]).toBe('Back to Session');
|
|
2220
|
+
for (const part of [field(), labelsMenu(), create()]) {
|
|
2221
|
+
expect(dialog).toContainElement(part);
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
// Escape in the field returns to the row; the root comes back.
|
|
2225
|
+
act(() => field().focus());
|
|
2226
|
+
fireEvent.keyDown(field(), {key: 'Escape'});
|
|
2227
|
+
await waitFor(() => expect(row).toHaveFocus());
|
|
2228
|
+
expect(isShown(rootMenu)).toBe(true);
|
|
2229
|
+
expect(isShown(rootField)).toBe(true);
|
|
2230
|
+
});
|
|
2231
|
+
|
|
2232
|
+
it('a shown view follows its header or footer as it comes and goes: beside the root menu with one, inside it without', async () => {
|
|
2233
|
+
function TogglingLabels() {
|
|
2234
|
+
const [hasHeader, setHasHeader] = useState(true);
|
|
2235
|
+
return (
|
|
2236
|
+
<>
|
|
2237
|
+
<button type="button" onClick={() => setHasHeader(v => !v)}>
|
|
2238
|
+
Toggle filter
|
|
2239
|
+
</button>
|
|
2240
|
+
<DropdownMenu button={{label: 'Session'}}>
|
|
2241
|
+
<DropdownMenuItem label="Rename" onClick={() => {}} />
|
|
2242
|
+
<DropdownMenuSubMenu
|
|
2243
|
+
label="Labels"
|
|
2244
|
+
header={
|
|
2245
|
+
hasHeader ? <input aria-label="Filter labels" /> : undefined
|
|
2246
|
+
}>
|
|
2247
|
+
<DropdownMenuItem label="Bug" onClick={() => {}} />
|
|
2248
|
+
</DropdownMenuSubMenu>
|
|
2249
|
+
</DropdownMenu>
|
|
2250
|
+
</>
|
|
2251
|
+
);
|
|
2252
|
+
}
|
|
2253
|
+
const user = userEvent.setup();
|
|
2254
|
+
render(<TogglingLabels />);
|
|
2255
|
+
await user.click(screen.getByRole('button', {name: /Session/}));
|
|
2256
|
+
const rootMenu = screen.getByRole('menu', {name: 'Session', ...h});
|
|
2257
|
+
fireEvent.click(screen.getByRole('menuitem', {name: /Labels/, ...h}), {
|
|
2258
|
+
detail: 1,
|
|
2259
|
+
});
|
|
2260
|
+
await screen.findByRole('dialog', {name: /Labels/, ...h});
|
|
2261
|
+
expect(isShown(rootMenu)).toBe(false);
|
|
2262
|
+
const toggle = screen.getByRole('button', {name: 'Toggle filter'});
|
|
2263
|
+
const bugIsShown = () =>
|
|
2264
|
+
isShown(screen.getByRole('menuitem', {name: 'Bug', ...h}));
|
|
2265
|
+
|
|
2266
|
+
// The header goes: the view drills in inside the root's menu, which
|
|
2267
|
+
// shows again with the view's rows.
|
|
2268
|
+
fireEvent.click(toggle);
|
|
2269
|
+
await waitFor(() =>
|
|
2270
|
+
expect(screen.queryByRole('dialog', {name: /Labels/, ...h})).toBeNull(),
|
|
2271
|
+
);
|
|
2272
|
+
expect(isShown(rootMenu)).toBe(true);
|
|
2273
|
+
expect(bugIsShown()).toBe(true);
|
|
2274
|
+
|
|
2275
|
+
// It comes back: the view moves beside the root's menu, which steps
|
|
2276
|
+
// aside again.
|
|
2277
|
+
fireEvent.click(toggle);
|
|
2278
|
+
const dialog = await screen.findByRole('dialog', {
|
|
2279
|
+
name: /Labels/,
|
|
2280
|
+
...h,
|
|
2281
|
+
});
|
|
2282
|
+
expect(isShown(dialog)).toBe(true);
|
|
2283
|
+
expect(isShown(rootMenu)).toBe(false);
|
|
2284
|
+
expect(bugIsShown()).toBe(true);
|
|
2285
|
+
});
|
|
2286
|
+
|
|
2287
|
+
it("a drilled-in view with a header or footer caps at the root's menuMaxHeight, as the plain view does", async () => {
|
|
2288
|
+
const {dialog} = await openLabels({menuMaxHeight: 560});
|
|
2289
|
+
expect(dialog.getAttribute('style')).toContain('min(560px');
|
|
2290
|
+
});
|
|
2291
|
+
|
|
2292
|
+
it('Tab in the drilled-in view wraps through its own regions', async () => {
|
|
2293
|
+
await openLabels();
|
|
2294
|
+
act(() => field().focus());
|
|
2295
|
+
fireEvent.keyDown(field(), {key: 'Tab'});
|
|
2296
|
+
expect(labelsMenu()).toHaveFocus();
|
|
2297
|
+
fireEvent.keyDown(labelsMenu(), {key: 'Tab'});
|
|
2298
|
+
expect(create()).toHaveFocus();
|
|
2299
|
+
fireEvent.keyDown(create(), {key: 'Tab'});
|
|
2300
|
+
expect(field()).toHaveFocus();
|
|
2301
|
+
});
|
|
2302
|
+
});
|
|
2303
|
+
});
|