diff --git a/web/src/components/core/SelectWrapper.test.tsx b/web/src/components/core/SelectWrapper.test.tsx new file mode 100644 index 0000000000..e74a89cf3a --- /dev/null +++ b/web/src/components/core/SelectWrapper.test.tsx @@ -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) => ( + + + First + Second + + +); + +describe("SelectWrapper", () => { + it("renders a toggle button using label or value", () => { + const { rerender } = plainRender(); + const button = screen.getByRole("button"); + expect(button.classList.contains("pf-v6-c-menu-toggle")).toBe(true); + within(button).getByText("The label"); + rerender(); + within(button).getByText("The value"); + }); + + it("toggles select options when the toggle button is clicked", async () => { + const { user } = plainRender(); + 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(); + 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(); + 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(); + 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(); + }); +}); diff --git a/web/src/components/core/SelectToggle.tsx b/web/src/components/core/SelectWrapper.tsx similarity index 72% rename from web/src/components/core/SelectToggle.tsx rename to web/src/components/core/SelectWrapper.tsx index 991c74993b..589f90aa51 100644 --- a/web/src/components/core/SelectToggle.tsx +++ b/web/src/components/core/SelectWrapper.tsx @@ -21,23 +21,31 @@ */ 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; -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 = () => { @@ -45,11 +53,11 @@ export default function SelectToggle({ }; const onSelect = ( - _event: React.MouseEvent | undefined, - value: string | number | undefined, + _: React.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) => { @@ -67,7 +75,7 @@ export default function SelectToggle({ return ( {option === "range" && ( @@ -744,13 +743,13 @@ export default function PartitionPage() { /> - } onChange={changeTarget} > - + @@ -763,19 +762,19 @@ export default function PartitionPage() { - } onChange={changeFilesystem} isDisabled={mountPointError !== undefined} > - + - @@ -784,7 +783,7 @@ export default function PartitionPage() { isDisabled={mountPointError !== undefined} > - + {target === NEW_PARTITION && sizeOption === "auto" && ( )}