Skip to content

Commit

Permalink
web: Add initial version of core/Field
Browse files Browse the repository at this point in the history
A component to help laying out interactive information in pages.
  • Loading branch information
dgdavid committed Apr 9, 2024
1 parent 86c64f9 commit dba6fc5
Show file tree
Hide file tree
Showing 5 changed files with 274 additions and 0 deletions.
45 changes: 45 additions & 0 deletions web/src/assets/styles/blocks.scss
Original file line number Diff line number Diff line change
Expand Up @@ -743,6 +743,51 @@ section [data-type="agama/reminder"] {
}
}

[data-type="agama/field"] {
> div:first-child {
font-size: var(--fs-large);

button {
padding-inline: 0;
}

button:hover {
color: var(--color-link-hover);
fill: var(--color-link-hover);
}

button b, button:hover b {
text-decoration: underline;
text-underline-offset: var(--spacer-smaller);
}
}

> div:nth-child(n+2) {
margin-inline-start: calc(var(--icon-size-s) + 1ch);
}

> div:nth-child(2) {
color: gray;
font-size: var(--fs-medium);
}

> div:nth-child(n+3) {
margin-block-start: var(--spacer-small);
}

&.on {
button {
fill: var(--color-link-hover);
}
}

hr {
margin-block: var(--spacer-normal);
border: 0;
border-bottom: thin dashed var(--color-gray);
}
}

