diff --git a/web/package/cockpit-d-installer.changes b/web/package/cockpit-d-installer.changes index 6db535e5ee..8e53048a25 100644 --- a/web/package/cockpit-d-installer.changes +++ b/web/package/cockpit-d-installer.changes @@ -1,3 +1,12 @@ +------------------------------------------------------------------- +Mon Mar 13 15:51:08 UTC 2023 - David Diaz + +- Sidebar improvements (gh#yast/d-installer#462) + * Allow adding actions from a page. + * Remove network information. + * Use underlined links and darker green color for improving contrast. + * Start using a disclosure widget for grouping related actions. + ------------------------------------------------------------------- Fri Mar 3 15:00:05 UTC 2023 - David Diaz diff --git a/web/src/App.test.jsx b/web/src/App.test.jsx index 113113ba3d..9adae67c95 100644 --- a/web/src/App.test.jsx +++ b/web/src/App.test.jsx @@ -1,5 +1,5 @@ /* - * Copyright (c) [2022] SUSE LLC + * Copyright (c) [2022-2023] SUSE LLC * * All Rights Reserved. * @@ -28,11 +28,6 @@ import { STARTUP, CONFIG, INSTALL } from "~/client/phase"; import { IDLE, BUSY } from "~/client/status"; jest.mock("~/client"); - -jest.mock('react-router-dom', () => ({ - Outlet: mockComponent("Content"), -})); - jest.mock("~/components/layout/Layout", () => mockLayout()); // Mock some components, @@ -147,7 +142,7 @@ describe("App", () => { it("renders the application content", async () => { installerRender(); - await screen.findByText("Content"); + await screen.findByText(/Outlet Content/); }); }); @@ -169,7 +164,7 @@ describe("App", () => { it("renders the Installation component on the INSTALL phase", async () => { installerRender(); - await screen.findByText("Content"); + await screen.findByText(/Outlet Content/); changePhaseTo(INSTALL); await screen.findByText("Installation Mock"); }); @@ -183,7 +178,7 @@ describe("App", () => { it("renders the application's content", async () => { installerRender(); - await screen.findByText("Content"); + await screen.findByText(/Outlet Content/); }); }); }); diff --git a/web/src/Main.test.jsx b/web/src/Main.test.jsx index fe874a3783..eb8e337a5a 100644 --- a/web/src/Main.test.jsx +++ b/web/src/Main.test.jsx @@ -1,5 +1,5 @@ /* - * Copyright (c) [2022] SUSE LLC + * Copyright (c) [2022-2023] SUSE LLC * * All Rights Reserved. * @@ -21,18 +21,16 @@ import React from "react"; import { screen } from "@testing-library/react"; -import { installerRender, mockComponent } from "~/test-utils"; +import { plainRender, mockComponent } from "~/test-utils"; import Main from "~/Main"; jest.mock("~/components/questions/Questions", () => mockComponent("Questions Mock")); -jest.mock('react-router-dom', () => ({ - Outlet: mockComponent("Content"), -})); it("renders the Questions component and the content", async () => { - installerRender(
); + plainRender(
); await screen.findByText("Questions Mock"); - await screen.findByText("Content"); + // react-router-dom Outlet is mocked. See test-utils for more details + await screen.findByText("Outlet Content"); }); diff --git a/web/src/assets/styles/blocks.scss b/web/src/assets/styles/blocks.scss index a43b9ec920..1ebd036c82 100644 --- a/web/src/assets/styles/blocks.scss +++ b/web/src/assets/styles/blocks.scss @@ -32,7 +32,7 @@ section > h2 { transition: all 0.15s ease-in-out; &:hover { - color: var(--color-primary); + color: var(--color-link-hover); } } } @@ -72,7 +72,7 @@ section > .content { } .sidebar { - --color-background-primary: var(--color-primary); + --color-background-primary: var(--color-primary-lighter); --wrapper-background: var(--color-gray-light); position: absolute; @@ -80,7 +80,7 @@ section > .content { right: 0; z-index: 1; inline-size: 70%; - box-shadow: 0 0 20px 10px var(--color-primary-darkest); + box-shadow: 0 0 20px 10px var(--color-primary); } .sidebar header { @@ -91,6 +91,43 @@ section > .content { border-top: 1px solid var(--color-gray); } +.sidebar > div { + margin-inline-start: var(--pf-global--spacer--md); +} + +.sidebar a, .sidebar button { + font-size: 16px; + font-weight: var(--fw-bold); + text-decoration: underline; + text-underline-offset: 2px; + padding-block: 0; + + &:hover { + color: var(--color-link-hover); + text-decoration: underline; + + svg { + color: var(--color-link); + } + } + + svg { + color: var(--color-link); + vertical-align: text-bottom; + margin-block-end: -2px; + } +} + +.sidebar a { + margin-inline-start: var(--pf-global--spacer--md); + + // Keep links and buttons labels aligned by adding the same margin than + // .pf-c-button__icon.pf-m-start + svg { + margin-inline-end: var(--pf-global--spacer--xs); + } +} + // Remove not wanted PatternFly padding left on a loading link .sidebar button.pf-m-progress { --pf-c-button--m-progress--PaddingLeft: var(--pf-global--spacer--md); @@ -110,6 +147,37 @@ section > .content { transition: all 0.2s ease-in-out; } +.disclosure > button { + margin-inline-start: var(--pf-global--spacer--md); + display: inline-flex; + align-items: center; + // Keep links and buttons labels aligned by adding the same margin than + // .pf-c-button__icon.pf-m-start + svg { + margin-inline-end: var(--pf-global--spacer--xs); + transition: transform 0.2s ease-in-out; + } + + &[aria-expanded="true"] { + svg { + transform: rotate(90deg); + } + } + + &[aria-expanded="false"] + div { + display: none; + visibility: hidden; + } +} + +.disclosure > div { + margin-inline-start: calc( + var(--pf-global--spacer--md) + 12px // half of the icon size; + ); + border-inline-start: 1px solid var(--color-primary-lighter); + padding-block: var(--spacer-small); +} + // raw file content with formatting similar to
 .filecontent {
   font-family: var(--ff-code);
diff --git a/web/src/assets/styles/global.scss b/web/src/assets/styles/global.scss
index 815a6aa5f2..9e2723e621 100644
--- a/web/src/assets/styles/global.scss
+++ b/web/src/assets/styles/global.scss
@@ -21,6 +21,21 @@ a {
   color: currentcolor;
 }
 
+a,
+// TODO: make it better, using PatternFly custom properties for overriding it
+button.pf-m-plain,
+button.pf-m-link {
+  text-decoration: underline;
+  text-decoration-thickness: 0.1em;
+  text-underline-offset: 0.2em;
+  transition: all 0.15s ease-in-out;
+
+  &:hover {
+    color: var(--color-link-hover);
+    text-decoration: underline;
+  }
+}
+
 fieldset {
   padding: var(--fs-base);
   border: 0;
diff --git a/web/src/assets/styles/patternfly-overrides.scss b/web/src/assets/styles/patternfly-overrides.scss
index 9415d5acfc..70647cd675 100644
--- a/web/src/assets/styles/patternfly-overrides.scss
+++ b/web/src/assets/styles/patternfly-overrides.scss
@@ -56,11 +56,9 @@
   --pf-c-button--m-plain--hover--Color: var(--color-button-plain-link-hover);
 }
 
-// Adds a tiny "padding" when focusing a primary or secondary action to avoid
-// https://github.com/yast/d-installer/issues/115#issuecomment-1087375598
-.pf-c-button.pf-m-primary:focus-visible,
-.pf-c-button.pf-m-secondary:focus-visible {
-  box-shadow: 0 0 0 1px white, 0 0 0 2px var(--focus-color);
+.pf-c-button.pf-m-secondary {
+  --pf-c-button--m-secondary--hover--after--BorderColor: var(--color-link-hover);
+  --pf-c-button--m-secondary--hover--Color: var(--color-link-hover);
 }
 
 // SVG icons does not obey font-size
diff --git a/web/src/assets/styles/utilities.scss b/web/src/assets/styles/utilities.scss
index 7ff9ba5be3..d8d48a8b5e 100644
--- a/web/src/assets/styles/utilities.scss
+++ b/web/src/assets/styles/utilities.scss
@@ -108,6 +108,14 @@
   border: none;
 }
 
+.plain-button {
+  border: none;
+  background: none;
+  color: inherit;
+  font: inherit;
+  padding: 0;
+}
+
 .tallest {
   /** block-size fallbacks **/
   height: 95dvh;
diff --git a/web/src/assets/styles/variables.scss b/web/src/assets/styles/variables.scss
index 002fe153fe..637ce4b83c 100644
--- a/web/src/assets/styles/variables.scss
+++ b/web/src/assets/styles/variables.scss
@@ -27,26 +27,26 @@
   --wrapper-padding: var(--spacer-normal);
   --wrapper-background: white;
 
-  --color-primary: #30ba78;
-  --color-primary-darkest: #0c322c;
+  --color-primary: #0c322c;
+  --color-primary-lighter: #30ba78;
   --color-gray-light: #fcfcfc;
   --color-gray: #f2f2f2;
   --color-gray-dark: #efefef; // Fog
   --color-gray-darker: #999;
 
-  --color-link: #30ba78;
-  --color-link-hover: #0c322c;
+  --color-link: #0c322c;
+  --color-link-hover: #30ba78;
 
-  --color-button-primary: #30ba78;
-  --color-button-primary-hover: #0c322c;
+  --color-button-primary: var(--color-link);
+  --color-button-primary-hover: var(--color-link-hover);
 
-  --color-button-plain-link: #30ba78;
-  --color-button-plain-link-hover: #0c322c;
+  --color-button-plain-link: var(--color-link);
+  --color-button-plain-link-hover: var(--color-link-hover);
 
-  --color-background-primary: var(--color-primary-darkest);
+  --color-background-primary: var(--color-primary);
   --color-background-secondary: var(--color-gray-dark);
 
-  --color-text-primary: var(--color-primary-darkest);
+  --color-text-primary: var(--color-primary);
   --color-text-secondary: var(--color-gray-dark);
 
   --color-success: #30ba78;
diff --git a/web/src/components/core/About.jsx b/web/src/components/core/About.jsx
index ce913f233a..ae4054d94e 100644
--- a/web/src/components/core/About.jsx
+++ b/web/src/components/core/About.jsx
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) [2022] SUSE LLC
+ * Copyright (c) [2022-2023] SUSE LLC
  *
  * All Rights Reserved.
  *
@@ -20,19 +20,14 @@
  */
 
 import React, { useState } from "react";
-import { noop } from "~/utils";
 import { Button, Text } from "@patternfly/react-core";
 import { Icon } from "~/components/layout";
 import { Popup } from "~/components/core";
 
-export default function About({ onClickCallback = noop }) {
+export default function About() {
   const [isOpen, setIsOpen] = useState(false);
 
-  const open = () => {
-    setIsOpen(true);
-    onClickCallback();
-  };
-
+  const open = () => setIsOpen(true);
   const close = () => setIsOpen(false);
 
   return (
@@ -42,7 +37,7 @@ export default function About({ onClickCallback = noop }) {
         icon={}
         onClick={open}
       >
-        About
+        About D-Installer
       
 
        {
       expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
     });
   });
-
-  it("triggers given onClickCallback function when opening the dialog", async () => {
-    const onClickCallback = jest.fn();
-
-    const { user } = plainRender();
-    const button = screen.getByRole("button", { name: /About/i });
-
-    await user.click(button);
-    expect(onClickCallback).toHaveBeenCalled();
-  });
 });
