Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 29 additions & 7 deletions web/src/components/core/Sidebar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
* find current contact information at www.suse.com.
*/

import React, { useEffect, useRef, useState } from "react";
import React, { useEffect, useLayoutEffect, useRef, useState } from "react";
import { Button, Text } from "@patternfly/react-core";
import { Icon, AppActions } from "~/components/layout";
import { If, NotificationMark } from "~/components/core";
Expand All @@ -32,9 +32,9 @@ import { useNotification } from "~/context/notification";
* @returns {HTMLElement[]}
*/
const siblingsFor = (node) => {
if (!node) return [];
const parent = node?.parentNode;

return [...node.parentNode.children].filter(n => n !== node);
return parent ? [...parent.children].filter(n => n !== node) : [];
};

/**
Expand All @@ -50,22 +50,36 @@ export default function Sidebar ({ children }) {
const closeButtonRef = useRef(null);
const [notification] = useNotification();

const open = () => {
setIsOpen(true);
/**
* Set siblings as not interactive and not discoverable
*/
const makeSiblingsInert = () => {
siblingsFor(asideRef.current).forEach(s => {
s.setAttribute('inert', '');
s.setAttribute('aria-hidden', true);
});
};

const close = () => {
setIsOpen(false);
/**
* Set siblings as interactive and discoverable
*/
const makeSiblingsAlive = () => {
siblingsFor(asideRef.current).forEach(s => {
s.removeAttribute('inert');
s.removeAttribute('aria-hidden');
});
};

const open = () => {
setIsOpen(true);
makeSiblingsInert();
};

const close = () => {
setIsOpen(false);
makeSiblingsAlive();
};

/**
* Handler for automatically closing the sidebar when a click bubbles from a
* children of its content.
Expand All @@ -86,6 +100,14 @@ export default function Sidebar ({ children }) {
if (isOpen) closeButtonRef.current.focus();
}, [isOpen]);

useLayoutEffect(() => {
// Ensure siblings do not remain inert when the component is unmounted.
// Using useLayoutEffect over useEffect for allowing the cleanup function to
// be executed immediately BEFORE unmounting the component and still having
// access to siblings.
return () => makeSiblingsAlive();
}, []);

// display additional info when running in a development server
let targetInfo = null;
if (process.env.WEBPACK_SERVE) {
Expand Down
76 changes: 54 additions & 22 deletions web/src/components/core/Sidebar.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
import React from "react";
import { screen, within } from "@testing-library/react";
import { installerRender, mockLayout, mockComponent, withNotificationProvider } from "~/test-utils";
import { Sidebar } from "~/components/core";
import { If, Sidebar } from "~/components/core";
import { createClient } from "~/client";

// Mock layout
Expand Down Expand Up @@ -78,27 +78,6 @@ it("renders a link for hiding the sidebar", async () => {
expect(sidebar).toHaveAttribute("data-state", "hidden");
});

it("sets siblings as inert and aria-hidden while it's open", async () => {
const { user } = installerRender(withNotificationProvider(
<>
<div>A sidebar sibling</div>
<Sidebar />
</>
));

const openLink = await screen.findByLabelText(/Show/i);
const closeLink = await screen.findByLabelText(/Hide/i);
const sidebarSibling = screen.getByText("A sidebar sibling");
expect(sidebarSibling).not.toHaveAttribute("aria-hidden");
expect(sidebarSibling).not.toHaveAttribute("inert");
await user.click(openLink);
expect(sidebarSibling).toHaveAttribute("aria-hidden");
expect(sidebarSibling).toHaveAttribute("inert");
await user.click(closeLink);
expect(sidebarSibling).not.toHaveAttribute("aria-hidden");
expect(sidebarSibling).not.toHaveAttribute("inert");
});

it("moves the focus to the close action after opening it", async () => {
const { user } = installerRender(withNotificationProvider(<Sidebar />));

Expand Down Expand Up @@ -182,3 +161,56 @@ describe("if there are not issues", () => {
expect(mark).toBeNull();
});
});

describe("side effects on siblings", () => {
const SidebarWithSiblings = () => {
const [isSidebarMount, setIsSidebarMount] = React.useState(true);

// NOTE: using the "data-keep-sidebar-open" to avoid triggering the #close
// function before unmounting the component.
const Content = () => (
<button data-keep-sidebar-open onClick={() => setIsSidebarMount(false)}>
Unmount Sidebar
</button>
);

return (
<>
<article>A sidebar sibling</article>
<If condition={isSidebarMount} then={<Sidebar><Content /></Sidebar>} />
</>
);
};

it("sets siblings as inert and aria-hidden while it's open", async () => {
const { user } = installerRender(withNotificationProvider(<SidebarWithSiblings />));

const openLink = await screen.findByLabelText(/Show/i);
const closeLink = await screen.findByLabelText(/Hide/i);
const sidebarSibling = screen.getByText("A sidebar sibling");
expect(sidebarSibling).not.toHaveAttribute("aria-hidden");
expect(sidebarSibling).not.toHaveAttribute("inert");
await user.click(openLink);
expect(sidebarSibling).toHaveAttribute("aria-hidden");
expect(sidebarSibling).toHaveAttribute("inert");
await user.click(closeLink);
expect(sidebarSibling).not.toHaveAttribute("aria-hidden");
expect(sidebarSibling).not.toHaveAttribute("inert");
});

it("removes inert and aria-hidden siblings attributes if it's unmounted", async () => {
const { user } = installerRender(withNotificationProvider(<SidebarWithSiblings />));

const openLink = await screen.findByLabelText(/Show/i);
const unmountButton = await screen.getByRole("button", { name: "Unmount Sidebar" });
const sidebarSibling = screen.getByText("A sidebar sibling");
expect(sidebarSibling).not.toHaveAttribute("aria-hidden");
expect(sidebarSibling).not.toHaveAttribute("inert");
await user.click(openLink);
expect(sidebarSibling).toHaveAttribute("aria-hidden");
expect(sidebarSibling).toHaveAttribute("inert");
await user.click(unmountButton);
expect(sidebarSibling).not.toHaveAttribute("aria-hidden");
expect(sidebarSibling).not.toHaveAttribute("inert");
});
});