🏡 index : quine-global/quine-anaconda-webui.git

/*
 * 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 => {
        // Support both single selection (string) and multiple selection (array)
        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; // Skip items that do not match the search criteria
            }

            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; // Skip empty groups
            }
            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);

    // Scroll only once when the menu is opened and the selected item is available.
    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>
        </>
    );
};