/* Copyright 2024 New Vector Ltd. Copyright 2022 The Matrix.org Foundation C.I.C. SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ import classNames from "classnames"; import React, { FunctionComponent, Key, PropsWithChildren, ReactNode } from "react"; import { MenuItemRadio } from "../../accessibility/context_menu/MenuItemRadio"; import { ButtonEvent } from "../views/elements/AccessibleButton"; import ContextMenu, { aboveLeftOf, ChevronFace, ContextMenuButton, useContextMenu } from "./ContextMenu"; export type GenericDropdownMenuOption = { key: T; label: ReactNode; description?: ReactNode; adornment?: ReactNode; }; export type GenericDropdownMenuGroup = GenericDropdownMenuOption & { options: GenericDropdownMenuOption[]; }; export type GenericDropdownMenuItem = GenericDropdownMenuGroup | GenericDropdownMenuOption; export function GenericDropdownMenuOption({ label, description, onClick, isSelected, adornment, }: GenericDropdownMenuOption & { onClick: (ev: ButtonEvent) => void; isSelected: boolean; }): JSX.Element { return (
{label} {description}
{adornment}
); } export function GenericDropdownMenuGroup({ label, description, adornment, children, }: PropsWithChildren>): JSX.Element { return ( <>
{label} {description}
{adornment}
{children} ); } function isGenericDropdownMenuGroupArray( items: readonly GenericDropdownMenuItem[], ): items is GenericDropdownMenuGroup[] { return isGenericDropdownMenuGroup(items[0]); } function isGenericDropdownMenuGroup(item: GenericDropdownMenuItem): item is GenericDropdownMenuGroup { return "options" in item; } type WithKeyFunction = T extends Key ? { toKey?: (key: T) => Key; } : { toKey: (key: T) => Key; }; export interface AdditionalOptionsProps { menuDisplayed: boolean; closeMenu: () => void; openMenu: () => void; } type IProps = WithKeyFunction & { value: T; options: readonly GenericDropdownMenuOption[] | readonly GenericDropdownMenuGroup[]; onChange: (option: T) => void; selectedLabel: (option: GenericDropdownMenuItem | null | undefined) => ReactNode; onOpen?: (ev: ButtonEvent) => void; onClose?: (ev: ButtonEvent) => void; className?: string; AdditionalOptions?: FunctionComponent; }; function calculateKey(value: T, toKey: ((key: T) => Key) | undefined): Key { return toKey ? toKey(value) : (value as Key); } export function GenericDropdownMenu({ value, onChange, options, selectedLabel, onOpen, onClose, toKey, className, AdditionalOptions, }: IProps): JSX.Element { const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); const valueKey = calculateKey(value, toKey); const selected: GenericDropdownMenuItem | undefined = options .flatMap((it) => (isGenericDropdownMenuGroup(it) ? [it, ...it.options] : [it])) .find((option) => calculateKey(option.key, toKey) === valueKey); let contextMenuOptions: JSX.Element; if (options && isGenericDropdownMenuGroupArray(options)) { contextMenuOptions = ( <> {options.map((group) => ( {group.options.map((option) => ( { onChange(option.key); closeMenu(); onClose?.(ev); }} adornment={option.adornment} isSelected={calculateKey(option.key, toKey) === valueKey} /> ))} ))} ); } else { contextMenuOptions = ( <> {options.map((option) => ( { onChange(option.key); closeMenu(); onClose?.(ev); }} adornment={option.adornment} isSelected={calculateKey(option.key, toKey) === valueKey} /> ))} ); } const contextMenu = menuDisplayed && button.current ? ( {contextMenuOptions} {AdditionalOptions && ( )} ) : null; return ( <> { openMenu(); onOpen?.(ev); }} > {selectedLabel(selected)} {contextMenu} ); }