Skip to content

Commit

Permalink
Merge pull request #332 from yast/improve-section-component
Browse files Browse the repository at this point in the history
[web] Improve Section component (former Category)
  • Loading branch information
dgdavid authored Nov 25, 2022
2 parents 5a25202 + 6012248 commit 549f914
Show file tree
Hide file tree
Showing 8 changed files with 321 additions and 86 deletions.
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 Fieldset } from "./Fieldset";
export { default as InstallationFinished } from "./InstallationFinished";
Expand Down
Loading

0 comments on commit 549f914

Please sign in to comment.