Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[web] Improve Section component (former Category) #332

Merged
merged 8 commits into from
Nov 25, 2022
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
74 changes: 0 additions & 74 deletions web/src/components/core/Category.jsx

This file was deleted.

10 changes: 5 additions & 5 deletions web/src/components/core/Overview.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { useSoftware } from "@context/software";
import { Button, Flex, FlexItem } from "@patternfly/react-core";

import { Title, PageIcon, PageActions, MainActions } from "@components/layout";
import { Category, InstallButton } from "@components/core";
import { Section, InstallButton } from "@components/core";
import { LanguageSelector } from "@components/language";
import { Storage } from "@components/storage";
import { Users } from "@components/users";
Expand Down Expand Up @@ -67,12 +67,12 @@ function Overview() {
}

const categories = [
<Category key="language" title="Language" icon={LanguagesSelectionIcon}>
<Section key="language" title="Language" icon={LanguagesSelectionIcon}>
<LanguageSelector />
</Category>,
<Category key="network" title="Network" icon={NetworkIcon}>
</Section>,
<Section key="network" title="Network" icon={NetworkIcon}>
<Network />
</Category>,
</Section>,
<Storage key="storage" showErrors />,
<Users key="users" showErrors={showErrors} />
];
Expand Down
179 changes: 179 additions & 0 deletions web/src/components/core/Section.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
/*
* Copyright (c) [2022] 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 {
Button,
Split,
SplitItem,
Stack,
StackItem,
Text,
TextContent,
TextVariants,
Tooltip
} from "@patternfly/react-core";

import { classNames } from "@/utils";
import { ValidationErrors } from "@components/core";

import { CogIcon } from '@patternfly/react-icons';

import "./section.scss";

/**
* Helper method for rendering section react-icons
*
* @param {React.FunctionComponent|React.ComponentClass} icon
* @param {string} ariaLabel
* @param {number} [size=32]
*
* @return {React.ReactNode}
*/
const renderIcon = (icon, ariaLabel, size = 32) => {
if (!icon) return null;

const Icon = icon;

return (
<figure aria-label={ariaLabel}>
<Icon size={size} />
</figure>
);
};

/**
*
* Displays an installation section
* @component
*
* @example <caption>Simple usage</caption>
* <Section title="Users" icon={UsersIcon}>
* <UserSectionContent />
* </Section>
*
* @example <caption>A section with a description</caption>
* <Section title="Users" icon={UsersIcon} description="Use this section for setting the user data">
* <UserSectionContent />
* </Section>
*
* @example <caption>A section without icon but settings action with tooltip</caption>
* <Section
* key="language"
* title="Language"
* actionTooltip="Click here for tweaking language settings"
* onActionClick={() => setLanguageSettingsVisible(true)}
* >
* <LanguageSelector />
* </Section>
*
* @example <caption>A section with title separator and custom action icon</caption>
* <Section
* title="Target"
* icon={TargetIcon}
* actionIcon={TargetSettingIcon}
* onActionClick={() => setDisplayTargetSettings(true)}
* usingSeparator
* >
* <StorageTargetSelector />
* </Section>
*
* @param {object} props
* @param {string} props.title - The title for the section
* @param {string} [props.description] - A tiny description for the section
* @param {boolean} [props.usingSeparator] - whether or not a thin border should be shown between title and content
* @param {React.FunctionComponent} [props.icon] - An icon for the section
* @param {import("@client/mixins").ValidationError[]} [props.errors] - Validation errors to be shown before the title
* @param {React.FunctionComponent|React.ComponentClass} [props.actionIcon=CogIcon] - An icon to be used for section actions
* @param {React.ReactNode} [props.actionTooltip] - text to be shown as a tooltip when user hovers action icon, if present
* @param {React.MouseEventHandler} [props.onActionClick] - callback to be triggered when user clicks on action icon, if present
* @param {JSX.Element} [props.children] - the section content
* @param {object} [props.otherProps] PF4/Split props, see {@link https://www.patternfly.org/v4/layouts/split#props}
*/
export default function Section({
title,
description,
usingSeparator,
icon,
errors,
actionIcon = CogIcon,
actionTooltip,
onActionClick,
children,
...otherProps
}) {
const renderAction = () => {
if (typeof onActionClick !== 'function') return null;

const Action = () => (
<Button variant="plain" className="d-installer-section-action" isInline onClick={onActionClick}>
{renderIcon(actionIcon, `${title} section action icon`)}
</Button>
);

if (!actionTooltip) return <Action />;

return (
<Tooltip content={actionTooltip} position="right" distance={10} entryDelay={200} exitDelay={200}>
<Action />
</Tooltip>
);
};

const titleClassNames = classNames(
"d-installer-section-title",
usingSeparator && "using-separator"
);

return (
<Split className="d-installer-section" hasGutter {...otherProps}>
<SplitItem className="d-installer-section-icon">
{renderIcon(icon, `${title} section icon`, 32)}
</SplitItem>
<SplitItem isFilled>
<Stack hasGutter>
<StackItem>
<TextContent>
<Text component={TextVariants.h2} className={titleClassNames}>
{title} {renderAction()}
</Text>
</TextContent>
</StackItem>
{ description && description !== "" &&
<StackItem className="d-installer-section-description">
<TextContent>
<Text component={TextVariants.small}>
{description}
</Text>
</TextContent>
</StackItem> }
{ errors &&
<StackItem>
<ValidationErrors errors={errors} title={`${title} errors`} />
</StackItem> }
<StackItem>{children}</StackItem>
</Stack>
</SplitItem>
</Split>
);
}
93 changes: 93 additions & 0 deletions web/src/components/core/Section.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* Copyright (c) [2022] 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, { useState } from "react";
import { act, screen } from "@testing-library/react";
import { installerRender } from "@/test-utils";
import { Section } from "@components/core";

