diff --git a/web/src/components/core/Section.jsx b/web/src/components/core/Section.jsx index 1e16d6b486..99a219eb6c 100644 --- a/web/src/components/core/Section.jsx +++ b/web/src/components/core/Section.jsx @@ -26,54 +26,6 @@ import { Link } from "react-router-dom"; import { Icon } from '~/components/layout'; import { ValidationErrors } from "~/components/core"; -/** - * Internal component for rendering the section icon - * - * @param {object} props - * @param {string} [props.name] - the name of the icon - * @param {number} [props.size=32] - the icon size - * - * @return {React.ReactElement} - */ -const SectionIcon = ({ name, size = 32 }) => { - if (!name) return null; - - return ; -}; - -/** - * Internal component for rendering the section title - * - * @param {object} props - * @param {string} props.id - the id for the header. - * @param {string} props.text - the title for the section. - * @param {string} props.path - the path where the section links to. - * - * @return {JSX.Element} - */ -const SectionTitle = ({ id, text, path }) => { - if (!text?.trim()) return null; - - const title = !path?.trim() ? <>{text} : {text}; - - return

{title}

; -}; - -/** - * Internal component for wrapping and rendering the section content - * - * @param {object} props - * @param {React.ReactElement|React.ReactElement[]} props.children - the content to be wrapped - * @return {JSX.Element} - */ -const SectionContent = ({ children }) => { - return ( -
- {children} -
- ); -}; - /** * Renders children into an HTML section * @component @@ -122,13 +74,15 @@ export default function Section({ console.error("The Section component must have either, a 'title' or an 'aria-label'"); } - const SectionHeader = () => { - if (!title) return; + const Header = () => { + if (!title?.trim()) return; + + const header = !path?.trim() ? <>{title} : {title}; return ( <> - - + +

{header}

); }; @@ -140,12 +94,12 @@ export default function Section({ aria-label={ariaLabel || undefined} aria-labelledby={ title && !ariaLabel ? headerId : undefined} > - - +
+
{errors?.length > 0 && } {children} - +
); } diff --git a/web/src/components/layout/Icon.jsx b/web/src/components/layout/Icon.jsx index a516f65361..f3866cccf3 100644 --- a/web/src/components/layout/Icon.jsx +++ b/web/src/components/layout/Icon.jsx @@ -20,9 +20,6 @@ */ import React from 'react'; -import { sprintf } from "sprintf-js"; - -import { _ } from "~/i18n"; // NOTE: "@icons" is an alias to use a shorter path to real @material-symbols // icons location. Check the tsconfig.json file to see its value. @@ -132,6 +129,9 @@ const icons = { * * If exists, it renders requested icon with given size. * + * @note: if either, name prop has a falsy value or requested icon is not found, + * it will outputs a message to the console.error and renders nothing. + * * @todo: import icons dynamically if the list grows too much. See * - https://stackoverflow.com/a/61472427 * - https://ryanhutzley.medium.com/dynamic-svg-imports-in-create-react-app-d6d411f6d6c6 @@ -139,16 +139,21 @@ const icons = { * @example * * - * @param {object} props - component props - * @param {string} props.name - desired icon - * @param {string} [props.className=""] - CSS classes - * @param {string|number} [props.size=32] - the icon width and height - * @param {object} [props.otherProps] other props sent to SVG icon + * @param {object} props - Component props + * @param {string} props.name - Name of the desired icon. + * @param {string} [props.className=""] - CSS classes. + * @param {string|number} [props.size=32] - Size used for both, width and height. + * @param {object} [props.otherProps] Other props sent to SVG icon. * */ export default function Icon({ name, className = "", size = 32, ...otherProps }) { + if (!name) { + console.error(`Icon called without name. '${name}' given instead. Rendering nothing.`); + return null; + } + if (!icons[name]) { - console.error(sprintf(_("Icon %s not found!"), name)); + console.error(`Icon '${name}' not found!`); return null; } diff --git a/web/src/components/layout/Icon.test.jsx b/web/src/components/layout/Icon.test.jsx index a2a154d22f..490598a13d 100644 --- a/web/src/components/layout/Icon.test.jsx +++ b/web/src/components/layout/Icon.test.jsx @@ -23,21 +23,7 @@ import React from "react"; import { plainRender } from "~/test-utils"; import { Icon } from "~/components/layout"; -describe("when given a known name", () => { - it("renders an aria-hidden SVG element", async () => { - const { container } = plainRender(); - const svgElement = container.querySelector('svg'); - expect(svgElement).toHaveAttribute("aria-hidden", "true"); - }); - - it("includes the icon name as a data attribute of the SVG", async () => { - const { container } = plainRender(); - const svgElement = container.querySelector('svg'); - expect(svgElement).toHaveAttribute("data-icon-name", "wifi"); - }); -}); - -describe("when given an unknown name", () => { +describe("Icon", () => { beforeAll(() => { jest.spyOn(console, "error").mockImplementation(); }); @@ -46,15 +32,54 @@ describe("when given an unknown name", () => { console.error.mockRestore(); }); - it("outputs to console.error", () => { - plainRender(); - expect(console.error).toHaveBeenCalledWith( - expect.stringContaining("apsens not found") - ); + describe("mounted with a falsy value as name", () => { + it("outputs to console.error", () => { + plainRender(); + expect(console.error).toHaveBeenCalledWith( + expect.stringContaining("Rendering nothing") + ); + }); + + it("renders nothing", () => { + const { container: contentWhenNotDefined } = plainRender(); + expect(contentWhenNotDefined).toBeEmptyDOMElement(); + + const { container: contentWhenEmpty } = plainRender(); + expect(contentWhenEmpty).toBeEmptyDOMElement(); + + const { container: contentWhenFalse } = plainRender(); + expect(contentWhenFalse).toBeEmptyDOMElement(); + + const { container: contentWhenNull } = plainRender(); + expect(contentWhenNull).toBeEmptyDOMElement(); + }); + }); + + describe("mounted with a known name", () => { + it("renders an aria-hidden SVG element", async () => { + const { container } = plainRender(); + const svgElement = container.querySelector('svg'); + expect(svgElement).toHaveAttribute("aria-hidden", "true"); + }); + + it("includes the icon name as a data attribute of the SVG", async () => { + const { container } = plainRender(); + const svgElement = container.querySelector('svg'); + expect(svgElement).toHaveAttribute("data-icon-name", "wifi"); + }); }); - it("renders nothing", async () => { - const { container } = plainRender(); - expect(container).toBeEmptyDOMElement(); + describe("mounted with unknown name", () => { + it("outputs to console.error", () => { + plainRender(); + expect(console.error).toHaveBeenCalledWith( + expect.stringContaining("'apsens' not found") + ); + }); + + it("renders nothing", async () => { + const { container } = plainRender(); + expect(container).toBeEmptyDOMElement(); + }); }); });