diff --git a/web/src/components/core/Disclosure.jsx b/web/src/components/core/Disclosure.jsx
new file mode 100644
index 0000000000..c1ee7542fe
--- /dev/null
+++ b/web/src/components/core/Disclosure.jsx
@@ -0,0 +1,73 @@
+/*
+ * Copyright (c) [2023] SUSE LLC
+ *
+ * All Rights Reserved.
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of version 2 of the GNU General Public License as published
+ * by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, contact SUSE LLC.
+ *
+ * To contact SUSE LLC about this file by physical or electronic mail, you may
+ * find current contact information at www.suse.com.
+ */
+
+// @ts-check
+
+import React, { useState } from "react";
+import { Icon } from '~/components/layout';
+
+/**
+ * Build and render an accessible disclosure
+ * @component
+ *
+ * TODO: use inert and/or hidden/hidden="until-found" attribute for the panel?
+ *  https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/inert
+ *  https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/hidden
+ *
+ * FIXME: do not send all otherProps to the icon but only the
+ *  data-keep-sidebar-open attribute for being able keep the sidebar open
+ *
+ * @example Simple usage
+ *   
+ *     
+ *   
+ *
+ * @example Sending attributes to the button
+ *   
+ *     
+ *   
+ *
+ * @param {object} props
+ * @param {string} props.label - the label to be used as button text
+ * @param {React.ReactElement} props.children - the section content
+ * @param {object} props.otherProps - rest of props, sent to the button element
+ */
+export default function Disclosure({ label, children, ...otherProps }) {
+  const [isExpanded, setIsExpanded] = useState(false);
+  const toggle = () => setIsExpanded(!isExpanded);
+
+  return (
+    
+ +
+ {children} +
+
+ ); +} diff --git a/web/src/components/core/Disclosure.test.jsx b/web/src/components/core/Disclosure.test.jsx new file mode 100644 index 0000000000..594fd1c2e6 --- /dev/null +++ b/web/src/components/core/Disclosure.test.jsx @@ -0,0 +1,59 @@ +/* + * Copyright (c) [2023] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import React from "react"; +import { screen } from "@testing-library/react"; +import { plainRender } from "~/test-utils"; +import { Disclosure } from "~/components/core"; + +describe("Disclosure", () => { + it("renders a button with given label", () => { + plainRender(The disclosed content); + + screen.getByRole("button", { name: "Developer tools" }); + }); + + it("renders a panel with given children", () => { + plainRender( + + A disclosed link +

A disclosed paragraph

+
+ ); + + screen.getByRole("link", { name: "A disclosed link" }); + screen.getByText("A disclosed paragraph"); + }); + + it("renders it initially collapsed", () => { + plainRender(The disclosed content); + const button = screen.getByRole("button", { name: "Developer tools" }); + expect(button).toHaveAttribute("aria-expanded", "false"); + }); + + it("expands it when user clicks on the button ", async () => { + const { user } = plainRender(The disclosed content); + const button = screen.getByRole("button", { name: "Developer tools" }); + + await user.click(button); + expect(button).toHaveAttribute("aria-expanded", "true"); + }); +}); diff --git a/web/src/components/core/LogsButton.jsx b/web/src/components/core/LogsButton.jsx index 4776de5ced..10747e767c 100644 --- a/web/src/components/core/LogsButton.jsx +++ b/web/src/components/core/LogsButton.jsx @@ -1,5 +1,5 @@ /* - * Copyright (c) [2022] SUSE LLC + * Copyright (c) [2022-2023] SUSE LLC * * All Rights Reserved. * @@ -31,7 +31,6 @@ const FILETYPE = "application/x-xz"; /** * Button for collecting and downloading YaST logs - * * @component * * @param {object} props diff --git a/web/src/components/core/Page.test.jsx b/web/src/components/core/Page.test.jsx index 75ef659e76..e9199bbc3b 100644 --- a/web/src/components/core/Page.test.jsx +++ b/web/src/components/core/Page.test.jsx @@ -24,11 +24,6 @@ import { screen } from "@testing-library/react"; import { installerRender, mockLayout } from "~/test-utils"; import { Page } from "~/components/core"; -const mockNavigateFn = jest.fn(); - -jest.mock('react-router-dom', () => ({ - useNavigate: () => mockNavigateFn, -})); jest.mock("~/components/layout/Layout", () => mockLayout()); describe("Page", () => { diff --git a/web/src/components/core/PageOptions.jsx b/web/src/components/core/PageOptions.jsx new file mode 100644 index 0000000000..42b3ab3937 --- /dev/null +++ b/web/src/components/core/PageOptions.jsx @@ -0,0 +1,82 @@ +/* + * Copyright (c) [2023] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +// @ts-check + +import React from "react"; +import { PageOptionsContent } from '~/components/layout'; + +/** + * Wrapper for teleported page options that bubbles onClick + * event to the slot element. + * + * Needed to "dispatch" the onClick events bind to any + * parent on the DOM tree for "teleported nodes", bypassing the + * default React Portal behavior of bubbling events up through the + * React tree only. + * + * @example Simple usage + * + * Configure iSCSI devices + * + * + * + * @param {object} props + * @param {string} [props.title="Page options"] - a title for the group + * @param {string} [props.className="flex-stack"] - CSS class for the wrapper div + * @param {React.ReactElement} props.children - the teleported content + */ +export default function PageOptions({ + title = "Page options", + className = "flex-stack", + children +}) { + const forwardEvents = (target) => { + return ( +
{ + // Using a CustomEvent because the originalTarget is needed to check the dataset. + // See Sidebar.jsx for better understanding + const customEvent = new CustomEvent( + e.type, + { ...e, detail: { originalTarget: e.target } } + ); + target.dispatchEvent(customEvent); + }} + > +

{title}

+ {children} +
+ ); + }; + + return ( + + { (target) => forwardEvents(target) } + + ); +} diff --git a/web/src/components/core/PageOptions.test.jsx b/web/src/components/core/PageOptions.test.jsx new file mode 100644 index 0000000000..543d8a4e4d --- /dev/null +++ b/web/src/components/core/PageOptions.test.jsx @@ -0,0 +1,71 @@ +/* + * Copyright (c) [2023] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import React from "react"; +import { screen } from "@testing-library/react"; +import { plainRender } from "~/test-utils"; +import { PageOptionsSlot } from "~/components/layout"; +import { PageOptions } from "~/components/core"; + +describe("PageOptions", () => { + it("renders given title", () => { + plainRender( + <> + + + The page options content + + + ); + + screen.getByText("Awesome options"); + }); + + it("renders given children", () => { + plainRender( + <> + + + The page options content + + + ); + + screen.getByText("The page options content"); + }); + + it("dispatches onClick events to the target", async () => { + const onClickHandler = jest.fn(); + const { user } = plainRender( + <> + + + + + + ); + + const button = screen.getByRole("button", { name: "Click tester" }); + await user.click(button); + + expect(onClickHandler).toHaveBeenCalled(); + }); +}); diff --git a/web/src/components/core/Section.test.jsx b/web/src/components/core/Section.test.jsx index 56af194095..3f3410316d 100644 --- a/web/src/components/core/Section.test.jsx +++ b/web/src/components/core/Section.test.jsx @@ -21,13 +21,9 @@ import React from "react"; import { screen, within } from "@testing-library/react"; -import { plainRender } from "~/test-utils"; +import { plainRender, installerRender } from "~/test-utils"; import { Section } from "~/components/core"; -jest.mock('react-router-dom', () => ({ - Link: ({ to, children }) => {children} -})); - describe("Section", () => { it("renders given title", () => { plainRender(
); @@ -74,10 +70,9 @@ describe("Section", () => { describe("when path is given", () => { it("renders a link for navigating to it", async () => { - plainRender(
); + installerRender(
); const heading = screen.getByRole("heading", { name: "Settings" }); const link = within(heading).getByRole("link", { name: "Settings" }); - // NOTE: ReactRouter#Link is mocked at the top of file. expect(link).toHaveAttribute("href", "/settings"); }); }); @@ -86,7 +81,7 @@ describe("Section", () => { describe("and path is not present", () => { it("triggers it when the user click on the section title", async () => { const openDialog = jest.fn(); - const { user } = plainRender( + const { user } = installerRender(
); const button = screen.getByRole("button", { name: "Settings" }); @@ -96,15 +91,9 @@ describe("Section", () => { }); describe("but path is present too", () => { - // Silence "Error: Not Implemented: navigation..." from jsdom when clicking a link - // https://github.com/jsdom/jsdom/issues/2112 - const eventListener = (e) => e.preventDefault(); - beforeEach(() => window.addEventListener("click", eventListener)); - afterEach(() => window.removeEventListener("click", eventListener, true)); - it("does not triggers it when the user click on the section title", async () => { const openDialog = jest.fn(); - const { user } = plainRender( + const { user } = installerRender(
); const link = screen.getByRole("link", { name: "Settings" }); diff --git a/web/src/components/core/ShowLogButton.jsx b/web/src/components/core/ShowLogButton.jsx index 043189abd0..979a91b0cd 100644 --- a/web/src/components/core/ShowLogButton.jsx +++ b/web/src/components/core/ShowLogButton.jsx @@ -26,22 +26,13 @@ import { Button } from "@patternfly/react-core"; /** * Button for displaying the YaST logs - * * @component - * - * @param {function} onClickCallback callback triggered after clicking the button */ -const ShowLogButton = ({ onClickCallback }) => { +const ShowLogButton = () => { const [isLogDisplayed, setIsLogDisplayed] = useState(false); - const onClick = () => { - if (onClickCallback) onClickCallback(); - setIsLogDisplayed(true); - }; - - const onClose = () => { - setIsLogDisplayed(false); - }; + const onClick = () => setIsLogDisplayed(true); + const onClose = () => setIsLogDisplayed(false); return ( <> diff --git a/web/src/components/core/ShowTerminalButton.jsx b/web/src/components/core/ShowTerminalButton.jsx index 08758f7e97..59df0a4eca 100644 --- a/web/src/components/core/ShowTerminalButton.jsx +++ b/web/src/components/core/ShowTerminalButton.jsx @@ -27,22 +27,13 @@ import { Icon } from "~/components/layout"; /** * Button for displaying the terminal application - * * @component - * - * @param {function} onClickCallback callback triggered after clicking the button */ -const ShowTerminalButton = ({ onClickCallback }) => { +const ShowTerminalButton = () => { const [isTermDisplayed, setIsTermDisplayed] = useState(false); - const onClick = () => { - if (onClickCallback) onClickCallback(); - setIsTermDisplayed(true); - }; - - const onClose = () => { - setIsTermDisplayed(false); - }; + const onClick = () => setIsTermDisplayed(true); + const onClose = () => setIsTermDisplayed(false); return ( <> @@ -52,7 +43,7 @@ const ShowTerminalButton = ({ onClickCallback }) => { isDisabled={isTermDisplayed} icon={} > - Terminal + Open Terminal { isTermDisplayed && diff --git a/web/src/components/core/Sidebar.jsx b/web/src/components/core/Sidebar.jsx index 418aeaac71..85d9b6884f 100644 --- a/web/src/components/core/Sidebar.jsx +++ b/web/src/components/core/Sidebar.jsx @@ -21,19 +21,33 @@ import React, { useEffect, useRef, useState } from "react"; import { Icon, PageActions } from "~/components/layout"; -import { About, ChangeProductButton, LogsButton, ShowLogButton, ShowTerminalButton } from "~/components/core"; -import { TargetIpsPopup } from "~/components/network"; /** * D-Installer sidebar navigation */ -export default function Sidebar() { +export default function Sidebar({ children }) { const [isOpen, setIsOpen] = useState(false); const closeButtonRef = useRef(null); const open = () => setIsOpen(true); const close = () => setIsOpen(false); + /** + * Handler for automatically closing the sidebar when a click bubbles from a + * children of its content. + * + * @param {MouseEvent} event + */ + const onClick = (event) => { + const target = event.detail?.originalTarget || event.target; + const isLinkOrButton = target instanceof HTMLAnchorElement || target instanceof HTMLButtonElement; + const keepOpen = target.dataset.keepSidebarOpen; + + if (!isLinkOrButton || keepOpen) return; + + close(); + }; + useEffect(() => { if (isOpen) closeButtonRef.current.focus(); }, [isOpen]); @@ -48,7 +62,7 @@ export default function Sidebar() { aria-controls="navigation-and-options" aria-expanded={isOpen} > - + @@ -59,7 +73,7 @@ export default function Sidebar() { data-state={isOpen ? "visible" : "hidden"} >
-

Options

+

Options

-
- - - - - - +
+ { children }