Files
ThreadNet-Web/apps/web/src/components/views/dialogs/BaseDialog.tsx
T

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

210 lines
7.9 KiB
TypeScript
Raw Normal View History

2017-01-24 15:54:05 +00:00
/*
2024-09-09 14:57:16 +01:00
Copyright 2024 New Vector Ltd.
Copyright 2019 The Matrix.org Foundation C.I.C.
2024-09-09 14:57:16 +01:00
Copyright 2018, 2019 New Vector Ltd
Copyright 2017 Vector Creations Ltd
2017-01-24 15:54:05 +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-01-24 15:54:05 +00:00
*/
import React, { type JSX } from "react";
import FocusLock from "react-focus-lock";
import classNames from "classnames";
2025-02-05 13:25:06 +00:00
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
2025-12-02 13:47:14 +00:00
import { I18nContext } from "@element-hq/web-shared-components";
import { CloseIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
2017-01-24 15:54:05 +00:00
import AccessibleButton from "../elements/AccessibleButton";
2021-06-29 13:11:58 +01:00
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { _t } from "../../../languageHandler";
2019-12-17 17:26:12 +00:00
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import Heading from "../typography/Heading";
2025-02-05 13:25:06 +00:00
import { PosthogScreenTracker, type ScreenName } from "../../../PosthogTrackers";
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
2021-09-04 16:16:21 +02:00
interface IProps {
/**
* Whether the dialog should have a 'close' button and a keyDown handler which
* will intercept 'Escape'.
*
* This should only be set to `false` if there is nothing the app can sensibly do if the
* dialog is cancelled, eg. "We can't restore your session and
* the app cannot work".
*
* Default: `true`.
*/
2021-09-04 16:16:21 +02:00
"hasCancel"?: boolean;
/**
* Callback that will be called when the 'close' button is clicked or 'Escape' is pressed.
*
* Not used if `hasCancel` is false.
*/
"onFinished"?: () => void;
2021-09-04 16:16:21 +02:00
// called when a key is pressed
"onKeyDown"?: (e: KeyboardEvent | React.KeyboardEvent) => void;
// CSS class to apply to dialog div
"className"?: string;
// if true, dialog container is 60% of the viewport width. Otherwise,
// the container will have no fixed size, allowing its contents to
// determine its size. Default: true.
"fixedWidth"?: boolean;
// To be displayed at the top of the dialog. Even above the title.
"top"?: React.ReactNode;
2021-09-04 16:16:21 +02:00
// Title for the dialog.
"title"?: React.ReactNode;
// Specific aria label to use, if not provided will set aria-labelledBy to mx_Dialog_title
"aria-label"?: string;
2021-09-04 16:16:21 +02:00
// Path to an icon to put in the header
"headerImage"?: string;
// children should be the content of the dialog
"children"?: React.ReactNode;
// Id of content element
// If provided, this is used to add a aria-describedby attribute
"contentId"?: string;
// optional additional class for the title element (basically anything that can be passed to classnames)
"titleClass"?: string | string[];
"headerButton"?: JSX.Element;
2022-02-09 14:25:58 +00:00
// optional Posthog ScreenName to supply during the lifetime of this dialog
"screenName"?: ScreenName;
2021-09-04 16:16:21 +02:00
}
2017-01-24 15:54:05 +00:00
2020-08-29 12:57:11 +01:00
/*
2017-01-24 15:54:05 +00:00
* Basic container for modal dialogs.
*
* Includes a div for the title, and a keypress handler which cancels the
* dialog on escape.
*/
2021-09-04 16:16:21 +02:00
export default class BaseDialog extends React.Component<IProps> {
private matrixClient: MatrixClient;
2017-01-24 15:54:05 +00:00
public static defaultProps: Partial<IProps> = {
2020-08-29 12:14:16 +01:00
hasCancel: true,
fixedWidth: true,
};
2023-02-13 11:39:16 +00:00
public constructor(props: IProps) {
2020-08-29 12:14:16 +01:00
super(props);
2018-04-27 12:38:49 +01:00
// XXX: The contract on MatrixClientContext says it is only available within a LoggedInView subtree,
// given that modals function outside the MatrixChat React tree this simulates that. We don't want to
// use safeGet as it throwing would mean we cannot use modals whilst the user isn't logged in.
// The longer term solution is to move our ModalManager into the React tree to inherit contexts properly.
this.matrixClient = MatrixClientPeg.get()!;
2020-08-29 12:14:16 +01:00
}
2018-02-07 09:45:36 +00:00
2021-09-04 16:16:21 +02:00
private onKeyDown = (e: KeyboardEvent | React.KeyboardEvent): void => {
this.props.onKeyDown?.(e);
const action = getKeyBindingsManager().getAccessibilityAction(e);
switch (action) {
case KeyBindingAction.Escape:
if (!this.props.hasCancel) break;
e.stopPropagation();
e.preventDefault();
this.props.onFinished?.();
break;
2017-01-24 15:54:05 +00:00
}
2020-08-29 12:14:16 +01:00
};
2017-01-24 15:54:05 +00:00
private onCancelClick = (): void => {
this.props.onFinished?.();
2020-08-29 12:14:16 +01:00
};
2017-02-13 16:03:21 +00:00
public render(): React.ReactNode {
let cancelButton;
if (this.props.hasCancel) {
cancelButton = (
2021-09-04 16:16:21 +02:00
<AccessibleButton
onClick={this.onCancelClick}
className="mx_Dialog_cancelButton"
2024-12-03 15:25:06 +01:00
title={_t("action|close")}
aria-label={_t("dialog_close_label")}
2024-06-26 17:47:01 +02:00
placement="bottom"
>
<CloseIcon />
</AccessibleButton>
);
}
2020-01-22 11:44:47 +00:00
let headerImage;
if (this.props.headerImage) {
2021-07-23 10:23:45 +01:00
headerImage = <img className="mx_Dialog_titleImage" src={this.props.headerImage} alt="" />;
2020-01-22 11:44:47 +00:00
}
2023-02-13 11:39:16 +00:00
const lockProps: Record<string, any> = {
"onKeyDown": this.onKeyDown,
"role": "dialog",
// Allow the dialog to be keyboard focusable
// So the escape key handling works in more cases (say you select the header)
"tabIndex": -1,
// This should point to a node describing the dialog.
// If we were about to completely follow this recommendation we'd need to
// make all the components relying on BaseDialog to be aware of it.
// So instead we will use the whole content as the description.
// Description comes first and if the content contains more text,
// AT users can skip its presentation.
"aria-describedby": this.props.contentId,
};
if (this.props["aria-label"]) {
lockProps["aria-label"] = this.props["aria-label"];
} else {
lockProps["aria-labelledby"] = "mx_BaseDialog_title";
}
2017-01-24 15:54:05 +00:00
return (
2025-12-02 13:47:14 +00:00
// XXX: We can't import ModuleAPI here because it causes a dependency cycle - hack and
// use the copy on the window object :(
<I18nContext.Provider value={window.mxModuleApi.i18n}>
<MatrixClientContext.Provider value={this.matrixClient}>
{this.props.screenName && <PosthogScreenTracker screenName={this.props.screenName} />}
<FocusLock
returnFocus={true}
lockProps={lockProps}
className={classNames(this.props.className, {
mx_Dialog_fixedWidth: this.props.fixedWidth,
2019-12-17 17:26:12 +00:00
})}
>
2025-12-02 13:47:14 +00:00
{this.props.top}
<div
className={classNames("mx_Dialog_header", {
mx_Dialog_headerWithButton: !!this.props.headerButton,
})}
>
{!!(this.props.title || headerImage) && (
<Heading
size="3"
as="h1"
className={classNames("mx_Dialog_title", this.props.titleClass)}
id="mx_BaseDialog_title"
>
{headerImage}
{this.props.title}
</Heading>
)}
{this.props.headerButton}
</div>
{this.props.children}
{cancelButton}
</FocusLock>
</MatrixClientContext.Provider>
</I18nContext.Provider>
2017-01-24 15:54:05 +00:00
);
2020-08-29 12:14:16 +01:00
}
}