Files
ThreadNet-Web/src/components/structures/GenericDropdownMenu.tsx
T
David LangleyandGitHub 491f0cd08a Change license (#13)
* Copyright headers 1

* Licence headers 2

* Copyright Headers 3

* Copyright Headers 4

* Copyright Headers 5

* Copyright Headers 6

* Copyright headers 7

* Add copyright headers for html and config file

* Replace license files and update package.json

* Update with CLA

* lint
2024-09-09 13:57:16 +00:00

205 lines
6.8 KiB
TypeScript

/*
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<T> = {
key: T;
label: ReactNode;
description?: ReactNode;
adornment?: ReactNode;
};
export type GenericDropdownMenuGroup<T> = GenericDropdownMenuOption<T> & {
options: GenericDropdownMenuOption<T>[];
};
export type GenericDropdownMenuItem<T> = GenericDropdownMenuGroup<T> | GenericDropdownMenuOption<T>;
export function GenericDropdownMenuOption<T extends Key>({
label,
description,
onClick,
isSelected,
adornment,
}: GenericDropdownMenuOption<T> & {
onClick: (ev: ButtonEvent) => void;
isSelected: boolean;
}): JSX.Element {
return (
<MenuItemRadio
active={isSelected}
className="mx_GenericDropdownMenu_Option mx_GenericDropdownMenu_Option--item"
onClick={onClick}
>
<div className="mx_GenericDropdownMenu_Option--label">
<span>{label}</span>
<span>{description}</span>
</div>
{adornment}
</MenuItemRadio>
);
}
export function GenericDropdownMenuGroup<T extends Key>({
label,
description,
adornment,
children,
}: PropsWithChildren<GenericDropdownMenuOption<T>>): JSX.Element {
return (
<>
<div className="mx_GenericDropdownMenu_Option mx_GenericDropdownMenu_Option--header">
<div className="mx_GenericDropdownMenu_Option--label">
<span>{label}</span>
<span>{description}</span>
</div>
{adornment}
</div>
{children}
</>
);
}
function isGenericDropdownMenuGroupArray<T>(
items: readonly GenericDropdownMenuItem<T>[],
): items is GenericDropdownMenuGroup<T>[] {
return isGenericDropdownMenuGroup(items[0]);
}
function isGenericDropdownMenuGroup<T>(item: GenericDropdownMenuItem<T>): item is GenericDropdownMenuGroup<T> {
return "options" in item;
}
type WithKeyFunction<T> = T extends Key
? {
toKey?: (key: T) => Key;
}
: {
toKey: (key: T) => Key;
};
export interface AdditionalOptionsProps {
menuDisplayed: boolean;
closeMenu: () => void;
openMenu: () => void;
}
type IProps<T> = WithKeyFunction<T> & {
value: T;
options: readonly GenericDropdownMenuOption<T>[] | readonly GenericDropdownMenuGroup<T>[];
onChange: (option: T) => void;
selectedLabel: (option: GenericDropdownMenuItem<T> | null | undefined) => ReactNode;
onOpen?: (ev: ButtonEvent) => void;
onClose?: (ev: ButtonEvent) => void;
className?: string;
AdditionalOptions?: FunctionComponent<AdditionalOptionsProps>;
};
export function GenericDropdownMenu<T>({
value,
onChange,
options,
selectedLabel,
onOpen,
onClose,
toKey,
className,
AdditionalOptions,
}: IProps<T>): JSX.Element {
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu<HTMLElement>();
const selected: GenericDropdownMenuItem<T> | undefined = options
.flatMap((it) => (isGenericDropdownMenuGroup(it) ? [it, ...it.options] : [it]))
.find((option) => (toKey ? toKey(option.key) === toKey(value) : option.key === value));
let contextMenuOptions: JSX.Element;
if (options && isGenericDropdownMenuGroupArray(options)) {
contextMenuOptions = (
<>
{options.map((group) => (
<GenericDropdownMenuGroup
key={toKey?.(group.key) ?? (group.key as Key)}
label={group.label}
description={group.description}
adornment={group.adornment}
>
{group.options.map((option) => (
<GenericDropdownMenuOption
key={toKey?.(option.key) ?? (option.key as Key)}
label={option.label}
description={option.description}
onClick={(ev: ButtonEvent) => {
onChange(option.key);
closeMenu();
onClose?.(ev);
}}
adornment={option.adornment}
isSelected={option === selected}
/>
))}
</GenericDropdownMenuGroup>
))}
</>
);
} else {
contextMenuOptions = (
<>
{options.map((option) => (
<GenericDropdownMenuOption
key={toKey?.(option.key) ?? (option.key as Key)}
label={option.label}
description={option.description}
onClick={(ev: ButtonEvent) => {
onChange(option.key);
closeMenu();
onClose?.(ev);
}}
adornment={option.adornment}
isSelected={option === selected}
/>
))}
</>
);
}
const contextMenu =
menuDisplayed && button.current ? (
<ContextMenu
onFinished={closeMenu}
chevronFace={ChevronFace.Top}
wrapperClassName={classNames("mx_GenericDropdownMenu_wrapper", className)}
{...aboveLeftOf(button.current.getBoundingClientRect())}
>
{contextMenuOptions}
{AdditionalOptions && (
<AdditionalOptions menuDisplayed={menuDisplayed} openMenu={openMenu} closeMenu={closeMenu} />
)}
</ContextMenu>
) : null;
return (
<>
<ContextMenuButton
className="mx_GenericDropdownMenu_button"
ref={button}
isExpanded={menuDisplayed}
onClick={(ev: ButtonEvent) => {
openMenu();
onOpen?.(ev);
}}
>
{selectedLabel(selected)}
</ContextMenuButton>
{contextMenu}
</>
);
}