Skip to content

Commit

Permalink
feat(web): rename SelectToggle and add unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
dgdavid committed Feb 3, 2025
1 parent c2f8a81 commit 5a21f50
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 21 deletions.
93 changes: 93 additions & 0 deletions web/src/components/core/SelectWrapper.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* Copyright (c) [2025] SUSE LLC
*
* All Rights Reserved.
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation; either version 2 of the License, or (at your option)
* any later version.
*
* 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, waitForElementToBeRemoved, within } from "@testing-library/react";
import { plainRender } from "~/test-utils";
import SelectWrapper, { SelectWrapperProps } from "~/components/core/SelectWrapper";
import { SelectList, SelectOption } from "@patternfly/react-core";

const TestingSelector = (props: Partial<SelectWrapperProps>) => (
<SelectWrapper label="Selector" value="2" {...props}>
<SelectList>
<SelectOption value="1">First</SelectOption>
<SelectOption value="2">Second</SelectOption>
</SelectList>
</SelectWrapper>
);

describe("SelectWrapper", () => {
it("renders a toggle button using label or value", () => {
const { rerender } = plainRender(<TestingSelector label="The label" value="The value" />);
const button = screen.getByRole("button");
expect(button.classList.contains("pf-v6-c-menu-toggle")).toBe(true);
within(button).getByText("The label");
rerender(<TestingSelector label={undefined} value="The value" />);
within(button).getByText("The value");
});

it("toggles select options when the toggle button is clicked", async () => {
const { user } = plainRender(<TestingSelector />);
const button = screen.getByRole("button", { name: "Selector" });
expect(button).toHaveAttribute("aria-expanded", "false");
expect(screen.queryAllByRole("option")).toEqual([]);
await user.click(button);
expect(button).toHaveAttribute("aria-expanded", "true");
expect(screen.queryAllByRole("option").length).toEqual(2);
await user.click(button);
expect(button).toHaveAttribute("aria-expanded", "false");
await waitForElementToBeRemoved(() => screen.getAllByRole("option"));
});

it("toggles select options when an option is clicked", async () => {
const { user } = plainRender(<TestingSelector />);
const button = screen.getByRole("button", { name: "Selector" });
await user.click(button);
const firstOption = screen.getByRole("option", { name: "First" });
await user.click(firstOption);
await waitForElementToBeRemoved(() => screen.getByRole("listbox"));
expect(button).toHaveAttribute("aria-expanded", "false");
});

it("triggers onChange callback when not selected option is clicked", async () => {
const onChangeFn = jest.fn();
const { user } = plainRender(<TestingSelector onChange={onChangeFn} />);
const button = screen.getByRole("button", { name: "Selector" });
await user.click(button);
const secondOption = screen.getByRole("option", { name: "Second" });
await user.click(secondOption);
expect(onChangeFn).not.toHaveBeenCalled();
await user.click(button);
const firstOption = screen.getByRole("option", { name: "First" });
await user.click(firstOption);
expect(onChangeFn).toHaveBeenCalledWith("1");
});

it("focuses the button toggle after selection", async () => {
const { user } = plainRender(<TestingSelector />);
const button = screen.getByRole("button", { name: "Selector" });
await user.click(button);
const secondOption = screen.getByRole("option", { name: "Second" });
await user.click(secondOption);
expect(button).toHaveFocus();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -21,35 +21,43 @@
*/

import React from "react";
import { Select, MenuToggle, MenuToggleElement } from "@patternfly/react-core";
import { Select, MenuToggle, MenuToggleElement, SelectProps } from "@patternfly/react-core";

export type SelectToggleProps = {
export type SelectWrapperProps = {
id?: string;
value: number | string;
label?: React.ReactNode;
onChange?: (value: number | string) => void;
isDisabled?: boolean;
children?: React.ReactNode;
};
} & Omit<SelectProps, "toggle">;

export default function SelectToggle({
/**
* Wrapper to simplify the usage of PF/Menu/Select
*
* Abstracts the toggle setup by building it internally based on the received props.
*
* @see https://www.patternfly.org/components/menus/select/
*/
export default function SelectWrapper({
id,
value,
label,
onChange,
isDisabled = false,
children,
}: SelectToggleProps): React.ReactElement {
}: SelectWrapperProps): React.ReactElement {
const [isOpen, setIsOpen] = React.useState(false);

const onToggleClick = () => {
setIsOpen(!isOpen);
};

const onSelect = (
_event: React.MouseEvent<Element, MouseEvent> | undefined,
value: string | number | undefined,
_: React.MouseEvent<Element, MouseEvent> | undefined,
nextValue: string | number | undefined,
) => {
setIsOpen(false);
onChange && onChange(value as string);
onChange && nextValue !== value && onChange(nextValue as string);
};

const toggle = (toggleRef: React.Ref<MenuToggleElement>) => {
Expand All @@ -67,7 +75,7 @@ export default function SelectToggle({

return (
<Select
id="option-variations-select"
id={id}
isOpen={isOpen}
selected={value}
onSelect={onSelect}
Expand Down
3 changes: 2 additions & 1 deletion web/src/components/core/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) [2022-2024] SUSE LLC
* Copyright (c) [2022-2025] SUSE LLC
*
* All Rights Reserved.
*
Expand Down Expand Up @@ -46,3 +46,4 @@ export { default as EmptyState } from "./EmptyState";
export { default as InstallerOptions } from "./InstallerOptions";
export { default as IssuesDrawer } from "./IssuesDrawer";
export { default as Drawer } from "./Drawer";
export { default as SelectWrapper } from "./SelectWrapper";
19 changes: 9 additions & 10 deletions web/src/components/storage/PartitionPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,8 @@ import {
HelperText,
HelperTextItem,
} from "@patternfly/react-core";
import { Page } from "~/components/core/";
import { Page, SelectWrapper as Select } from "~/components/core/";
import SelectTypeaheadCreatable from "~/components/core/SelectTypeaheadCreatable";
import SelectToggle from "~/components/core/SelectToggle";
import { useDevices, useVolumeTemplates } from "~/queries/storage";
import {
useDrive,
Expand Down Expand Up @@ -578,13 +577,13 @@ function CustomSize({ value, mountPoint, onChange }: CustomSizeProps) {
</FlexItem>
<FlexItem>
<FormGroup fieldId="maxSize" label={_("Maximum")}>
<SelectToggle
<Select
value={option}
label={<CustomSizeOptionLabel value={option} />}
onChange={changeOption}
>
<CustomSizeOptions />
</SelectToggle>
</Select>
</FormGroup>
</FlexItem>
{option === "range" && (
Expand Down Expand Up @@ -744,13 +743,13 @@ export default function PartitionPage() {
/>
</FlexItem>
<FlexItem>
<SelectToggle
<Select
value={target}
label={<TargetOptionLabel value={target} />}
onChange={changeTarget}
>
<TargetOptions />
</SelectToggle>
</Select>
</FlexItem>
</Flex>
<FormHelperText>
Expand All @@ -763,19 +762,19 @@ export default function PartitionPage() {
</FormHelperText>
</FormGroup>
<FormGroup fieldId="fileSystem" label={_("File system")}>
<SelectToggle
<Select
value={filesystem}
label={<FilesystemOptionLabel value={filesystem} target={target} />}
onChange={changeFilesystem}
isDisabled={mountPointError !== undefined}
>
<FilesystemOptions mountPoint={mountPoint} target={target} />
</SelectToggle>
</Select>
</FormGroup>
<Flex>
<FlexItem>
<FormGroup fieldId="size" label={_("Size")}>
<SelectToggle
<Select
value={sizeOption}
label={
<SizeOptionLabel value={sizeOption} mountPoint={mountPoint} target={target} />
Expand All @@ -784,7 +783,7 @@ export default function PartitionPage() {
isDisabled={mountPointError !== undefined}
>
<SizeOptions mountPoint={mountPoint} target={target} />
</SelectToggle>
</Select>
{target === NEW_PARTITION && sizeOption === "auto" && (
<AutoSize mountPoint={mountPoint} partition={solvedPartition} />
)}
Expand Down

0 comments on commit 5a21f50

Please sign in to comment.