const FakeIcon = () => "FI";

describe("Section", () => {
it("renders given title", () => {
installerRender(<Section title="Awesome settings" />);

screen.getByRole("heading", { name: "Awesome settings" });
});

it("renders given description", () => {
installerRender(
<Section title="Awesome settings" description="Intended to perform awesome tweaks" />
);

screen.getByText("Intended to perform awesome tweaks");
});

it("renders given icon", () => {
installerRender(<Section title="Awesome settings" icon={FakeIcon} />);

screen.getByRole("figure", { name: "Awesome settings section icon" });
});

it("renders given errors", () => {
installerRender(
<Section title="Awesome settings" errors={[{ message: "Something went wrong" }]} />
);

screen.getByText("Something went wrong");
});

describe("when onActionClick callback is given", () => {
it("renders an action icon", () => {
installerRender(
<Section title="Awesome settings" onActionClick={() => null} />
);

screen.getByRole("figure", { name: "Awesome settings section action icon" });
});

it("triggers the action when user clicks on it", async () => {
const AwesomeSection = () => {
const [showInput, setShowInput] = useState(false);
return (
<Section title="Awesome settings" onActionClick={() => setShowInput(true)}>
{ showInput &&
<>
<label htmlFor="awesome-input">Awesome input</label>
<input id="awesome-input" type="text" />
</> }
</Section>
);
};

const { user } = installerRender(<AwesomeSection />);

let inputText = screen.queryByRole("textbox", { name: "Awesome input" });
expect(inputText).not.toBeInTheDocument();

const actionIcon = screen.getByRole("figure", { name: "Awesome settings section action icon" });
await act(async () => user.click(actionIcon));

inputText = screen.queryByRole("textbox", { name: "Awesome input" });
expect(inputText).toBeInTheDocument();
});
});
});
2 changes: 1 addition & 1 deletion web/src/components/core/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
*/

export { default as About } from "./About";
export { default as Category } from "./Category";
export { default as Section } from "./Section";
export { default as FormLabel } from "./FormLabel";
export { default as InstallationFinished } from "./InstallationFinished";
export { default as InstallationProgress } from "./InstallationProgress";
Expand Down
Loading