mirror of https://github.com/vector-im/riot-web
Merge pull request #4871 from matrix-org/t3chguy/room-list/3
Convert Context Menu to TypeScriptpull/21833/head
commit
d7ad555c12
|
@ -17,3 +17,4 @@ limitations under the License.
|
||||||
// Based on https://stackoverflow.com/a/53229857/3532235
|
// Based on https://stackoverflow.com/a/53229857/3532235
|
||||||
export type Without<T, U> = {[P in Exclude<keyof T, keyof U>] ? : never};
|
export type Without<T, U> = {[P in Exclude<keyof T, keyof U>] ? : never};
|
||||||
export type XOR<T, U> = (T | U) extends object ? (Without<T, U> & U) | (Without<U, T> & T) : T | U;
|
export type XOR<T, U> = (T | U) extends object ? (Without<T, U> & U) | (Without<U, T> & T) : T | U;
|
||||||
|
export type Writeable<T> = { -readonly [P in keyof T]: T[P] };
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2018 New Vector Ltd
|
||||||
|
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import AccessibleButton, {IProps as IAccessibleButtonProps} from "../../components/views/elements/AccessibleButton";
|
||||||
|
|
||||||
|
interface IProps extends IAccessibleButtonProps {
|
||||||
|
label?: string;
|
||||||
|
// whether or not the context menu is currently open
|
||||||
|
isExpanded: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Semantic component for representing the AccessibleButton which launches a <ContextMenu />
|
||||||
|
export const ContextMenuButton: React.FC<IProps> = ({
|
||||||
|
label,
|
||||||
|
isExpanded,
|
||||||
|
children,
|
||||||
|
onClick,
|
||||||
|
onContextMenu,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<AccessibleButton
|
||||||
|
{...props}
|
||||||
|
onClick={onClick}
|
||||||
|
onContextMenu={onContextMenu || onClick}
|
||||||
|
title={label}
|
||||||
|
aria-label={label}
|
||||||
|
aria-haspopup={true}
|
||||||
|
aria-expanded={isExpanded}
|
||||||
|
>
|
||||||
|
{ children }
|
||||||
|
</AccessibleButton>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,30 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2018 New Vector Ltd
|
||||||
|
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
interface IProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Semantic component for representing a role=group for grouping menu radios/checkboxes
|
||||||
|
export const MenuGroup: React.FC<IProps> = ({children, label, ...props}) => {
|
||||||
|
return <div {...props} role="group" aria-label={label}>
|
||||||
|
{ children }
|
||||||
|
</div>;
|
||||||
|
};
|
|
@ -0,0 +1,35 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2018 New Vector Ltd
|
||||||
|
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import AccessibleButton from "../../components/views/elements/AccessibleButton";
|
||||||
|
|
||||||
|
interface IProps extends React.ComponentProps<typeof AccessibleButton> {
|
||||||
|
label?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Semantic component for representing a role=menuitem
|
||||||
|
export const MenuItem: React.FC<IProps> = ({children, label, ...props}) => {
|
||||||
|
return (
|
||||||
|
<AccessibleButton {...props} role="menuitem" tabIndex={-1} aria-label={label}>
|
||||||
|
{ children }
|
||||||
|
</AccessibleButton>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2018 New Vector Ltd
|
||||||
|
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import AccessibleButton from "../../components/views/elements/AccessibleButton";
|
||||||
|
|
||||||
|
interface IProps extends React.ComponentProps<typeof AccessibleButton> {
|
||||||
|
label?: string;
|
||||||
|
active: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Semantic component for representing a role=menuitemcheckbox
|
||||||
|
export const MenuItemCheckbox: React.FC<IProps> = ({children, label, active, disabled, ...props}) => {
|
||||||
|
return (
|
||||||
|
<AccessibleButton
|
||||||
|
{...props}
|
||||||
|
role="menuitemcheckbox"
|
||||||
|
aria-checked={active}
|
||||||
|
aria-disabled={disabled}
|
||||||
|
disabled={disabled}
|
||||||
|
tabIndex={-1}
|
||||||
|
aria-label={label}
|
||||||
|
>
|
||||||
|
{ children }
|
||||||
|
</AccessibleButton>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2018 New Vector Ltd
|
||||||
|
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import AccessibleButton from "../../components/views/elements/AccessibleButton";
|
||||||
|
|
||||||
|
interface IProps extends React.ComponentProps<typeof AccessibleButton> {
|
||||||
|
label?: string;
|
||||||
|
active: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Semantic component for representing a role=menuitemradio
|
||||||
|
export const MenuItemRadio: React.FC<IProps> = ({children, label, active, disabled, ...props}) => {
|
||||||
|
return (
|
||||||
|
<AccessibleButton
|
||||||
|
{...props}
|
||||||
|
role="menuitemradio"
|
||||||
|
aria-checked={active}
|
||||||
|
aria-disabled={disabled}
|
||||||
|
disabled={disabled}
|
||||||
|
tabIndex={-1}
|
||||||
|
aria-label={label}
|
||||||
|
>
|
||||||
|
{ children }
|
||||||
|
</AccessibleButton>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,64 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2018 New Vector Ltd
|
||||||
|
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import {Key} from "../../Keyboard";
|
||||||
|
import StyledCheckbox from "../../components/views/elements/StyledCheckbox";
|
||||||
|
|
||||||
|
interface IProps extends React.ComponentProps<typeof StyledCheckbox> {
|
||||||
|
label?: string;
|
||||||
|
onChange(); // we handle keyup/down ourselves so lose the ChangeEvent
|
||||||
|
onClose(): void; // gets called after onChange on Key.ENTER
|
||||||
|
}
|
||||||
|
|
||||||
|
// Semantic component for representing a styled role=menuitemcheckbox
|
||||||
|
export const StyledMenuItemCheckbox: React.FC<IProps> = ({children, label, onChange, onClose, ...props}) => {
|
||||||
|
const onKeyDown = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === Key.ENTER || e.key === Key.SPACE) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
onChange();
|
||||||
|
// Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12
|
||||||
|
if (e.key === Key.ENTER) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const onKeyUp = (e: React.KeyboardEvent) => {
|
||||||
|
// prevent the input default handler as we handle it on keydown to match
|
||||||
|
// https://www.w3.org/TR/wai-aria-practices/examples/menubar/menubar-2/menubar-2.html
|
||||||
|
if (e.key === Key.SPACE || e.key === Key.ENTER) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<StyledCheckbox
|
||||||
|
{...props}
|
||||||
|
role="menuitemcheckbox"
|
||||||
|
tabIndex={-1}
|
||||||
|
aria-label={label}
|
||||||
|
onChange={onChange}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
onKeyUp={onKeyUp}
|
||||||
|
>
|
||||||
|
{ children }
|
||||||
|
</StyledCheckbox>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,64 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2018 New Vector Ltd
|
||||||
|
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import {Key} from "../../Keyboard";
|
||||||
|
import StyledRadioButton from "../../components/views/elements/StyledRadioButton";
|
||||||
|
|
||||||
|
interface IProps extends React.ComponentProps<typeof StyledRadioButton> {
|
||||||
|
label?: string;
|
||||||
|
onChange(); // we handle keyup/down ourselves so lose the ChangeEvent
|
||||||
|
onClose(): void; // gets called after onChange on Key.ENTER
|
||||||
|
}
|
||||||
|
|
||||||
|
// Semantic component for representing a styled role=menuitemradio
|
||||||
|
export const StyledMenuItemRadio: React.FC<IProps> = ({children, label, onChange, onClose, ...props}) => {
|
||||||
|
const onKeyDown = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === Key.ENTER || e.key === Key.SPACE) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
onChange();
|
||||||
|
// Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12
|
||||||
|
if (e.key === Key.ENTER) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const onKeyUp = (e: React.KeyboardEvent) => {
|
||||||
|
// prevent the input default handler as we handle it on keydown to match
|
||||||
|
// https://www.w3.org/TR/wai-aria-practices/examples/menubar/menubar-2/menubar-2.html
|
||||||
|
if (e.key === Key.SPACE || e.key === Key.ENTER) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<StyledRadioButton
|
||||||
|
{...props}
|
||||||
|
role="menuitemradio"
|
||||||
|
tabIndex={-1}
|
||||||
|
aria-label={label}
|
||||||
|
onChange={onChange}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
onKeyUp={onKeyUp}
|
||||||
|
>
|
||||||
|
{ children }
|
||||||
|
</StyledRadioButton>
|
||||||
|
);
|
||||||
|
};
|
|
@ -16,15 +16,12 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, {useRef, useState} from 'react';
|
import React, {CSSProperties, useRef, useState} from "react";
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from "react-dom";
|
||||||
import PropTypes from 'prop-types';
|
import classNames from "classnames";
|
||||||
import classNames from 'classnames';
|
|
||||||
import {Key} from "../../Keyboard";
|
import {Key} from "../../Keyboard";
|
||||||
import * as sdk from "../../index";
|
import {Writeable} from "../../@types/common";
|
||||||
import AccessibleButton from "../views/elements/AccessibleButton";
|
|
||||||
import StyledCheckbox from "../views/elements/StyledCheckbox";
|
|
||||||
import StyledRadioButton from "../views/elements/StyledRadioButton";
|
|
||||||
|
|
||||||
// Shamelessly ripped off Modal.js. There's probably a better way
|
// Shamelessly ripped off Modal.js. There's probably a better way
|
||||||
// of doing reusable widgets like dialog boxes & menus where we go and
|
// of doing reusable widgets like dialog boxes & menus where we go and
|
||||||
|
@ -32,8 +29,8 @@ import StyledRadioButton from "../views/elements/StyledRadioButton";
|
||||||
|
|
||||||
const ContextualMenuContainerId = "mx_ContextualMenu_Container";
|
const ContextualMenuContainerId = "mx_ContextualMenu_Container";
|
||||||
|
|
||||||
function getOrCreateContainer() {
|
function getOrCreateContainer(): HTMLDivElement {
|
||||||
let container = document.getElementById(ContextualMenuContainerId);
|
let container = document.getElementById(ContextualMenuContainerId) as HTMLDivElement;
|
||||||
|
|
||||||
if (!container) {
|
if (!container) {
|
||||||
container = document.createElement("div");
|
container = document.createElement("div");
|
||||||
|
@ -45,50 +42,70 @@ function getOrCreateContainer() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const ARIA_MENU_ITEM_ROLES = new Set(["menuitem", "menuitemcheckbox", "menuitemradio"]);
|
const ARIA_MENU_ITEM_ROLES = new Set(["menuitem", "menuitemcheckbox", "menuitemradio"]);
|
||||||
|
|
||||||
|
interface IPosition {
|
||||||
|
top?: number;
|
||||||
|
bottom?: number;
|
||||||
|
left?: number;
|
||||||
|
right?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ChevronFace {
|
||||||
|
Top = "top",
|
||||||
|
Bottom = "bottom",
|
||||||
|
Left = "left",
|
||||||
|
Right = "right",
|
||||||
|
None = "none",
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IProps extends IPosition {
|
||||||
|
menuWidth?: number;
|
||||||
|
menuHeight?: number;
|
||||||
|
|
||||||
|
chevronOffset?: number;
|
||||||
|
chevronFace?: ChevronFace;
|
||||||
|
|
||||||
|
menuPaddingTop?: number;
|
||||||
|
menuPaddingBottom?: number;
|
||||||
|
menuPaddingLeft?: number;
|
||||||
|
menuPaddingRight?: number;
|
||||||
|
|
||||||
|
zIndex?: number;
|
||||||
|
|
||||||
|
// If true, insert an invisible screen-sized element behind the menu that when clicked will close it.
|
||||||
|
hasBackground?: boolean;
|
||||||
|
// whether this context menu should be focus managed. If false it must handle itself
|
||||||
|
managed?: boolean;
|
||||||
|
|
||||||
|
// Function to be called on menu close
|
||||||
|
onFinished();
|
||||||
|
// on resize callback
|
||||||
|
windowResize?();
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
contextMenuElem: HTMLDivElement;
|
||||||
|
}
|
||||||
|
|
||||||
// Generic ContextMenu Portal wrapper
|
// Generic ContextMenu Portal wrapper
|
||||||
// all options inside the menu should be of role=menuitem/menuitemcheckbox/menuitemradiobutton and have tabIndex={-1}
|
// all options inside the menu should be of role=menuitem/menuitemcheckbox/menuitemradiobutton and have tabIndex={-1}
|
||||||
// this will allow the ContextMenu to manage its own focus using arrow keys as per the ARIA guidelines.
|
// this will allow the ContextMenu to manage its own focus using arrow keys as per the ARIA guidelines.
|
||||||
export class ContextMenu extends React.Component {
|
export class ContextMenu extends React.PureComponent<IProps, IState> {
|
||||||
static propTypes = {
|
private initialFocus: HTMLElement;
|
||||||
top: PropTypes.number,
|
|
||||||
bottom: PropTypes.number,
|
|
||||||
left: PropTypes.number,
|
|
||||||
right: PropTypes.number,
|
|
||||||
menuWidth: PropTypes.number,
|
|
||||||
menuHeight: PropTypes.number,
|
|
||||||
chevronOffset: PropTypes.number,
|
|
||||||
chevronFace: PropTypes.string, // top, bottom, left, right or none
|
|
||||||
// Function to be called on menu close
|
|
||||||
onFinished: PropTypes.func.isRequired,
|
|
||||||
menuPaddingTop: PropTypes.number,
|
|
||||||
menuPaddingRight: PropTypes.number,
|
|
||||||
menuPaddingBottom: PropTypes.number,
|
|
||||||
menuPaddingLeft: PropTypes.number,
|
|
||||||
zIndex: PropTypes.number,
|
|
||||||
|
|
||||||
// If true, insert an invisible screen-sized element behind the
|
|
||||||
// menu that when clicked will close it.
|
|
||||||
hasBackground: PropTypes.bool,
|
|
||||||
|
|
||||||
// on resize callback
|
|
||||||
windowResize: PropTypes.func,
|
|
||||||
|
|
||||||
managed: PropTypes.bool, // whether this context menu should be focus managed. If false it must handle itself
|
|
||||||
};
|
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
hasBackground: true,
|
hasBackground: true,
|
||||||
managed: true,
|
managed: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor() {
|
constructor(props, context) {
|
||||||
super();
|
super(props, context);
|
||||||
this.state = {
|
this.state = {
|
||||||
contextMenuElem: null,
|
contextMenuElem: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
// persist what had focus when we got initialized so we can return it after
|
// persist what had focus when we got initialized so we can return it after
|
||||||
this.initialFocus = document.activeElement;
|
this.initialFocus = document.activeElement as HTMLElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
@ -96,7 +113,7 @@ export class ContextMenu extends React.Component {
|
||||||
this.initialFocus.focus();
|
this.initialFocus.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
collectContextMenuRect = (element) => {
|
private collectContextMenuRect = (element) => {
|
||||||
// We don't need to clean up when unmounting, so ignore
|
// We don't need to clean up when unmounting, so ignore
|
||||||
if (!element) return;
|
if (!element) return;
|
||||||
|
|
||||||
|
@ -113,7 +130,7 @@ export class ContextMenu extends React.Component {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onContextMenu = (e) => {
|
private onContextMenu = (e) => {
|
||||||
if (this.props.onFinished) {
|
if (this.props.onFinished) {
|
||||||
this.props.onFinished();
|
this.props.onFinished();
|
||||||
|
|
||||||
|
@ -136,20 +153,20 @@ export class ContextMenu extends React.Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onContextMenuPreventBubbling = (e) => {
|
private onContextMenuPreventBubbling = (e) => {
|
||||||
// stop propagation so that any context menu handlers don't leak out of this context menu
|
// stop propagation so that any context menu handlers don't leak out of this context menu
|
||||||
// but do not inhibit the default browser menu
|
// but do not inhibit the default browser menu
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Prevent clicks on the background from going through to the component which opened the menu.
|
// Prevent clicks on the background from going through to the component which opened the menu.
|
||||||
_onFinished = (ev: InputEvent) => {
|
private onFinished = (ev: React.MouseEvent) => {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
if (this.props.onFinished) this.props.onFinished();
|
if (this.props.onFinished) this.props.onFinished();
|
||||||
};
|
};
|
||||||
|
|
||||||
_onMoveFocus = (element, up) => {
|
private onMoveFocus = (element: Element, up: boolean) => {
|
||||||
let descending = false; // are we currently descending or ascending through the DOM tree?
|
let descending = false; // are we currently descending or ascending through the DOM tree?
|
||||||
|
|
||||||
do {
|
do {
|
||||||
|
@ -183,25 +200,25 @@ export class ContextMenu extends React.Component {
|
||||||
} while (element && !ARIA_MENU_ITEM_ROLES.has(element.getAttribute("role")));
|
} while (element && !ARIA_MENU_ITEM_ROLES.has(element.getAttribute("role")));
|
||||||
|
|
||||||
if (element) {
|
if (element) {
|
||||||
element.focus();
|
(element as HTMLElement).focus();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_onMoveFocusHomeEnd = (element, up) => {
|
private onMoveFocusHomeEnd = (element: Element, up: boolean) => {
|
||||||
let results = element.querySelectorAll('[role^="menuitem"]');
|
let results = element.querySelectorAll('[role^="menuitem"]');
|
||||||
if (!results) {
|
if (!results) {
|
||||||
results = element.querySelectorAll('[tab-index]');
|
results = element.querySelectorAll('[tab-index]');
|
||||||
}
|
}
|
||||||
if (results && results.length) {
|
if (results && results.length) {
|
||||||
if (up) {
|
if (up) {
|
||||||
results[0].focus();
|
(results[0] as HTMLElement).focus();
|
||||||
} else {
|
} else {
|
||||||
results[results.length - 1].focus();
|
(results[results.length - 1] as HTMLElement).focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_onKeyDown = (ev) => {
|
private onKeyDown = (ev: React.KeyboardEvent) => {
|
||||||
if (!this.props.managed) {
|
if (!this.props.managed) {
|
||||||
if (ev.key === Key.ESCAPE) {
|
if (ev.key === Key.ESCAPE) {
|
||||||
this.props.onFinished();
|
this.props.onFinished();
|
||||||
|
@ -219,16 +236,16 @@ export class ContextMenu extends React.Component {
|
||||||
this.props.onFinished();
|
this.props.onFinished();
|
||||||
break;
|
break;
|
||||||
case Key.ARROW_UP:
|
case Key.ARROW_UP:
|
||||||
this._onMoveFocus(ev.target, true);
|
this.onMoveFocus(ev.target as Element, true);
|
||||||
break;
|
break;
|
||||||
case Key.ARROW_DOWN:
|
case Key.ARROW_DOWN:
|
||||||
this._onMoveFocus(ev.target, false);
|
this.onMoveFocus(ev.target as Element, false);
|
||||||
break;
|
break;
|
||||||
case Key.HOME:
|
case Key.HOME:
|
||||||
this._onMoveFocusHomeEnd(this.state.contextMenuElem, true);
|
this.onMoveFocusHomeEnd(this.state.contextMenuElem, true);
|
||||||
break;
|
break;
|
||||||
case Key.END:
|
case Key.END:
|
||||||
this._onMoveFocusHomeEnd(this.state.contextMenuElem, false);
|
this.onMoveFocusHomeEnd(this.state.contextMenuElem, false);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
handled = false;
|
handled = false;
|
||||||
|
@ -241,9 +258,8 @@ export class ContextMenu extends React.Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
renderMenu(hasBackground=this.props.hasBackground) {
|
protected renderMenu(hasBackground = this.props.hasBackground) {
|
||||||
const position = {};
|
const position: Partial<Writeable<DOMRect>> = {};
|
||||||
let chevronFace = null;
|
|
||||||
const props = this.props;
|
const props = this.props;
|
||||||
|
|
||||||
if (props.top) {
|
if (props.top) {
|
||||||
|
@ -252,23 +268,24 @@ export class ContextMenu extends React.Component {
|
||||||
position.bottom = props.bottom;
|
position.bottom = props.bottom;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let chevronFace: ChevronFace;
|
||||||
if (props.left) {
|
if (props.left) {
|
||||||
position.left = props.left;
|
position.left = props.left;
|
||||||
chevronFace = 'left';
|
chevronFace = ChevronFace.Left;
|
||||||
} else {
|
} else {
|
||||||
position.right = props.right;
|
position.right = props.right;
|
||||||
chevronFace = 'right';
|
chevronFace = ChevronFace.Right;
|
||||||
}
|
}
|
||||||
|
|
||||||
const contextMenuRect = this.state.contextMenuElem ? this.state.contextMenuElem.getBoundingClientRect() : null;
|
const contextMenuRect = this.state.contextMenuElem ? this.state.contextMenuElem.getBoundingClientRect() : null;
|
||||||
|
|
||||||
const chevronOffset = {};
|
const chevronOffset: CSSProperties = {};
|
||||||
if (props.chevronFace) {
|
if (props.chevronFace) {
|
||||||
chevronFace = props.chevronFace;
|
chevronFace = props.chevronFace;
|
||||||
}
|
}
|
||||||
const hasChevron = chevronFace && chevronFace !== "none";
|
const hasChevron = chevronFace && chevronFace !== ChevronFace.None;
|
||||||
|
|
||||||
if (chevronFace === 'top' || chevronFace === 'bottom') {
|
if (chevronFace === ChevronFace.Top || chevronFace === ChevronFace.Bottom) {
|
||||||
chevronOffset.left = props.chevronOffset;
|
chevronOffset.left = props.chevronOffset;
|
||||||
} else if (position.top !== undefined) {
|
} else if (position.top !== undefined) {
|
||||||
const target = position.top;
|
const target = position.top;
|
||||||
|
@ -298,13 +315,13 @@ export class ContextMenu extends React.Component {
|
||||||
'mx_ContextualMenu_right': !hasChevron && position.right,
|
'mx_ContextualMenu_right': !hasChevron && position.right,
|
||||||
'mx_ContextualMenu_top': !hasChevron && position.top,
|
'mx_ContextualMenu_top': !hasChevron && position.top,
|
||||||
'mx_ContextualMenu_bottom': !hasChevron && position.bottom,
|
'mx_ContextualMenu_bottom': !hasChevron && position.bottom,
|
||||||
'mx_ContextualMenu_withChevron_left': chevronFace === 'left',
|
'mx_ContextualMenu_withChevron_left': chevronFace === ChevronFace.Left,
|
||||||
'mx_ContextualMenu_withChevron_right': chevronFace === 'right',
|
'mx_ContextualMenu_withChevron_right': chevronFace === ChevronFace.Right,
|
||||||
'mx_ContextualMenu_withChevron_top': chevronFace === 'top',
|
'mx_ContextualMenu_withChevron_top': chevronFace === ChevronFace.Top,
|
||||||
'mx_ContextualMenu_withChevron_bottom': chevronFace === 'bottom',
|
'mx_ContextualMenu_withChevron_bottom': chevronFace === ChevronFace.Bottom,
|
||||||
});
|
});
|
||||||
|
|
||||||
const menuStyle = {};
|
const menuStyle: CSSProperties = {};
|
||||||
if (props.menuWidth) {
|
if (props.menuWidth) {
|
||||||
menuStyle.width = props.menuWidth;
|
menuStyle.width = props.menuWidth;
|
||||||
}
|
}
|
||||||
|
@ -335,13 +352,28 @@ export class ContextMenu extends React.Component {
|
||||||
let background;
|
let background;
|
||||||
if (hasBackground) {
|
if (hasBackground) {
|
||||||
background = (
|
background = (
|
||||||
<div className="mx_ContextualMenu_background" style={wrapperStyle} onClick={this._onFinished} onContextMenu={this.onContextMenu} />
|
<div
|
||||||
|
className="mx_ContextualMenu_background"
|
||||||
|
style={wrapperStyle}
|
||||||
|
onClick={this.onFinished}
|
||||||
|
onContextMenu={this.onContextMenu}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_ContextualMenu_wrapper" style={{...position, ...wrapperStyle}} onKeyDown={this._onKeyDown} onContextMenu={this.onContextMenuPreventBubbling}>
|
<div
|
||||||
<div className={menuClasses} style={menuStyle} ref={this.collectContextMenuRect} role={this.props.managed ? "menu" : undefined}>
|
className="mx_ContextualMenu_wrapper"
|
||||||
|
style={{...position, ...wrapperStyle}}
|
||||||
|
onKeyDown={this.onKeyDown}
|
||||||
|
onContextMenu={this.onContextMenuPreventBubbling}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={menuClasses}
|
||||||
|
style={menuStyle}
|
||||||
|
ref={this.collectContextMenuRect}
|
||||||
|
role={this.props.managed ? "menu" : undefined}
|
||||||
|
>
|
||||||
{ chevron }
|
{ chevron }
|
||||||
{ props.children }
|
{ props.children }
|
||||||
</div>
|
</div>
|
||||||
|
@ -350,195 +382,13 @@ export class ContextMenu extends React.Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render(): React.ReactChild {
|
||||||
return ReactDOM.createPortal(this.renderMenu(), getOrCreateContainer());
|
return ReactDOM.createPortal(this.renderMenu(), getOrCreateContainer());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Semantic component for representing the AccessibleButton which launches a <ContextMenu />
|
|
||||||
export const ContextMenuButton = ({ label, isExpanded, children, onClick, onContextMenu, ...props }) => {
|
|
||||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
|
||||||
return (
|
|
||||||
<AccessibleButton
|
|
||||||
{...props}
|
|
||||||
onClick={onClick}
|
|
||||||
onContextMenu={onContextMenu || onClick}
|
|
||||||
title={label}
|
|
||||||
aria-label={label}
|
|
||||||
aria-haspopup={true}
|
|
||||||
aria-expanded={isExpanded}
|
|
||||||
>
|
|
||||||
{ children }
|
|
||||||
</AccessibleButton>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
ContextMenuButton.propTypes = {
|
|
||||||
...AccessibleButton.propTypes,
|
|
||||||
label: PropTypes.string,
|
|
||||||
isExpanded: PropTypes.bool.isRequired, // whether or not the context menu is currently open
|
|
||||||
};
|
|
||||||
|
|
||||||
// Semantic component for representing a role=menuitem
|
|
||||||
export const MenuItem = ({children, label, ...props}) => {
|
|
||||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
|
||||||
return (
|
|
||||||
<AccessibleButton {...props} role="menuitem" tabIndex={-1} aria-label={label}>
|
|
||||||
{ children }
|
|
||||||
</AccessibleButton>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
MenuItem.propTypes = {
|
|
||||||
...AccessibleButton.propTypes,
|
|
||||||
label: PropTypes.string, // optional
|
|
||||||
className: PropTypes.string, // optional
|
|
||||||
onClick: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Semantic component for representing a role=group for grouping menu radios/checkboxes
|
|
||||||
export const MenuGroup = ({children, label, ...props}) => {
|
|
||||||
return <div {...props} role="group" aria-label={label}>
|
|
||||||
{ children }
|
|
||||||
</div>;
|
|
||||||
};
|
|
||||||
MenuGroup.propTypes = {
|
|
||||||
label: PropTypes.string.isRequired,
|
|
||||||
className: PropTypes.string, // optional
|
|
||||||
};
|
|
||||||
|
|
||||||
// Semantic component for representing a role=menuitemcheckbox
|
|
||||||
export const MenuItemCheckbox = ({children, label, active=false, disabled=false, ...props}) => {
|
|
||||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
|
||||||
return (
|
|
||||||
<AccessibleButton {...props} role="menuitemcheckbox" aria-checked={active} aria-disabled={disabled} tabIndex={-1} aria-label={label}>
|
|
||||||
{ children }
|
|
||||||
</AccessibleButton>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
MenuItemCheckbox.propTypes = {
|
|
||||||
...AccessibleButton.propTypes,
|
|
||||||
label: PropTypes.string, // optional
|
|
||||||
active: PropTypes.bool.isRequired,
|
|
||||||
disabled: PropTypes.bool, // optional
|
|
||||||
className: PropTypes.string, // optional
|
|
||||||
onClick: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Semantic component for representing a styled role=menuitemcheckbox
|
|
||||||
export const StyledMenuItemCheckbox = ({children, label, onChange, onClose, checked, disabled=false, ...props}) => {
|
|
||||||
const onKeyDown = (e) => {
|
|
||||||
if (e.key === Key.ENTER || e.key === Key.SPACE) {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
onChange();
|
|
||||||
// Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12
|
|
||||||
if (e.key === Key.ENTER) {
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const onKeyUp = (e) => {
|
|
||||||
// prevent the input default handler as we handle it on keydown to match
|
|
||||||
// https://www.w3.org/TR/wai-aria-practices/examples/menubar/menubar-2/menubar-2.html
|
|
||||||
if (e.key === Key.SPACE || e.key === Key.ENTER) {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<StyledCheckbox
|
|
||||||
{...props}
|
|
||||||
role="menuitemcheckbox"
|
|
||||||
aria-checked={checked}
|
|
||||||
checked={checked}
|
|
||||||
aria-disabled={disabled}
|
|
||||||
tabIndex={-1}
|
|
||||||
aria-label={label}
|
|
||||||
onChange={onChange}
|
|
||||||
onKeyDown={onKeyDown}
|
|
||||||
onKeyUp={onKeyUp}
|
|
||||||
>
|
|
||||||
{ children }
|
|
||||||
</StyledCheckbox>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
StyledMenuItemCheckbox.propTypes = {
|
|
||||||
...StyledCheckbox.propTypes,
|
|
||||||
label: PropTypes.string, // optional
|
|
||||||
checked: PropTypes.bool.isRequired,
|
|
||||||
disabled: PropTypes.bool, // optional
|
|
||||||
className: PropTypes.string, // optional
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
onClose: PropTypes.func.isRequired, // gets called after onChange on Key.ENTER
|
|
||||||
};
|
|
||||||
|
|
||||||
// Semantic component for representing a role=menuitemradio
|
|
||||||
export const MenuItemRadio = ({children, label, active=false, disabled=false, ...props}) => {
|
|
||||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
|
||||||
return (
|
|
||||||
<AccessibleButton {...props} role="menuitemradio" aria-checked={active} aria-disabled={disabled} tabIndex={-1} aria-label={label}>
|
|
||||||
{ children }
|
|
||||||
</AccessibleButton>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
MenuItemRadio.propTypes = {
|
|
||||||
...AccessibleButton.propTypes,
|
|
||||||
label: PropTypes.string, // optional
|
|
||||||
active: PropTypes.bool.isRequired,
|
|
||||||
disabled: PropTypes.bool, // optional
|
|
||||||
className: PropTypes.string, // optional
|
|
||||||
onClick: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Semantic component for representing a styled role=menuitemradio
|
|
||||||
export const StyledMenuItemRadio = ({children, label, onChange, onClose, checked=false, disabled=false, ...props}) => {
|
|
||||||
const onKeyDown = (e) => {
|
|
||||||
if (e.key === Key.ENTER || e.key === Key.SPACE) {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
onChange();
|
|
||||||
// Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12
|
|
||||||
if (e.key === Key.ENTER) {
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const onKeyUp = (e) => {
|
|
||||||
// prevent the input default handler as we handle it on keydown to match
|
|
||||||
// https://www.w3.org/TR/wai-aria-practices/examples/menubar/menubar-2/menubar-2.html
|
|
||||||
if (e.key === Key.SPACE || e.key === Key.ENTER) {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<StyledRadioButton
|
|
||||||
{...props}
|
|
||||||
role="menuitemradio"
|
|
||||||
aria-checked={checked}
|
|
||||||
checked={checked}
|
|
||||||
aria-disabled={disabled}
|
|
||||||
tabIndex={-1}
|
|
||||||
aria-label={label}
|
|
||||||
onChange={onChange}
|
|
||||||
onKeyDown={onKeyDown}
|
|
||||||
onKeyUp={onKeyUp}
|
|
||||||
>
|
|
||||||
{ children }
|
|
||||||
</StyledRadioButton>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
StyledMenuItemRadio.propTypes = {
|
|
||||||
...StyledMenuItemRadio.propTypes,
|
|
||||||
label: PropTypes.string, // optional
|
|
||||||
checked: PropTypes.bool.isRequired,
|
|
||||||
disabled: PropTypes.bool, // optional
|
|
||||||
className: PropTypes.string, // optional
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
onClose: PropTypes.func.isRequired, // gets called after onChange on Key.ENTER
|
|
||||||
};
|
|
||||||
|
|
||||||
// Placement method for <ContextMenu /> to position context menu to right of elementRect with chevronOffset
|
// Placement method for <ContextMenu /> to position context menu to right of elementRect with chevronOffset
|
||||||
export const toRightOf = (elementRect, chevronOffset=12) => {
|
export const toRightOf = (elementRect: DOMRect, chevronOffset = 12) => {
|
||||||
const left = elementRect.right + window.pageXOffset + 3;
|
const left = elementRect.right + window.pageXOffset + 3;
|
||||||
let top = elementRect.top + (elementRect.height / 2) + window.pageYOffset;
|
let top = elementRect.top + (elementRect.height / 2) + window.pageYOffset;
|
||||||
top -= chevronOffset + 8; // where 8 is half the height of the chevron
|
top -= chevronOffset + 8; // where 8 is half the height of the chevron
|
||||||
|
@ -546,8 +396,8 @@ export const toRightOf = (elementRect, chevronOffset=12) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Placement method for <ContextMenu /> to position context menu right-aligned and flowing to the left of elementRect
|
// Placement method for <ContextMenu /> to position context menu right-aligned and flowing to the left of elementRect
|
||||||
export const aboveLeftOf = (elementRect, chevronFace="none") => {
|
export const aboveLeftOf = (elementRect: DOMRect, chevronFace = ChevronFace.None) => {
|
||||||
const menuOptions = { chevronFace };
|
const menuOptions: IPosition & { chevronFace: ChevronFace } = { chevronFace };
|
||||||
|
|
||||||
const buttonRight = elementRect.right + window.pageXOffset;
|
const buttonRight = elementRect.right + window.pageXOffset;
|
||||||
const buttonBottom = elementRect.bottom + window.pageYOffset;
|
const buttonBottom = elementRect.bottom + window.pageYOffset;
|
||||||
|
@ -605,3 +455,12 @@ export function createMenu(ElementClass, props) {
|
||||||
|
|
||||||
return {close: onFinished};
|
return {close: onFinished};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// re-export the semantic helper components for simplicity
|
||||||
|
export {ContextMenuButton} from "../../accessibility/context_menu/ContextMenuButton";
|
||||||
|
export {MenuGroup} from "../../accessibility/context_menu/MenuGroup";
|
||||||
|
export {MenuItem} from "../../accessibility/context_menu/MenuItem";
|
||||||
|
export {MenuItemCheckbox} from "../../accessibility/context_menu/MenuItemCheckbox";
|
||||||
|
export {MenuItemRadio} from "../../accessibility/context_menu/MenuItemRadio";
|
||||||
|
export {StyledMenuItemCheckbox} from "../../accessibility/context_menu/StyledMenuItemCheckbox";
|
||||||
|
export {StyledMenuItemRadio} from "../../accessibility/context_menu/StyledMenuItemRadio";
|
|
@ -14,14 +14,13 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as React from "react";
|
import React, { createRef } from "react";
|
||||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||||
import { ActionPayload } from "../../dispatcher/payloads";
|
import { ActionPayload } from "../../dispatcher/payloads";
|
||||||
import { Action } from "../../dispatcher/actions";
|
import { Action } from "../../dispatcher/actions";
|
||||||
import { createRef } from "react";
|
|
||||||
import { _t } from "../../languageHandler";
|
import { _t } from "../../languageHandler";
|
||||||
import {ContextMenu, ContextMenuButton, MenuItem} from "./ContextMenu";
|
import { ChevronFace, ContextMenu, ContextMenuButton, MenuItem } from "./ContextMenu";
|
||||||
import {USER_NOTIFICATIONS_TAB, USER_SECURITY_TAB} from "../views/dialogs/UserSettingsDialog";
|
import {USER_NOTIFICATIONS_TAB, USER_SECURITY_TAB} from "../views/dialogs/UserSettingsDialog";
|
||||||
import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload";
|
import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload";
|
||||||
import RedesignFeedbackDialog from "../views/dialogs/RedesignFeedbackDialog";
|
import RedesignFeedbackDialog from "../views/dialogs/RedesignFeedbackDialog";
|
||||||
|
@ -122,7 +121,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private onOpenMenuClick = (ev: InputEvent) => {
|
private onOpenMenuClick = (ev: React.MouseEvent) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
const target = ev.target as HTMLButtonElement;
|
const target = ev.target as HTMLButtonElement;
|
||||||
|
@ -235,7 +234,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ContextMenu
|
<ContextMenu
|
||||||
chevronFace="none"
|
chevronFace={ChevronFace.None}
|
||||||
// -20 to overlap the context menu by just over the width of the `...` icon and make it look connected
|
// -20 to overlap the context menu by just over the width of the `...` icon and make it look connected
|
||||||
left={this.state.contextMenuPosition.width + this.state.contextMenuPosition.left - 20}
|
left={this.state.contextMenuPosition.width + this.state.contextMenuPosition.left - 20}
|
||||||
top={this.state.contextMenuPosition.top + this.state.contextMenuPosition.height}
|
top={this.state.contextMenuPosition.top + this.state.contextMenuPosition.height}
|
||||||
|
|
|
@ -64,7 +64,6 @@ export default function AccessibleButton({
|
||||||
className,
|
className,
|
||||||
...restProps
|
...restProps
|
||||||
}: IProps) {
|
}: IProps) {
|
||||||
|
|
||||||
const newProps: IAccessibleButtonProps = restProps;
|
const newProps: IAccessibleButtonProps = restProps;
|
||||||
if (!disabled) {
|
if (!disabled) {
|
||||||
newProps.onClick = onClick;
|
newProps.onClick = onClick;
|
||||||
|
|
|
@ -27,6 +27,7 @@ import RoomTile2 from "./RoomTile2";
|
||||||
import { ResizableBox, ResizeCallbackData } from "react-resizable";
|
import { ResizableBox, ResizeCallbackData } from "react-resizable";
|
||||||
import { ListLayout } from "../../../stores/room-list/ListLayout";
|
import { ListLayout } from "../../../stores/room-list/ListLayout";
|
||||||
import {
|
import {
|
||||||
|
ChevronFace,
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
ContextMenuButton,
|
ContextMenuButton,
|
||||||
StyledMenuItemCheckbox,
|
StyledMenuItemCheckbox,
|
||||||
|
@ -161,7 +162,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private onOpenMenuClick = (ev: InputEvent) => {
|
private onOpenMenuClick = (ev: React.MouseEvent) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
const target = ev.target as HTMLButtonElement;
|
const target = ev.target as HTMLButtonElement;
|
||||||
|
@ -365,7 +366,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
contextMenu = (
|
contextMenu = (
|
||||||
<ContextMenu
|
<ContextMenu
|
||||||
chevronFace="none"
|
chevronFace={ChevronFace.None}
|
||||||
left={this.state.contextMenuPosition.left}
|
left={this.state.contextMenuPosition.left}
|
||||||
top={this.state.contextMenuPosition.top + this.state.contextMenuPosition.height}
|
top={this.state.contextMenuPosition.top + this.state.contextMenuPosition.height}
|
||||||
onFinished={this.onCloseMenu}
|
onFinished={this.onCloseMenu}
|
||||||
|
|
|
@ -27,6 +27,7 @@ import { Key } from "../../../Keyboard";
|
||||||
import ActiveRoomObserver from "../../../ActiveRoomObserver";
|
import ActiveRoomObserver from "../../../ActiveRoomObserver";
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import {
|
import {
|
||||||
|
ChevronFace,
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
ContextMenuButton,
|
ContextMenuButton,
|
||||||
MenuItemRadio,
|
MenuItemRadio,
|
||||||
|
@ -87,7 +88,7 @@ const contextMenuBelow = (elementRect: PartialDOMRect) => {
|
||||||
// align the context menu's icons with the icon which opened the context menu
|
// align the context menu's icons with the icon which opened the context menu
|
||||||
const left = elementRect.left + window.pageXOffset - 9;
|
const left = elementRect.left + window.pageXOffset - 9;
|
||||||
const top = elementRect.bottom + window.pageYOffset + 17;
|
const top = elementRect.bottom + window.pageYOffset + 17;
|
||||||
const chevronFace = "none";
|
const chevronFace = ChevronFace.None;
|
||||||
return {left, top, chevronFace};
|
return {left, top, chevronFace};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -170,7 +171,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||||
this.setState({selected: isActive});
|
this.setState({selected: isActive});
|
||||||
};
|
};
|
||||||
|
|
||||||
private onNotificationsMenuOpenClick = (ev: InputEvent) => {
|
private onNotificationsMenuOpenClick = (ev: React.MouseEvent) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
const target = ev.target as HTMLButtonElement;
|
const target = ev.target as HTMLButtonElement;
|
||||||
|
@ -181,7 +182,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||||
this.setState({notificationsMenuPosition: null});
|
this.setState({notificationsMenuPosition: null});
|
||||||
};
|
};
|
||||||
|
|
||||||
private onGeneralMenuOpenClick = (ev: InputEvent) => {
|
private onGeneralMenuOpenClick = (ev: React.MouseEvent) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
const target = ev.target as HTMLButtonElement;
|
const target = ev.target as HTMLButtonElement;
|
||||||
|
|
Loading…
Reference in New Issue