import { jsx as _jsx } from "react/jsx-runtime";
// (C) 2025-2026 GoodData Corporation
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useAutoupdateRef } from "@gooddata/sdk-ui";
import { makeMenuKeyboardNavigation } from "../@utils/keyboardNavigation.js";
import { DefaultUiListboxInteractiveItemComponent } from "./defaults/DefaultUiListboxInteractiveItemComponent.js";
import { DefaultUiListboxStaticItemComponent } from "./defaults/DefaultUiListboxStaticItemComponent.js";
import { firstCharacterSearch } from "./defaults/firstCharacterSearch.js";
import { b, e } from "./listboxBem.js";
/**
 * An accessible listbox component that can be navigated by keyboard.
 * Usable in a <Dropdown /> component.
 * Should implement https://www.w3.org/WAI/ARIA/apg/patterns/listbox/
 *
 * @internal
 */
export function UiListbox({ items, dataTestId, itemDataTestId, width, maxWidth, maxHeight, onSelect, onClose, onUnhandledKeyDown = firstCharacterSearch, selectedItemId, InteractiveItemComponent = DefaultUiListboxInteractiveItemComponent, StaticItemComponent = DefaultUiListboxStaticItemComponent, shouldKeyboardActionPreventDefault, shouldKeyboardActionStopPropagation, shouldCloseOnSelect = true, isDisabledFocusable = false, isCompact = false, reference, ariaAttributes, }) {
    const isItemFocusable = useCallback((item) => {
        if (item?.type !== "interactive") {
            return false;
        }
        return isDisabledFocusable || !item.isDisabled;
    }, [isDisabledFocusable]);
    const [focusedIndex, setFocusedIndex] = useState(() => {
        // First try to find the selected item if it's focusable
        const selectedIndex = items.findIndex((item) => item.id === selectedItemId && isItemFocusable(item));
        if (selectedIndex >= 0) {
            return selectedIndex;
        }
        // Otherwise find the first focusable item
        const firstFocusableIndex = items.findIndex(isItemFocusable);
        return firstFocusableIndex >= 0 ? firstFocusableIndex : undefined;
    });
    const itemRefs = useRef([]);
    // Update refs array size when items change. The actual refs are updated during render.
    useEffect(() => {
        itemRefs.current = itemRefs.current.slice(0, items.length);
    }, [items]);
    const focusedItem = focusedIndex == null ? undefined : items[focusedIndex];
    const focusedItemNode = focusedIndex == null ? undefined : itemRefs.current[focusedIndex];
    // Scroll focused item into view
    useEffect(() => {
        if (!focusedItemNode) {
            return;
        }
        focusedItemNode.scrollIntoView({ block: "nearest" });
    }, [focusedItemNode]);
    const handleSelectItem = useCallback((item, mods) => {
        if (!item || item.isDisabled) {
            return;
        }
        onSelect?.(item, mods ?? {});
        if (shouldCloseOnSelect) {
            onClose?.();
        }
    }, [onClose, onSelect, shouldCloseOnSelect]);
    const contextRef = useAutoupdateRef({
        itemRefs,
        focusedIndex,
        items,
        onClose,
        onSelect: handleSelectItem,
        setFocusedIndex,
        selectedItemId,
        isItemFocusable,
    });
    const handleKeyDown = useMemo(() => makeMenuKeyboardNavigation({
        onFocusPrevious: () => {
            setFocusedIndex((prevIndex) => {
                let newIndex = (prevIndex ?? 0) - 1;
                // Skip non-focusable items
                while (newIndex >= 0 && !isItemFocusable(items[newIndex])) {
                    newIndex--;
                }
                return newIndex >= 0 ? newIndex : prevIndex;
            });
        },
        onFocusNext: () => {
            setFocusedIndex((prevIndex) => {
                let newIndex = (prevIndex ?? 0) + 1;
                // Skip non-focusable items
                while (newIndex < items.length && !isItemFocusable(items[newIndex])) {
                    newIndex++;
                }
                return newIndex < items.length ? newIndex : prevIndex;
            });
        },
        onFocusFirst: () => {
            // Find the first focusable item
            const firstFocusableIndex = items.findIndex(isItemFocusable);
            setFocusedIndex(firstFocusableIndex >= 0 ? firstFocusableIndex : undefined);
        },
        onFocusLast: () => {
            // Find the last focusable item
            for (let i = items.length - 1; i >= 0; i--) {
                if (isItemFocusable(items[i])) {
                    setFocusedIndex(i);
                    return;
                }
            }
            setFocusedIndex(undefined);
        },
        onSelect: (e) => {
            if (focusedItem?.type === "interactive") {
                handleSelectItem(focusedItem, {
                    type: "keyboard",
                    newTab: e.ctrlKey || e.metaKey,
                });
            }
        },
        onClose,
        onUnhandledKeyDown: (event) => {
            onUnhandledKeyDown(event, contextRef.current);
        },
    }, {
        shouldPreventDefault: shouldKeyboardActionPreventDefault,
        shouldStopPropagation: shouldKeyboardActionStopPropagation,
    }), [
        contextRef,
        focusedItem,
        handleSelectItem,
        isItemFocusable,
        items,
        onClose,
        onUnhandledKeyDown,
        shouldKeyboardActionPreventDefault,
        shouldKeyboardActionStopPropagation,
    ]);
    return (_jsx("div", { className: b(), style: { width, maxWidth, maxHeight }, "data-testid": dataTestId, children: _jsx("ul", { className: e("items"), ref: reference, tabIndex: 0, onKeyDown: handleKeyDown, "aria-activedescendant": makeItemId(ariaAttributes.id, focusedItem), ...ariaAttributes, role: "listbox", children: items.map((item, index) => {
                const testId = typeof itemDataTestId === "function" ? itemDataTestId(item) : itemDataTestId;
                return item.type === "interactive" ? (_jsx("li", { ref: (el) => {
                        itemRefs.current[index] = el;
                    }, id: makeItemId(ariaAttributes.id, item), role: "option", "aria-selected": item.id === selectedItemId, "aria-disabled": item.isDisabled, "data-testid": testId, "aria-controls": ariaAttributes["aria-controls"], children: _jsx(InteractiveItemComponent, { onSelect: (e) => {
                            handleSelectItem(item, {
                                type: "mouse",
                                newTab: e.ctrlKey || e.metaKey || e.button === 1,
                            });
                        }, item: item, isFocused: index === focusedIndex, isSelected: item.id === selectedItemId, isCompact: isCompact }) }, item.id)) : (
                // Static items (separators, headers) are presentational; `role="presentation"`
                // keeps the <li> out of the a11y tree so it isn't exposed as a `listitem`,
                // which is an invalid child of `role="listbox"` (only `option`/`group` allowed).
                _jsx("li", { ref: (el) => {
                        itemRefs.current[index] = el;
                    }, role: "presentation", "data-testid": testId, children: _jsx(StaticItemComponent, { item: item }) }, item.id ?? index));
            }) }) }));
}
const makeItemId = (listboxId, item) => item?.type === "interactive" ? `item-${listboxId}-${item.id}` : undefined;