#boot-form {
legend {
label {
Expand Down
107 changes: 107 additions & 0 deletions web/src/components/core/Field.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
* Copyright (c) [2024] 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 { Icon } from "~/components/layout";
import { If } from "~/components/core";

/**
* @typedef {import("~/components/layout/Icon").IconName} IconName
* @typedef {import("~/components/layout/Icon").IconSize} IconSize
*/

/**
* @typedef {object} FieldProps
* @property {React.ReactNode} label - The field label.
* @property {React.ReactNode} [value] - The field value.
* @property {React.ReactNode} [description] - A field description, useful for providing context to the user.
* @property {IconName} [icon] - The name of the icon for the field.
* @property {IconSize} [iconSize] - The size for the field icon.
* @property {string} [className] - ClassName
* @property {() => {}} [onClick] - Callback
* @property {React.ReactNode} [children] - A content to be rendered as field children
*
* @typedef {Omit<FieldProps, 'icon'>} FieldPropsWithoutIcon
*/

/**
* Component for laying out a page field
*
* @param {FieldProps} props
*/
const Field = ({
label,
value,
description,
icon,
iconSize,
onClick,
children,
...props
}) => {
return (
<div {...props} data-type="agama/field">
<div>
<button className="plain-control" onClick={onClick}>
<If condition={icon?.length > 0} then={<Icon name={icon} size={iconSize || "s"} />} /> <b>{label}</b>
</button> {value}
</div>
<div>
{description}
</div>
<div>
{ children }
</div>
</div>
);
};

/**
* @param {Omit<FieldProps, 'icon'>} props
*/
const SettingsField = ({ ...props }) => {
return <Field {...props} icon="settings" />;
};

/**
* @param {Omit<FieldProps, 'icon'> & {isChecked: true}} props
*/
const SwitchField = ({ isChecked, ...props }) => {
const iconName = isChecked ? "toggle_on" : "toggle_off";
const className = isChecked ? "on" : "off";

return <Field {...props} icon={iconName} className={className} />;
};

/**
* @param {Omit<FieldProps, 'icon'> & {isExpanded: boolean}} props
*/
const ExpandableField = ({ isExpanded, ...props }) => {
const iconName = isExpanded ? "collapse_all" : "expand_all";
const className = isExpanded ? "expanded" : "collapsed";

return <Field {...props} icon={iconName} className={className} />;
};

export default Field;
export { ExpandableField, SettingsField, SwitchField };
112 changes: 112 additions & 0 deletions web/src/components/core/Field.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
* Copyright (c) [2024] 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 { Field, ExpandableField, SettingsField, SwitchField } from "~/components/core";

const onClick = jest.fn();

describe("Field", () => {
it("renders a button with given icon and label", () => {
const { container } = plainRender(
<Field icon="edit" label="Theme" value="dark" onClick={onClick} />
);
screen.getByRole("button", { name: "Theme" });
const icon = container.querySelector("button > svg");
expect(icon).toHaveAttribute("data-icon-name", "edit");
});

it("renders value, description, and given children", () => {
plainRender(
<Field
icon="edit"
label="Theme"
value="dark"
description="Choose your preferred color schema."
onClick={onClick}
>
<p>This is a <b>preview</b></p>;
</Field>
);
screen.getByText("dark");
screen.getByText("Choose your preferred color schema.");
screen.getByText("This is a");
screen.getByText("preview");
});

it("triggers the onClick callback when users clicks the button", async () => {
const { user } = plainRender(
<Field label="Theme" value="dark" onClick={onClick} />
);
const button = screen.getByRole("button");
await user.click(button);
expect(onClick).toHaveBeenCalled();
});
});

describe("SettingsField", () => {
it("uses the 'settings' icon", () => {
const { container } = plainRender(
// Trying to set other icon, although typechecking should catch it.
<SettingsField icon="edit" label="Theme" value="dark" onClick={onClick} />
);
const icon = container.querySelector("button > svg");
expect(icon).toHaveAttribute("data-icon-name", "settings");
});
});

describe("SwitchField", () => {
it("uses the 'toggle_on' icon when isChecked", () => {
const { container } = plainRender(
<SwitchField label="Zoom" value="active" isChecked />
);
const icon = container.querySelector("button > svg");
expect(icon).toHaveAttribute("data-icon-name", "toggle_on");
});

it("uses the 'toggle_off' icon when not isChecked", () => {
const { container } = plainRender(
<SwitchField label="Zoom" value="not active" />
);
const icon = container.querySelector("button > svg");
expect(icon).toHaveAttribute("data-icon-name", "toggle_off");
});
});

describe("ExpandableField", () => {
it("uses the 'collapse_all' icon when isExpanded", () => {
const { container } = plainRender(
<ExpandableField label="More settings" isExpanded />
);
const icon = container.querySelector("button > svg");
expect(icon).toHaveAttribute("data-icon-name", "collapse_all");
});

it("uses the 'expand_all' icon when not isExpanded", () => {
const { container } = plainRender(
<ExpandableField label="More settings" />
);
const icon = container.querySelector("button > svg");
expect(icon).toHaveAttribute("data-icon-name", "expand_all");
});
});
2 changes: 2 additions & 0 deletions web/src/components/core/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,5 @@ export { default as Reminder } from "./Reminder";
export { default as Tag } from "./Tag";
export { default as TreeTable } from "./TreeTable";
export { default as ControlledPanels } from "./ControlledPanels";
export { default as Field } from "./Field";
export { ExpandableField, SettingsField, SwitchField } from "./Field";
8 changes: 8 additions & 0 deletions web/src/components/layout/Icon.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,15 @@ import Apps from "@icons/apps.svg?component";
import Badge from "@icons/badge.svg?component";
import CheckCircle from "@icons/check_circle.svg?component";
import ChevronRight from "@icons/chevron_right.svg?component";
import CollapseAll from "@icons/collapse_all.svg?component";
import Delete from "@icons/delete.svg?component";
import Description from "@icons/description.svg?component";
import Download from "@icons/download.svg?component";
import Downloading from "@icons/downloading.svg?component";
import Edit from "@icons/edit.svg?component";
import EditSquare from "@icons/edit_square.svg?component";
import Error from "@icons/error.svg?component";
import ExpandAll from "@icons/expand_all.svg?component";
import ExpandMore from "@icons/expand_more.svg?component";
import Folder from "@icons/folder.svg?component";
import FolderOff from "@icons/folder_off.svg?component";
Expand Down Expand Up @@ -64,6 +66,8 @@ import Storage from "@icons/storage.svg?component";
import Sync from "@icons/sync.svg?component";
import TaskAlt from "@icons/task_alt.svg?component";
import Terminal from "@icons/terminal.svg?component";
import ToggleOff from "@icons/toggle_off.svg?component";
import ToggleOn from "@icons/toggle_on.svg?component";
import Translate from "@icons/translate.svg?component";
import Tune from "@icons/tune.svg?component";
import Warning from "@icons/warning.svg?component";
Expand All @@ -90,13 +94,15 @@ const icons = {
badge: Badge,
check_circle: CheckCircle,
chevron_right: ChevronRight,
collapse_all: CollapseAll,
delete: Delete,
description: Description,
download: Download,
downloading: Downloading,
edit: Edit,
edit_square: EditSquare,
error: Error,
expand_all: ExpandAll,
expand_more: ExpandMore,
folder: Folder,
folder_off: FolderOff,
Expand Down Expand Up @@ -127,6 +133,8 @@ const icons = {
sync: Sync,
task_alt: TaskAlt,
terminal: Terminal,
toggle_off: ToggleOff,
toggle_on: ToggleOn,
translate: Translate,
tune: Tune,
visibility: Visibility,
Expand Down

0 comments on commit dba6fc5

Please sign in to comment.