Skip to content

Commit

Permalink
fix(menu-full-screen, modal): ensure the call to action element is fo…
Browse files Browse the repository at this point in the history
…cused on close

Ensures the element which opens `Modal` or a fullscreen `Menu` is focused when the
respective component closes.

fix #6870
  • Loading branch information
tomdavies73 committed Oct 24, 2024
1 parent e4bf21a commit 0ca3949
Show file tree
Hide file tree
Showing 11 changed files with 378 additions and 6 deletions.
4 changes: 3 additions & 1 deletion src/components/dialog-full-screen/components.test-pw.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,14 @@ const nestedDialogTitle = "Nested Dialog";

export const DialogFullScreenComponent = ({
children = "This is an example",
open = true,
...props
}: Partial<DialogFullScreenProps>) => {
const [isOpen, setIsOpen] = useState(true);
const [isOpen, setIsOpen] = useState(open);
const ref = useRef<HTMLButtonElement | null>(null);
return (
<>
<Button onClick={() => setIsOpen(true)}>Open Dialog Full Screen</Button>
<DialogFullScreen
focusFirstElement={ref}
open={isOpen}
Expand Down
76 changes: 76 additions & 0 deletions src/components/dialog-full-screen/dialog-full-screen.pw.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,82 @@ test.describe("render DialogFullScreen component and check properties", () => {
);
});

test("when Dialog Full Screen is opened and then closed, the call to action element should be focused", async ({
mount,
page,
}) => {
await mount(<DialogFullScreenComponent open={false} />);

const button = page
.getByRole("button")
.filter({ hasText: "Open Dialog Full Screen" });
const dialogFullScreen = page.getByRole("dialog");
await expect(button).not.toBeFocused();
await expect(dialogFullScreen).not.toBeVisible();

await button.click();
await expect(dialogFullScreen).toBeVisible();
const closeButton = page.getByLabel("Close");
await closeButton.click();
await expect(button).toBeFocused();
await expect(dialogFullScreen).not.toBeVisible();
});

test("when Dialog Full Screen is open on render, then closed, opened and then closed again, the call to action element should be focused", async ({
mount,
page,
}) => {
await mount(<DialogFullScreenComponent />);

const dialogFullScreen = page.getByRole("dialog");
await expect(dialogFullScreen).toBeVisible();
const closeButton = page.getByLabel("Close");
await closeButton.click();

const button = page
.getByRole("button")
.filter({ hasText: "Open Dialog Full Screen" });
await expect(button).not.toBeFocused();
await expect(dialogFullScreen).not.toBeVisible();

await button.click();
await expect(dialogFullScreen).toBeVisible();
await closeButton.click();
await expect(button).toBeFocused();
});

test("when nested Dialog's are opened/closed their respective call to action elements should be focused correctly", async ({
mount,
page,
}) => {
await mount(<NestedDialog />);

const firstButton = page
.getByRole("button")
.filter({ hasText: "Open Main Dialog" });
const firstDialog = page.getByRole("dialog").first();
await expect(firstButton).not.toBeFocused();
await expect(firstDialog).not.toBeVisible();

await firstButton.click();
await expect(firstDialog).toBeVisible();
const secondButton = page
.getByRole("button")
.filter({ hasText: "Open Nested Dialog" });
await expect(secondButton).not.toBeFocused();
await secondButton.click();
const secondDialog = page.getByRole("dialog").last();
await expect(secondDialog).toBeVisible();

const secondCloseButton = page.getByLabel("Close").last();
await secondCloseButton.click();
await expect(secondButton).toBeFocused();

const firstCloseButton = page.getByLabel("Close").first();
await firstCloseButton.click();
await expect(firstButton).toBeFocused();
});

test("should render component with autofocus disabled", async ({
mount,
page,
Expand Down
37 changes: 35 additions & 2 deletions src/components/dialog/components.test-pw.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -210,8 +210,12 @@ export const DialogComponentFocusableSelectors = (
);
};

export const DefaultStory = () => {
const [isOpen, setIsOpen] = useState(defaultOpenState);
export const DefaultStory = ({
open = defaultOpenState,
}: {
open?: boolean;
}) => {
const [isOpen, setIsOpen] = useState(open);
return (
<>
<Button onClick={() => setIsOpen(true)}>Open Dialog</Button>
Expand Down Expand Up @@ -253,6 +257,35 @@ export const DefaultStory = () => {
);
};

export const DefaultNestedStory = () => {
const [isFirstDialogOpen, setIsFirstDialogOpen] = useState(false);
const [isNestedDialogOpen, setIsNestedDialogOpen] = useState(false);

return (
<>
<Button onClick={() => setIsFirstDialogOpen(true)}>
Open First Dialog
</Button>
<Dialog
open={isFirstDialogOpen}
onCancel={() => setIsFirstDialogOpen(false)}
title="First Dialog"
>
<Button onClick={() => setIsNestedDialogOpen(true)}>
Open Nested Dialog
</Button>
<Dialog
open={isNestedDialogOpen}
onCancel={() => setIsNestedDialogOpen(false)}
title="Nested Dialog"
>
<Textbox label="Nested Dialog Textbox" />
</Dialog>
</Dialog>
</>
);
};

export const Editable = () => {
const [isOpen, setIsOpen] = useState(defaultOpenState);
const [isDisabled, setIsDisabled] = useState(true);
Expand Down
73 changes: 73 additions & 0 deletions src/components/dialog/dialog.pw.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
DialogWithAutoFocusSelect,
DialogComponentFocusableSelectors,
DefaultStory,
DefaultNestedStory,
Editable,
WithHelp,
LoadingContent,
Expand Down Expand Up @@ -249,6 +250,78 @@ test.describe("Testing Dialog component properties", () => {
).toBeFocused();
});

test("when Dialog is opened and then closed, the call to action element should be focused", async ({
mount,
page,
}) => {
await mount(<DefaultStory />);

const button = page.getByRole("button").filter({ hasText: "Open Dialog" });
const dialog = page.getByRole("dialog");
await expect(button).not.toBeFocused();
await expect(dialog).not.toBeVisible();

await button.click();
await expect(dialog).toBeVisible();
const closeButton = page.getByLabel("Close");
await closeButton.click();
await expect(button).toBeFocused();
await expect(dialog).not.toBeVisible();
});

test("when Dialog is open on render, then closed, opened and then closed again, the call to action element should be focused", async ({
mount,
page,
}) => {
await mount(<DefaultStory open />);

const dialog = page.getByRole("dialog");
await expect(dialog).toBeVisible();
const closeButton = page.getByLabel("Close");
await closeButton.click();

const button = page.getByRole("button").filter({ hasText: "Open Dialog" });
await expect(button).not.toBeFocused();
await expect(dialog).not.toBeVisible();

await button.click();
await expect(dialog).toBeVisible();
await closeButton.click();
await expect(button).toBeFocused();
});

test("when nested Dialog's are opened/closed their respective call to action elements should be focused correctly", async ({
mount,
page,
}) => {
await mount(<DefaultNestedStory />);

const firstButton = page
.getByRole("button")
.filter({ hasText: "Open First Dialog" });
const firstDialog = page.getByRole("dialog").first();
await expect(firstButton).not.toBeFocused();
await expect(firstDialog).not.toBeVisible();

await firstButton.click();
await expect(firstDialog).toBeVisible();
const secondButton = page
.getByRole("button")
.filter({ hasText: "Open Nested Dialog" });
await expect(secondButton).not.toBeFocused();
await secondButton.click();
const secondDialog = page.getByRole("dialog").last();
await expect(secondDialog).toBeVisible();

const secondCloseButton = page.getByLabel("Close").last();
await secondCloseButton.click();
await expect(secondButton).toBeFocused();

const firstCloseButton = page.getByLabel("Close").first();
await firstCloseButton.click();
await expect(firstButton).toBeFocused();
});

test("when disableAutoFocus prop is passed, the first focusable element should not be focused", async ({
mount,
page,
Expand Down
24 changes: 24 additions & 0 deletions src/components/menu/component.test-pw.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,30 @@ export const MenuComponentFullScreen = (
);
};

export const MenuComponentFullScreenSimple = ({
open = true,
}: {
open?: boolean;
}) => {
const [menuOpen, setMenuOpen] = useState(open);

return (
<Menu menuType="light">
<MenuItem key="menu-item" onClick={() => setMenuOpen(true)}>
Menu
</MenuItem>
<MenuFullscreen
key="menu"
isOpen={menuOpen}
onClose={() => setMenuOpen(false)}
>
<MenuItem href="#">Menu Item One</MenuItem>
<MenuItem href="#">Menu Item Two</MenuItem>
</MenuFullscreen>
</Menu>
);
};

export const MenuComponentFullScreenWithLongSubmenuText = (
props: Partial<MenuFullscreenProps>
) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export const MenuFullscreen = ({
closeModal,
modalRef: menuRef,
topModalOverride,
focusCallToActionElement: document.activeElement as HTMLElement,
});

return (
Expand Down
41 changes: 41 additions & 0 deletions src/components/menu/menu.pw.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import {
MenuComponentSearch,
MenuWithChildrenUpdating,
MenuComponentFullScreen,
MenuComponentFullScreenSimple,
MenuFullScreenBackgroundScrollTest,
MenuComponentItems,
MenuFullScreenWithSearchButton,
Expand Down Expand Up @@ -1062,6 +1063,46 @@ test.describe("Prop tests for Menu component", () => {
}
);

test("when a Menu Fullscreen is opened and then closed, the call to action element should be focused", async ({
mount,
page,
}) => {
await mount(<MenuComponentFullScreenSimple open={false} />);

await page.setViewportSize({ width: 1200, height: 800 });
const item = page.getByRole("button").filter({ hasText: "Menu" });
await item.click();
const fullscreen = getComponent(page, "menu-fullscreen");
await waitForAnimationEnd(fullscreen);
const closeButton = page.getByLabel("Close");
await closeButton.click();
await expect(item).toBeFocused();
});

test("when Menu Fullscreen is open on render, then closed, opened and then closed again, the call to action element should be focused", async ({
mount,
page,
}) => {
await mount(<MenuComponentFullScreenSimple />);

await page.setViewportSize({ width: 1200, height: 800 });
const fullscreen = getComponent(page, "menu-fullscreen");
await waitForAnimationEnd(fullscreen);
await expect(fullscreen).toBeVisible();
const closeButton = page.getByLabel("Close");
await closeButton.click();

const item = page.getByRole("button").filter({ hasText: "Menu" });
await expect(item).not.toBeFocused();
await expect(fullscreen).not.toBeVisible();

await item.click();
await waitForAnimationEnd(fullscreen);
await expect(fullscreen).toBeVisible();
await closeButton.click();
await expect(item).toBeFocused();
});

// TODO: Skipped due to flaky focus behaviour. To review in FE-6428
test.skip(`should verify that inner Menu without link is NOT available with tabbing in Fullscreen Menu`, async ({
mount,
Expand Down
1 change: 1 addition & 0 deletions src/components/modal/modal.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ const Modal = ({
modalRef: ref,
setTriggerRefocusFlag,
topModalOverride,
focusCallToActionElement: document.activeElement as HTMLElement,
});

let background;
Expand Down
37 changes: 34 additions & 3 deletions src/components/sidebar/components.test-pw.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ import Toast from "../toast";
import Textbox from "../textbox";
import Dialog from "../dialog";

export const Default = (args: Partial<SidebarProps>) => {
const [isOpen, setIsOpen] = useState(true);
export const Default = ({ open = true }: { open?: boolean }) => {
const [isOpen, setIsOpen] = useState(open);
const onCancel = () => {
setIsOpen(false);
};
return (
<>
<Button onClick={() => setIsOpen(true)}>Open sidebar</Button>
<Sidebar {...args} aria-label="sidebar" open={isOpen} onCancel={onCancel}>
<Sidebar aria-label="sidebar" open={isOpen} onCancel={onCancel}>
<Box mb={2}>
<Button buttonType="primary">Test</Button>
<Button buttonType="secondary" ml={2}>
Expand All @@ -29,6 +29,37 @@ export const Default = (args: Partial<SidebarProps>) => {
);
};

export const DefaultNested = () => {
const [isFirstSidebarOpen, setIsFirstSidebarOpen] = useState(false);
const [isNestedSidebarOpen, setIsNestedSidebarOpen] = useState(false);
return (
<>
<Button onClick={() => setIsFirstSidebarOpen(true)}>
Open First Sidebar
</Button>
<Sidebar
open={isFirstSidebarOpen}
onCancel={() => setIsFirstSidebarOpen(false)}
>
<Button onClick={() => setIsNestedSidebarOpen(true)}>
Open Nested Sidebar
</Button>
<Sidebar
open={isNestedSidebarOpen}
onCancel={() => setIsNestedSidebarOpen(false)}
>
<Box mb={2}>
<Button buttonType="primary">Test</Button>
<Button buttonType="secondary" ml={2}>
Last
</Button>
</Box>
</Sidebar>
</Sidebar>
</>
);
};

export const SidebarComponent = (props: Partial<SidebarProps>) => {
return (
<>
Expand Down
Loading

0 comments on commit 0ca3949

Please sign in to comment.