* Copyright (C) 2025 Red Hat, Inc.
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
import cockpit from "cockpit";
import React, { useLayoutEffect, useRef, useState } from "react";
import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
import { Menu, MenuContent, MenuGroup, MenuItem, MenuList } from "@patternfly/react-core/dist/esm/components/Menu/index.js";
import { TextInputGroup, TextInputGroupMain, TextInputGroupUtilities } from "@patternfly/react-core/dist/esm/components/TextInputGroup/index.js";
import { SearchIcon } from "@patternfly/react-icons/dist/esm/icons/search-icon";
import { TimesIcon } from "@patternfly/react-icons/dist/esm/icons/times-icon";
import { warn as loggerWarn } from "../../helpers/log.js";
import "./MenuSearch.scss";
const _ = cockpit.gettext;
const renderOptions = (options, scrollRef, selection, search) => {
return options.map(option => {
let isSelected = false;
if (selection) {
if (Array.isArray(selection)) {
isSelected = selection.includes(option.itemId);
} else {
isSelected = option.itemId === selection;
}
}
switch (option.itemType) {
case "menu-item":
if (search && option.onSearch(search) === false) {
return null;
}
return (
<MenuItem
id={option.id}
description={option.itemDescription}
isAriaDisabled={option.isAriaDisabled}
isSelected={isSelected}
itemId={option.itemId}
key={option.key || option.id}
{...(isSelected ? { ref: scrollRef } : {})}
style={isSelected ? { backgroundColor: "var(--pf-v6-c-menu__list-item--hover--BackgroundColor)" } : undefined}
>
{option.itemLang
? (
<div lang={option.itemLang}>
{option.itemText}
</div>
)
: option.itemText}
</MenuItem>
);
case "menu-group": {
const itemChildren = renderOptions(option.itemChildren, scrollRef, selection, search);
if (itemChildren.filter((item) => item !== null).length === 0) {
return null;
}
return (
<MenuGroup
key={option.key || option.id}
id={option.id}
label={option.itemLabel}
labelHeadingLevel={option.itemLabelHeadingLevel}
>
{itemChildren}
</MenuGroup>
);
}
default:
loggerWarn(`Unknown item type: ${option.itemType}`);
return null;
}
});
};
export const MenuSearch = ({ ariaLabelSearch, handleOnSelect, menuType, options, screenId, selection }) => {
const [search, setSearch] = useState("");
const prefix = screenId + "-" + menuType;
const scrollRef = useRef(null);
const didScroll = useRef(false);
useLayoutEffect(() => {
if (scrollRef.current && !didScroll.current) {
scrollRef.current.scrollIntoView({ block: "center" });
didScroll.current = true;
}
});
const menuListContent = renderOptions(options, scrollRef, selection, search);
return (
<>
<TextInputGroup className={prefix + "-search"}>
<TextInputGroupMain
icon={<SearchIcon />}
value={search}
onChange={(event) => setSearch(event.target.value)}
aria-label={ariaLabelSearch}
/>
{search && (
<TextInputGroupUtilities>
<Button
variant="plain"
onClick={() => setSearch("")}
aria-label={_("Clear search")}
>
<TimesIcon />
</Button>
</TextInputGroupUtilities>
)}
</TextInputGroup>
<Menu
className={prefix + "-menu"}
id={prefix + "-menu"}
isScrollable
isPlain
onSelect={handleOnSelect}
aria-invalid={!selection}
>
<MenuContent>
<MenuList>
{menuListContent.filter((item) => item !== null).length > 0
? (
menuListContent
)
: (
<MenuItem isAriaDisabled>
{_("No results found")}
</MenuItem>
)}
</MenuList>
</MenuContent>
</Menu>
</>
);
};