2017-03-14 11:50:13 +00:00
|
|
|
/*
|
2024-09-09 14:57:16 +01:00
|
|
|
Copyright 2024 New Vector Ltd.
|
|
|
|
|
Copyright 2017-2021 The Matrix.org Foundation C.I.C.
|
2019-07-31 12:19:29 +01:00
|
|
|
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
2017-03-14 11:50:13 +00:00
|
|
|
|
2025-01-06 11:18:54 +00:00
|
|
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
2024-09-09 14:57:16 +01:00
|
|
|
Please see LICENSE files in the repository root for full details.
|
2017-03-14 11:50:13 +00:00
|
|
|
*/
|
|
|
|
|
|
2025-02-05 13:25:06 +00:00
|
|
|
import React, {
|
2025-03-27 10:43:58 +00:00
|
|
|
type JSX,
|
2025-02-05 13:25:06 +00:00
|
|
|
type ChangeEvent,
|
|
|
|
|
createRef,
|
|
|
|
|
type CSSProperties,
|
|
|
|
|
type ReactElement,
|
|
|
|
|
type ReactNode,
|
|
|
|
|
type Ref,
|
|
|
|
|
} from "react";
|
2017-03-14 11:50:13 +00:00
|
|
|
import classnames from "classnames";
|
2025-11-27 16:03:19 +00:00
|
|
|
import { ChevronDownIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
2021-07-06 09:56:02 +01:00
|
|
|
|
2025-02-05 13:25:06 +00:00
|
|
|
import AccessibleButton, { type ButtonEvent } from "./AccessibleButton";
|
2017-05-30 16:09:57 +02:00
|
|
|
import { _t } from "../../../languageHandler";
|
2022-02-28 17:05:52 +01:00
|
|
|
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
|
|
|
|
|
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
|
2022-11-18 09:22:43 +00:00
|
|
|
import { objectHasDiff } from "../../../utils/objects";
|
2025-02-05 13:25:06 +00:00
|
|
|
import { type NonEmptyArray } from "../../../@types/common";
|
2017-03-14 11:50:13 +00:00
|
|
|
|
2021-07-06 09:56:02 +01:00
|
|
|
interface IMenuOptionProps {
|
|
|
|
|
children: ReactElement;
|
|
|
|
|
highlighted?: boolean;
|
|
|
|
|
dropdownKey: string;
|
|
|
|
|
id?: string;
|
2023-04-20 18:13:30 +01:00
|
|
|
inputRef?: Ref<HTMLLIElement>;
|
2021-07-06 09:56:02 +01:00
|
|
|
onClick(dropdownKey: string): void;
|
|
|
|
|
onMouseEnter(dropdownKey: string): void;
|
|
|
|
|
}
|
2017-03-14 11:50:13 +00:00
|
|
|
|
2021-07-06 09:56:02 +01:00
|
|
|
class MenuOption extends React.Component<IMenuOptionProps> {
|
2022-12-16 12:29:59 +00:00
|
|
|
public static defaultProps = {
|
2017-11-03 12:30:58 +00:00
|
|
|
disabled: false,
|
|
|
|
|
};
|
2017-10-10 19:16:42 +01:00
|
|
|
|
2023-01-12 13:25:14 +00:00
|
|
|
private onMouseEnter = (): void => {
|
2017-03-14 11:50:13 +00:00
|
|
|
this.props.onMouseEnter(this.props.dropdownKey);
|
2021-07-06 09:56:02 +01:00
|
|
|
};
|
2017-03-14 11:50:13 +00:00
|
|
|
|
2023-01-12 13:25:14 +00:00
|
|
|
private onClick = (e: React.MouseEvent): void => {
|
2017-03-14 11:50:13 +00:00
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
this.props.onClick(this.props.dropdownKey);
|
2021-07-06 09:56:02 +01:00
|
|
|
};
|
2017-03-14 11:50:13 +00:00
|
|
|
|
2023-02-13 17:01:43 +00:00
|
|
|
public render(): React.ReactNode {
|
2017-03-14 11:50:13 +00:00
|
|
|
const optClasses = classnames({
|
|
|
|
|
mx_Dropdown_option: true,
|
|
|
|
|
mx_Dropdown_option_highlight: this.props.highlighted,
|
|
|
|
|
});
|
|
|
|
|
|
2019-12-15 14:24:56 +00:00
|
|
|
return (
|
2023-04-20 18:13:30 +01:00
|
|
|
<li
|
2019-12-15 15:04:57 +00:00
|
|
|
id={this.props.id}
|
2019-12-15 14:24:56 +00:00
|
|
|
className={optClasses}
|
2021-07-06 09:56:02 +01:00
|
|
|
onClick={this.onClick}
|
|
|
|
|
onMouseEnter={this.onMouseEnter}
|
2019-12-15 14:24:56 +00:00
|
|
|
role="option"
|
|
|
|
|
aria-selected={this.props.highlighted}
|
|
|
|
|
ref={this.props.inputRef}
|
2025-10-01 15:26:42 +02:00
|
|
|
tabIndex={-1}
|
2017-03-14 11:50:13 +00:00
|
|
|
>
|
2017-10-11 17:56:17 +01:00
|
|
|
{this.props.children}
|
2023-04-20 18:13:30 +01:00
|
|
|
</li>
|
2017-10-11 17:56:17 +01:00
|
|
|
);
|
2017-03-14 11:50:13 +00:00
|
|
|
}
|
2017-10-11 17:56:17 +01:00
|
|
|
}
|
2017-03-14 11:50:13 +00:00
|
|
|
|
2022-08-30 19:11:33 +02:00
|
|
|
export interface DropdownProps {
|
2021-07-06 09:56:02 +01:00
|
|
|
id: string;
|
|
|
|
|
// ARIA label
|
|
|
|
|
label: string;
|
|
|
|
|
value?: string;
|
|
|
|
|
className?: string;
|
2023-04-20 18:13:30 +01:00
|
|
|
autoComplete?: string;
|
2023-03-29 08:23:54 +01:00
|
|
|
children: NonEmptyArray<ReactElement & { key: string }>;
|
2021-07-06 09:56:02 +01:00
|
|
|
// negative for consistency with HTML
|
|
|
|
|
disabled?: boolean;
|
|
|
|
|
// The width that the dropdown should be. If specified,
|
|
|
|
|
// the dropped-down part of the menu will be set to this
|
|
|
|
|
// width.
|
|
|
|
|
menuWidth?: number;
|
|
|
|
|
searchEnabled?: boolean;
|
2022-07-28 10:10:04 +02:00
|
|
|
// Placeholder to show when no value is selected
|
|
|
|
|
placeholder?: string;
|
2021-07-06 09:56:02 +01:00
|
|
|
// Called when the selected option changes
|
|
|
|
|
onOptionChange(dropdownKey: string): void;
|
|
|
|
|
// Called when the value of the search field changes
|
|
|
|
|
onSearchChange?(query: string): void;
|
|
|
|
|
// Function that, given the key of an option, returns
|
|
|
|
|
// a node representing that option to be displayed in the
|
|
|
|
|
// box itself as the currently-selected option (ie. as
|
|
|
|
|
// opposed to in the actual dropped-down part). If
|
|
|
|
|
// unspecified, the appropriate child element is used as
|
|
|
|
|
// in the dropped-down menu.
|
|
|
|
|
getShortOption?(value: string): ReactNode;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface IState {
|
|
|
|
|
expanded: boolean;
|
2023-03-29 08:23:54 +01:00
|
|
|
highlightedOption: string;
|
2021-07-06 09:56:02 +01:00
|
|
|
searchQuery: string;
|
|
|
|
|
}
|
2017-03-14 11:50:13 +00:00
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* Reusable dropdown select control, akin to react-select,
|
|
|
|
|
* but somewhat simpler as react-select is 79KB of minified
|
|
|
|
|
* javascript.
|
|
|
|
|
*/
|
2022-08-30 19:11:33 +02:00
|
|
|
export default class Dropdown extends React.Component<DropdownProps, IState> {
|
2021-07-06 09:56:02 +01:00
|
|
|
private readonly buttonRef = createRef<HTMLDivElement>();
|
2023-03-13 15:07:20 +00:00
|
|
|
private dropdownRootElement: HTMLDivElement | null = null;
|
|
|
|
|
private ignoreEvent: MouseEvent | null = null;
|
2021-07-06 09:56:02 +01:00
|
|
|
private childrenByKey: Record<string, ReactNode> = {};
|
|
|
|
|
|
2022-12-16 12:29:59 +00:00
|
|
|
public constructor(props: DropdownProps) {
|
2017-03-14 11:50:13 +00:00
|
|
|
super(props);
|
|
|
|
|
|
2021-07-06 09:56:02 +01:00
|
|
|
this.reindexChildren(this.props.children);
|
2017-03-14 11:50:13 +00:00
|
|
|
|
2023-03-29 08:23:54 +01:00
|
|
|
const firstChild = props.children[0];
|
2017-03-14 11:50:13 +00:00
|
|
|
|
|
|
|
|
this.state = {
|
|
|
|
|
// True if the menu is dropped-down
|
|
|
|
|
expanded: false,
|
|
|
|
|
// The key of the highlighted option
|
|
|
|
|
// (the option that would become selected if you pressed enter)
|
2023-03-29 08:23:54 +01:00
|
|
|
highlightedOption: firstChild.key,
|
2017-03-14 11:50:13 +00:00
|
|
|
// the current search query
|
|
|
|
|
searchQuery: "",
|
|
|
|
|
};
|
2024-11-01 17:39:08 +00:00
|
|
|
}
|
2017-03-14 11:50:13 +00:00
|
|
|
|
2024-11-01 17:39:08 +00:00
|
|
|
public componentDidMount(): void {
|
2017-03-14 11:50:13 +00:00
|
|
|
// Listen for all clicks on the document so we can close the
|
|
|
|
|
// menu when the user clicks somewhere else
|
2021-07-06 09:56:02 +01:00
|
|
|
document.addEventListener("click", this.onDocumentClick, false);
|
2017-03-14 11:50:13 +00:00
|
|
|
}
|
|
|
|
|
|
2023-01-12 13:25:14 +00:00
|
|
|
public componentDidUpdate(prevProps: Readonly<DropdownProps>): void {
|
2022-11-18 09:22:43 +00:00
|
|
|
if (objectHasDiff(this.props, prevProps) && this.props.children?.length) {
|
|
|
|
|
this.reindexChildren(this.props.children);
|
|
|
|
|
const firstChild = this.props.children[0];
|
|
|
|
|
this.setState({
|
2023-03-29 08:23:54 +01:00
|
|
|
highlightedOption: firstChild.key,
|
2022-11-18 09:22:43 +00:00
|
|
|
});
|
|
|
|
|
}
|
2017-03-14 11:50:13 +00:00
|
|
|
}
|
|
|
|
|
|
2023-01-12 13:25:14 +00:00
|
|
|
public componentWillUnmount(): void {
|
2022-11-18 09:22:43 +00:00
|
|
|
document.removeEventListener("click", this.onDocumentClick, false);
|
2017-03-14 11:50:13 +00:00
|
|
|
}
|
|
|
|
|
|
2021-07-06 09:56:02 +01:00
|
|
|
private reindexChildren(children: ReactElement[]): void {
|
2017-03-14 11:50:13 +00:00
|
|
|
this.childrenByKey = {};
|
|
|
|
|
React.Children.forEach(children, (child) => {
|
2023-03-29 08:23:54 +01:00
|
|
|
this.childrenByKey[(child as DropdownProps["children"][number]).key] = child;
|
2017-03-14 11:50:13 +00:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-12 13:25:14 +00:00
|
|
|
private onDocumentClick = (ev: MouseEvent): void => {
|
2017-03-14 11:50:13 +00:00
|
|
|
// Close the dropdown if the user clicks anywhere that isn't
|
|
|
|
|
// within our root element
|
|
|
|
|
if (ev !== this.ignoreEvent) {
|
|
|
|
|
this.setState({
|
|
|
|
|
expanded: false,
|
|
|
|
|
});
|
|
|
|
|
}
|
2021-07-06 09:56:02 +01:00
|
|
|
};
|
2017-03-14 11:50:13 +00:00
|
|
|
|
2023-01-12 13:25:14 +00:00
|
|
|
private onRootClick = (ev: MouseEvent): void => {
|
2017-03-14 11:50:13 +00:00
|
|
|
// This captures any clicks that happen within our elements,
|
|
|
|
|
// such that we can then ignore them when they're seen by the
|
|
|
|
|
// click listener on the document handler, ie. not close the
|
|
|
|
|
// dropdown immediately after opening it.
|
|
|
|
|
// NB. We can't just stopPropagation() because then the event
|
|
|
|
|
// doesn't reach the React onClick().
|
|
|
|
|
this.ignoreEvent = ev;
|
2021-07-06 09:56:02 +01:00
|
|
|
};
|
2017-03-14 11:50:13 +00:00
|
|
|
|
2023-01-12 13:25:14 +00:00
|
|
|
private onAccessibleButtonClick = (ev: ButtonEvent): void => {
|
2017-10-10 19:16:42 +01:00
|
|
|
if (this.props.disabled) return;
|
|
|
|
|
|
2022-02-28 17:05:52 +01:00
|
|
|
const action = getKeyBindingsManager().getAccessibilityAction(ev as React.KeyboardEvent);
|
|
|
|
|
|
2017-05-18 16:58:57 +01:00
|
|
|
if (!this.state.expanded) {
|
2021-11-26 15:41:04 +00:00
|
|
|
this.setState({ expanded: true });
|
2017-05-18 16:58:57 +01:00
|
|
|
ev.preventDefault();
|
2022-02-28 17:05:52 +01:00
|
|
|
} else if (action === KeyBindingAction.Enter) {
|
2021-08-06 14:48:46 +01:00
|
|
|
// the accessible button consumes enter onKeyDown for firing onClick, so handle it here
|
|
|
|
|
this.props.onOptionChange(this.state.highlightedOption);
|
|
|
|
|
this.close();
|
2021-11-26 15:41:04 +00:00
|
|
|
} else if (!(ev as React.KeyboardEvent).key) {
|
|
|
|
|
// collapse on other non-keyboard event activations
|
|
|
|
|
this.setState({ expanded: false });
|
|
|
|
|
ev.preventDefault();
|
2017-05-18 16:58:57 +01:00
|
|
|
}
|
2021-07-06 09:56:02 +01:00
|
|
|
};
|
2017-03-14 11:50:13 +00:00
|
|
|
|
2023-01-12 13:25:14 +00:00
|
|
|
private close(): void {
|
2017-03-14 11:50:13 +00:00
|
|
|
this.setState({
|
|
|
|
|
expanded: false,
|
|
|
|
|
});
|
2019-12-15 15:04:57 +00:00
|
|
|
// their focus was on the input, its getting unmounted, move it to the button
|
2021-07-06 09:56:02 +01:00
|
|
|
if (this.buttonRef.current) {
|
|
|
|
|
this.buttonRef.current.focus();
|
2019-12-15 15:04:57 +00:00
|
|
|
}
|
2019-12-16 10:03:40 +00:00
|
|
|
}
|
|
|
|
|
|
2023-01-12 13:25:14 +00:00
|
|
|
private onMenuOptionClick = (dropdownKey: string): void => {
|
2021-07-06 09:56:02 +01:00
|
|
|
this.close();
|
2017-03-14 11:50:13 +00:00
|
|
|
this.props.onOptionChange(dropdownKey);
|
2021-07-06 09:56:02 +01:00
|
|
|
};
|
2017-03-14 11:50:13 +00:00
|
|
|
|
2023-01-12 13:25:14 +00:00
|
|
|
private onKeyDown = (e: React.KeyboardEvent): void => {
|
2019-12-17 15:14:01 +00:00
|
|
|
let handled = true;
|
2017-03-14 11:50:13 +00:00
|
|
|
|
2019-12-15 14:24:56 +00:00
|
|
|
// These keys don't generate keypress events and so needs to be on keyup
|
2022-02-28 17:05:52 +01:00
|
|
|
const action = getKeyBindingsManager().getAccessibilityAction(e);
|
|
|
|
|
switch (action) {
|
|
|
|
|
case KeyBindingAction.Enter:
|
2019-12-15 14:24:56 +00:00
|
|
|
this.props.onOptionChange(this.state.highlightedOption);
|
2019-12-17 17:31:29 +00:00
|
|
|
// fallthrough
|
2022-02-28 17:05:52 +01:00
|
|
|
case KeyBindingAction.Escape:
|
2021-07-06 09:56:02 +01:00
|
|
|
this.close();
|
2019-12-15 14:24:56 +00:00
|
|
|
break;
|
2022-02-28 17:05:52 +01:00
|
|
|
case KeyBindingAction.ArrowDown:
|
2021-12-06 09:25:44 +00:00
|
|
|
if (this.state.expanded) {
|
2025-10-01 15:26:42 +02:00
|
|
|
const nextKey = this.nextOption(this.state.highlightedOption);
|
2021-12-06 09:25:44 +00:00
|
|
|
this.setState({
|
2025-10-01 15:26:42 +02:00
|
|
|
highlightedOption: nextKey,
|
2021-12-06 09:25:44 +00:00
|
|
|
});
|
2025-10-01 15:26:42 +02:00
|
|
|
|
|
|
|
|
(
|
|
|
|
|
this.dropdownRootElement?.querySelector(`#${this.props.id}__${nextKey}`) as HTMLLIElement
|
|
|
|
|
)?.focus();
|
2021-12-06 09:25:44 +00:00
|
|
|
} else {
|
|
|
|
|
this.setState({ expanded: true });
|
|
|
|
|
}
|
2019-12-15 14:24:56 +00:00
|
|
|
break;
|
2022-02-28 17:05:52 +01:00
|
|
|
case KeyBindingAction.ArrowUp:
|
2021-12-06 09:25:44 +00:00
|
|
|
if (this.state.expanded) {
|
2025-10-01 15:26:42 +02:00
|
|
|
const prevKey = this.prevOption(this.state.highlightedOption);
|
2021-12-06 09:25:44 +00:00
|
|
|
this.setState({
|
2025-10-01 15:26:42 +02:00
|
|
|
highlightedOption: prevKey,
|
2021-12-06 09:25:44 +00:00
|
|
|
});
|
2025-10-01 15:26:42 +02:00
|
|
|
(
|
|
|
|
|
this.dropdownRootElement?.querySelector(`#${this.props.id}__${prevKey}`) as HTMLLIElement
|
|
|
|
|
)?.focus();
|
2021-12-06 09:25:44 +00:00
|
|
|
} else {
|
|
|
|
|
this.setState({ expanded: true });
|
|
|
|
|
}
|
2019-12-15 14:24:56 +00:00
|
|
|
break;
|
2019-12-17 15:14:01 +00:00
|
|
|
default:
|
|
|
|
|
handled = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (handled) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
2017-03-14 11:50:13 +00:00
|
|
|
}
|
2021-07-06 09:56:02 +01:00
|
|
|
};
|
2017-03-14 11:50:13 +00:00
|
|
|
|
2023-01-12 13:25:14 +00:00
|
|
|
private onInputChange = (e: ChangeEvent<HTMLInputElement>): void => {
|
2017-03-14 11:50:13 +00:00
|
|
|
this.setState({
|
2021-07-06 09:56:02 +01:00
|
|
|
searchQuery: e.currentTarget.value,
|
2017-03-14 11:50:13 +00:00
|
|
|
});
|
|
|
|
|
if (this.props.onSearchChange) {
|
2021-07-06 09:56:02 +01:00
|
|
|
this.props.onSearchChange(e.currentTarget.value);
|
2017-03-14 11:50:13 +00:00
|
|
|
}
|
2021-07-06 09:56:02 +01:00
|
|
|
};
|
2017-03-14 11:50:13 +00:00
|
|
|
|
2023-01-12 13:25:14 +00:00
|
|
|
private collectRoot = (e: HTMLDivElement): void => {
|
2017-03-14 11:50:13 +00:00
|
|
|
if (this.dropdownRootElement) {
|
2021-07-06 09:56:02 +01:00
|
|
|
this.dropdownRootElement.removeEventListener("click", this.onRootClick, false);
|
2017-03-14 11:50:13 +00:00
|
|
|
}
|
|
|
|
|
if (e) {
|
2021-07-06 09:56:02 +01:00
|
|
|
e.addEventListener("click", this.onRootClick, false);
|
2017-03-14 11:50:13 +00:00
|
|
|
}
|
|
|
|
|
this.dropdownRootElement = e;
|
2021-07-06 09:56:02 +01:00
|
|
|
};
|
2017-03-14 11:50:13 +00:00
|
|
|
|
2023-01-12 13:25:14 +00:00
|
|
|
private setHighlightedOption = (optionKey: string): void => {
|
2017-03-14 11:50:13 +00:00
|
|
|
this.setState({
|
|
|
|
|
highlightedOption: optionKey,
|
|
|
|
|
});
|
2021-07-06 09:56:02 +01:00
|
|
|
};
|
2017-03-14 11:50:13 +00:00
|
|
|
|
2021-07-06 09:56:02 +01:00
|
|
|
private nextOption(optionKey: string): string {
|
2017-03-14 11:50:13 +00:00
|
|
|
const keys = Object.keys(this.childrenByKey);
|
|
|
|
|
const index = keys.indexOf(optionKey);
|
|
|
|
|
return keys[(index + 1) % keys.length];
|
|
|
|
|
}
|
|
|
|
|
|
2021-07-06 09:56:02 +01:00
|
|
|
private prevOption(optionKey: string): string {
|
2017-03-14 11:50:13 +00:00
|
|
|
const keys = Object.keys(this.childrenByKey);
|
|
|
|
|
const index = keys.indexOf(optionKey);
|
2021-08-06 14:21:56 +01:00
|
|
|
return keys[index <= 0 ? keys.length - 1 : (index - 1) % keys.length];
|
2017-03-14 11:50:13 +00:00
|
|
|
}
|
|
|
|
|
|
2023-03-29 08:23:54 +01:00
|
|
|
private scrollIntoView(node: Element | null): void {
|
|
|
|
|
node?.scrollIntoView({
|
|
|
|
|
block: "nearest",
|
|
|
|
|
behavior: "auto",
|
|
|
|
|
});
|
2019-12-15 14:24:56 +00:00
|
|
|
}
|
|
|
|
|
|
2023-01-12 13:25:14 +00:00
|
|
|
private getMenuOptions(): JSX.Element[] {
|
2022-09-12 11:58:05 +01:00
|
|
|
const options = React.Children.map(this.props.children, (child: ReactElement) => {
|
2019-12-15 14:24:56 +00:00
|
|
|
const highlighted = this.state.highlightedOption === child.key;
|
2017-03-14 11:50:13 +00:00
|
|
|
return (
|
2019-12-15 14:24:56 +00:00
|
|
|
<MenuOption
|
2019-12-15 15:04:57 +00:00
|
|
|
id={`${this.props.id}__${child.key}`}
|
2019-12-15 14:24:56 +00:00
|
|
|
key={child.key}
|
2021-07-06 09:56:02 +01:00
|
|
|
dropdownKey={child.key as string}
|
2019-12-15 14:24:56 +00:00
|
|
|
highlighted={highlighted}
|
2021-07-06 09:56:02 +01:00
|
|
|
onMouseEnter={this.setHighlightedOption}
|
|
|
|
|
onClick={this.onMenuOptionClick}
|
|
|
|
|
inputRef={highlighted ? this.scrollIntoView : undefined}
|
2017-03-14 11:50:13 +00:00
|
|
|
>
|
2017-10-11 17:56:17 +01:00
|
|
|
{child}
|
2017-03-14 11:50:13 +00:00
|
|
|
</MenuOption>
|
|
|
|
|
);
|
|
|
|
|
});
|
2023-03-29 08:23:54 +01:00
|
|
|
if (!options?.length) {
|
2022-01-04 09:46:21 +01:00
|
|
|
return [
|
2023-04-20 18:13:30 +01:00
|
|
|
<li key="0" className="mx_Dropdown_option" role="option" aria-selected={false}>
|
2023-08-22 18:47:33 +01:00
|
|
|
{_t("common|no_results")}
|
2023-04-20 18:13:30 +01:00
|
|
|
</li>,
|
2017-04-21 11:37:08 +01:00
|
|
|
];
|
2017-03-14 11:50:13 +00:00
|
|
|
}
|
|
|
|
|
return options;
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-13 17:01:43 +00:00
|
|
|
public render(): React.ReactNode {
|
2023-04-20 18:13:30 +01:00
|
|
|
let currentValue: JSX.Element | undefined;
|
2017-03-14 11:50:13 +00:00
|
|
|
|
2021-07-06 09:56:02 +01:00
|
|
|
const menuStyle: CSSProperties = {};
|
2017-03-14 11:50:13 +00:00
|
|
|
if (this.props.menuWidth) menuStyle.width = this.props.menuWidth;
|
|
|
|
|
|
2023-04-20 18:13:30 +01:00
|
|
|
let menu: JSX.Element | undefined;
|
2017-03-14 11:50:13 +00:00
|
|
|
if (this.state.expanded) {
|
2017-04-19 10:08:04 +01:00
|
|
|
if (this.props.searchEnabled) {
|
2019-12-15 15:04:57 +00:00
|
|
|
currentValue = (
|
|
|
|
|
<input
|
2022-01-04 09:46:21 +01:00
|
|
|
id={`${this.props.id}_input`}
|
2019-12-15 15:04:57 +00:00
|
|
|
type="text"
|
2021-07-06 09:56:02 +01:00
|
|
|
autoFocus={true}
|
2023-04-20 18:13:30 +01:00
|
|
|
autoComplete={this.props.autoComplete}
|
2019-12-15 15:04:57 +00:00
|
|
|
className="mx_Dropdown_option"
|
2021-07-06 09:56:02 +01:00
|
|
|
onChange={this.onInputChange}
|
2019-12-15 15:04:57 +00:00
|
|
|
value={this.state.searchQuery}
|
|
|
|
|
role="combobox"
|
|
|
|
|
aria-autocomplete="list"
|
|
|
|
|
aria-activedescendant={`${this.props.id}__${this.state.highlightedOption}`}
|
2022-01-04 09:46:21 +01:00
|
|
|
aria-expanded={this.state.expanded}
|
|
|
|
|
aria-controls={`${this.props.id}_listbox`}
|
2019-12-15 15:04:57 +00:00
|
|
|
aria-disabled={this.props.disabled}
|
|
|
|
|
aria-label={this.props.label}
|
2021-08-06 14:48:46 +01:00
|
|
|
onKeyDown={this.onKeyDown}
|
2019-12-15 15:04:57 +00:00
|
|
|
/>
|
|
|
|
|
);
|
2017-04-19 10:08:04 +01:00
|
|
|
}
|
2019-12-15 15:04:57 +00:00
|
|
|
menu = (
|
2023-04-20 18:13:30 +01:00
|
|
|
<ul className="mx_Dropdown_menu" style={menuStyle} role="listbox" id={`${this.props.id}_listbox`}>
|
2021-07-06 09:56:02 +01:00
|
|
|
{this.getMenuOptions()}
|
2023-04-20 18:13:30 +01:00
|
|
|
</ul>
|
2019-12-15 15:04:57 +00:00
|
|
|
);
|
2017-04-19 10:08:04 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!currentValue) {
|
2023-03-29 08:23:54 +01:00
|
|
|
let selectedChild: ReactNode | undefined;
|
|
|
|
|
if (this.props.value) {
|
|
|
|
|
selectedChild = this.props.getShortOption
|
|
|
|
|
? this.props.getShortOption(this.props.value)
|
|
|
|
|
: this.childrenByKey[this.props.value];
|
|
|
|
|
}
|
|
|
|
|
|
2019-12-15 15:04:57 +00:00
|
|
|
currentValue = (
|
|
|
|
|
<div className="mx_Dropdown_option" id={`${this.props.id}_value`}>
|
2022-07-28 10:10:04 +02:00
|
|
|
{selectedChild || this.props.placeholder}
|
2017-10-11 17:56:17 +01:00
|
|
|
</div>
|
|
|
|
|
);
|
2017-03-14 11:50:13 +00:00
|
|
|
}
|
|
|
|
|
|
2023-03-13 15:07:20 +00:00
|
|
|
const dropdownClasses = classnames("mx_Dropdown", this.props.className, {
|
|
|
|
|
mx_Dropdown_disabled: !!this.props.disabled,
|
|
|
|
|
});
|
2017-03-14 11:50:13 +00:00
|
|
|
|
|
|
|
|
// Note the menu sits inside the AccessibleButton div so it's anchored
|
|
|
|
|
// to the input, but overflows below it. The root contains both.
|
2021-08-06 14:48:46 +01:00
|
|
|
return (
|
2023-03-13 15:07:20 +00:00
|
|
|
<div className={dropdownClasses} ref={this.collectRoot}>
|
2019-12-15 15:04:57 +00:00
|
|
|
<AccessibleButton
|
|
|
|
|
className="mx_Dropdown_input mx_no_textinput"
|
2021-08-06 14:48:46 +01:00
|
|
|
onClick={this.onAccessibleButtonClick}
|
2019-12-15 15:04:57 +00:00
|
|
|
aria-haspopup="listbox"
|
|
|
|
|
aria-expanded={this.state.expanded}
|
|
|
|
|
disabled={this.props.disabled}
|
2023-12-21 08:50:42 +00:00
|
|
|
ref={this.buttonRef}
|
2019-12-15 15:04:57 +00:00
|
|
|
aria-label={this.props.label}
|
|
|
|
|
aria-describedby={`${this.props.id}_value`}
|
2022-01-04 09:46:21 +01:00
|
|
|
aria-owns={`${this.props.id}_input`}
|
2021-08-06 14:48:46 +01:00
|
|
|
onKeyDown={this.onKeyDown}
|
2019-12-15 15:04:57 +00:00
|
|
|
>
|
2017-10-11 17:56:17 +01:00
|
|
|
{currentValue}
|
2025-11-27 16:03:19 +00:00
|
|
|
<ChevronDownIcon className="mx_Dropdown_arrow" />
|
2017-10-11 17:56:17 +01:00
|
|
|
{menu}
|
2017-03-14 11:50:13 +00:00
|
|
|
</AccessibleButton>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|