Improve ThreadPanel ctx menu accessibility (#7217)

This commit is contained in:
Michael Telatynski 2021-11-29 17:42:53 +00:00 committed by GitHub
parent 9727a82a12
commit fe24c8ad2a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 35 additions and 48 deletions

View file

@ -27,7 +27,6 @@ limitations under the License.
width: 100%; width: 100%;
height: 100%; height: 100%;
opacity: 1.0; opacity: 1.0;
z-index: 5000;
} }
.mx_ContextualMenu { .mx_ContextualMenu {

View file

@ -120,7 +120,7 @@ limitations under the License.
&:hover { &:hover {
background-color: $event-selected-color; background-color: $event-selected-color;
} }
&[aria-selected="true"] { &[aria-checked="true"] {
:first-child { :first-child {
margin-left: -20px; margin-left: -20px;
} }

View file

@ -103,7 +103,7 @@ interface IState {
// 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.
@replaceableComponent("structures.ContextMenu") @replaceableComponent("structures.ContextMenu")
export class ContextMenu extends React.PureComponent<IProps, IState> { export default class ContextMenu extends React.PureComponent<IProps, IState> {
private readonly initialFocus: HTMLElement; private readonly initialFocus: HTMLElement;
static defaultProps = { static defaultProps = {
@ -411,6 +411,7 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
onClick={this.onClick} onClick={this.onClick}
onContextMenu={this.onContextMenuPreventBubbling} onContextMenu={this.onContextMenuPreventBubbling}
> >
{ background }
<div <div
className={menuClasses} className={menuClasses}
style={menuStyle} style={menuStyle}
@ -419,7 +420,6 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
> >
{ body } { body }
</div> </div>
{ background }
</div> </div>
); );
} }
@ -530,30 +530,22 @@ export const useContextMenu = <T extends any = HTMLElement>(): ContextMenuTuple<
return [isOpen, button, open, close, setIsOpen]; return [isOpen, button, open, close, setIsOpen];
}; };
@replaceableComponent("structures.LegacyContextMenu")
export default class LegacyContextMenu extends ContextMenu {
render() {
return this.renderMenu(false);
}
}
// XXX: Deprecated, used only for dynamic Tooltips. Avoid using at all costs. // XXX: Deprecated, used only for dynamic Tooltips. Avoid using at all costs.
export function createMenu(ElementClass, props) { export function createMenu(ElementClass, props) {
const onFinished = function(...args) { const onFinished = function(...args) {
ReactDOM.unmountComponentAtNode(getOrCreateContainer()); ReactDOM.unmountComponentAtNode(getOrCreateContainer());
props?.onFinished?.apply(null, args);
if (props && props.onFinished) {
props.onFinished.apply(null, args);
}
}; };
const menu = <LegacyContextMenu const menu = <ContextMenu
{...props} {...props}
mountAsChild={true}
hasBackground={false}
onFinished={onFinished} // eslint-disable-line react/jsx-no-bind onFinished={onFinished} // eslint-disable-line react/jsx-no-bind
windowResize={onFinished} // eslint-disable-line react/jsx-no-bind windowResize={onFinished} // eslint-disable-line react/jsx-no-bind
> >
<ElementClass {...props} onFinished={onFinished} /> <ElementClass {...props} onFinished={onFinished} />
</LegacyContextMenu>; </ContextMenu>;
ReactDOM.render(menu, getOrCreateContainer()); ReactDOM.render(menu, getOrCreateContainer());

View file

@ -24,12 +24,11 @@ import ResizeNotifier from '../../utils/ResizeNotifier';
import MatrixClientContext from '../../contexts/MatrixClientContext'; import MatrixClientContext from '../../contexts/MatrixClientContext';
import { _t } from '../../languageHandler'; import { _t } from '../../languageHandler';
import { ContextMenuButton } from '../../accessibility/context_menu/ContextMenuButton'; import { ContextMenuButton } from '../../accessibility/context_menu/ContextMenuButton';
import ContextMenu, { ChevronFace, useContextMenu } from './ContextMenu'; import ContextMenu, { ChevronFace, MenuItemRadio, useContextMenu } from './ContextMenu';
import RoomContext, { TimelineRenderingType } from '../../contexts/RoomContext'; import RoomContext, { TimelineRenderingType } from '../../contexts/RoomContext';
import TimelinePanel from './TimelinePanel'; import TimelinePanel from './TimelinePanel';
import { Layout } from '../../settings/enums/Layout'; import { Layout } from '../../settings/enums/Layout';
import { useEventEmitter } from '../../hooks/useEventEmitter'; import { useEventEmitter } from '../../hooks/useEventEmitter';
import AccessibleButton from '../views/elements/AccessibleButton';
import { TileShape } from '../views/rooms/EventTile'; import { TileShape } from '../views/rooms/EventTile';
import { RoomPermalinkCreator } from '../../utils/permalinks/Permalinks'; import { RoomPermalinkCreator } from '../../utils/permalinks/Permalinks';
@ -98,14 +97,14 @@ export const ThreadPanelHeaderFilterOptionItem = ({
onClick: () => void; onClick: () => void;
isSelected: boolean; isSelected: boolean;
}) => { }) => {
return <AccessibleButton return <MenuItemRadio
aria-selected={isSelected} active={isSelected}
className="mx_ThreadPanel_Header_FilterOptionItem" className="mx_ThreadPanel_Header_FilterOptionItem"
onClick={onClick} onClick={onClick}
> >
<span>{ label }</span> <span>{ label }</span>
<span>{ description }</span> <span>{ description }</span>
</AccessibleButton>; </MenuItemRadio>;
}; };
export const ThreadPanelHeader = ({ filterOption, setFilterOption }: { export const ThreadPanelHeader = ({ filterOption, setFilterOption }: {
@ -141,8 +140,8 @@ export const ThreadPanelHeader = ({ filterOption, setFilterOption }: {
top={0} top={0}
right={25} right={25}
onFinished={closeMenu} onFinished={closeMenu}
managed={false}
chevronFace={ChevronFace.Top} chevronFace={ChevronFace.Top}
mountAsChild={true}
> >
{ contextMenuOptions } { contextMenuOptions }
</ContextMenu> : null; </ContextMenu> : null;

View file

@ -21,7 +21,7 @@ import MemberAvatar from '../avatars/MemberAvatar';
import classNames from 'classnames'; import classNames from 'classnames';
import StatusMessageContextMenu from "../context_menus/StatusMessageContextMenu"; import StatusMessageContextMenu from "../context_menus/StatusMessageContextMenu";
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import { ChevronFace, ContextMenu, ContextMenuButton } from "../../structures/ContextMenu"; import ContextMenu, { ChevronFace, ContextMenuButton } from "../../structures/ContextMenu";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { ResizeMethod } from "matrix-js-sdk/src/@types/partials"; import { ResizeMethod } from "matrix-js-sdk/src/@types/partials";

View file

@ -17,7 +17,7 @@ limitations under the License.
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import { ContextMenu, IProps as IContextMenuProps, MenuItem } from '../../structures/ContextMenu'; import ContextMenu, { IProps as IContextMenuProps, MenuItem } from '../../structures/ContextMenu';
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call'; import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
import CallHandler from '../../../CallHandler'; import CallHandler from '../../../CallHandler';
import InviteDialog, { KIND_CALL_TRANSFER } from '../dialogs/InviteDialog'; import InviteDialog, { KIND_CALL_TRANSFER } from '../dialogs/InviteDialog';

View file

@ -17,7 +17,7 @@ limitations under the License.
import * as React from "react"; import * as React from "react";
import { createRef } from "react"; import { createRef } from "react";
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton"; import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
import { ContextMenu, IProps as IContextMenuProps } from '../../structures/ContextMenu'; import ContextMenu, { IProps as IContextMenuProps } from '../../structures/ContextMenu';
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call'; import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
import Field from "../elements/Field"; import Field from "../elements/Field";
import DialPad from '../voip/DialPad'; import DialPad from '../voip/DialPad';

View file

@ -17,9 +17,8 @@ limitations under the License.
import React from "react"; import React from "react";
import classNames from "classnames"; import classNames from "classnames";
import { import ContextMenu, {
ChevronFace, ChevronFace,
ContextMenu,
IProps as IContextMenuProps, IProps as IContextMenuProps,
MenuItem, MenuItem,
MenuItemCheckbox, MenuItemRadio, MenuItemCheckbox, MenuItemRadio,

View file

@ -21,9 +21,8 @@ import { IProtocol } from "matrix-js-sdk/src/client";
import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { instanceForInstanceId } from '../../../utils/DirectoryUtils'; import { instanceForInstanceId } from '../../../utils/DirectoryUtils';
import { import ContextMenu, {
ChevronFace, ChevronFace,
ContextMenu,
ContextMenuButton, ContextMenuButton,
MenuGroup, MenuGroup,
MenuItem, MenuItem,

View file

@ -18,7 +18,7 @@ limitations under the License.
import TagTile from './TagTile'; import TagTile from './TagTile';
import React from 'react'; import React from 'react';
import { ContextMenu, toRightOf, useContextMenu } from "../../structures/ContextMenu"; import ContextMenu, { toRightOf, useContextMenu } from "../../structures/ContextMenu";
import * as sdk from '../../../index'; import * as sdk from '../../../index';
export default function DNDTagTile(props) { export default function DNDTagTile(props) {

View file

@ -23,7 +23,7 @@ import dis from '../../../dispatcher/dispatcher';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import classNames from 'classnames'; import classNames from 'classnames';
import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { ContextMenu, ContextMenuButton, toRightOf } from "../../structures/ContextMenu"; import ContextMenu, { ContextMenuButton, toRightOf } from "../../structures/ContextMenu";
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex"; import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";

View file

@ -23,7 +23,7 @@ import type { Relations } from 'matrix-js-sdk/src/models/relations';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import dis from '../../../dispatcher/dispatcher'; import dis from '../../../dispatcher/dispatcher';
import { Action } from '../../../dispatcher/actions'; import { Action } from '../../../dispatcher/actions';
import { aboveLeftOf, ContextMenu, ContextMenuTooltipButton, useContextMenu } from '../../structures/ContextMenu'; import ContextMenu, { aboveLeftOf, ContextMenuTooltipButton, useContextMenu } from '../../structures/ContextMenu';
import { isContentActionable, canEditContent } from '../../../utils/EventUtils'; import { isContentActionable, canEditContent } from '../../../utils/EventUtils';
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext"; import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
import Toolbar from "../../../accessibility/Toolbar"; import Toolbar from "../../../accessibility/Toolbar";

View file

@ -24,7 +24,7 @@ import { _t } from '../../../languageHandler';
import { isContentActionable } from '../../../utils/EventUtils'; import { isContentActionable } from '../../../utils/EventUtils';
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { ContextMenuTooltipButton } from "../../../accessibility/context_menu/ContextMenuTooltipButton"; import { ContextMenuTooltipButton } from "../../../accessibility/context_menu/ContextMenuTooltipButton";
import { aboveLeftOf, ContextMenu, useContextMenu } from "../../structures/ContextMenu"; import ContextMenu, { aboveLeftOf, useContextMenu } from "../../structures/ContextMenu";
import ReactionPicker from "../emojipicker/ReactionPicker"; import ReactionPicker from "../emojipicker/ReactionPicker";
import ReactionsRowButton from "./ReactionsRowButton"; import ReactionsRowButton from "./ReactionsRowButton";
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";

View file

@ -27,9 +27,8 @@ import { makeRoomPermalink, RoomPermalinkCreator } from '../../../utils/permalin
import ContentMessages from '../../../ContentMessages'; import ContentMessages from '../../../ContentMessages';
import E2EIcon from './E2EIcon'; import E2EIcon from './E2EIcon';
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import { import ContextMenu, {
aboveLeftOf, aboveLeftOf,
ContextMenu,
useContextMenu, useContextMenu,
MenuItem, MenuItem,
AboveLeftOf, AboveLeftOf,

View file

@ -26,9 +26,8 @@ import { _t } from "../../../languageHandler";
import AccessibleButton from "../../views/elements/AccessibleButton"; import AccessibleButton from "../../views/elements/AccessibleButton";
import RoomTile from "./RoomTile"; import RoomTile from "./RoomTile";
import { ListLayout } from "../../../stores/room-list/ListLayout"; import { ListLayout } from "../../../stores/room-list/ListLayout";
import { import ContextMenu, {
ChevronFace, ChevronFace,
ContextMenu,
ContextMenuTooltipButton, ContextMenuTooltipButton,
StyledMenuItemCheckbox, StyledMenuItemCheckbox,
StyledMenuItemRadio, StyledMenuItemRadio,

View file

@ -13,6 +13,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import { Room } from 'matrix-js-sdk/src/models/room'; import { Room } from 'matrix-js-sdk/src/models/room';
import { _t, _td } from '../../../languageHandler'; import { _t, _td } from '../../../languageHandler';
@ -24,7 +25,7 @@ import WidgetUtils, { IWidgetEvent } from '../../../utils/WidgetUtils';
import PersistedElement from "../elements/PersistedElement"; import PersistedElement from "../elements/PersistedElement";
import { IntegrationManagers } from "../../../integrations/IntegrationManagers"; import { IntegrationManagers } from "../../../integrations/IntegrationManagers";
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import { ChevronFace, ContextMenu } from "../../structures/ContextMenu"; import ContextMenu, { ChevronFace } from "../../structures/ContextMenu";
import { WidgetType } from "../../../widgets/WidgetType"; import { WidgetType } from "../../../widgets/WidgetType";
import { Action } from "../../../dispatcher/actions"; import { Action } from "../../../dispatcher/actions";
import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingStore"; import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingStore";

View file

@ -18,7 +18,7 @@ import React, { useMemo } from "react";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import { alwaysAboveRightOf, ChevronFace, ContextMenu, useContextMenu } from "../../structures/ContextMenu"; import ContextMenu, { alwaysAboveRightOf, ChevronFace, useContextMenu } from "../../structures/ContextMenu";
import AccessibleButton from "../elements/AccessibleButton"; import AccessibleButton from "../elements/AccessibleButton";
import StyledCheckbox from "../elements/StyledCheckbox"; import StyledCheckbox from "../elements/StyledCheckbox";
import { MetaSpace } from "../../../stores/spaces"; import { MetaSpace } from "../../../stores/spaces";

View file

@ -22,7 +22,7 @@ import { HistoryVisibility, Preset } from "matrix-js-sdk/src/@types/partials";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import { ChevronFace, ContextMenu } from "../../structures/ContextMenu"; import ContextMenu, { ChevronFace } from "../../structures/ContextMenu";
import createRoom, { IOpts as ICreateOpts } from "../../../createRoom"; import createRoom, { IOpts as ICreateOpts } from "../../../createRoom";
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import SpaceBasicSettings, { SpaceAvatar } from "./SpaceBasicSettings"; import SpaceBasicSettings, { SpaceAvatar } from "./SpaceBasicSettings";

View file

@ -70,7 +70,7 @@ describe('ThreadPanel', () => {
wrapper.find(ContextMenuButton).simulate('click'); wrapper.find(ContextMenuButton).simulate('click');
const found = wrapper.find(ThreadPanelHeaderFilterOptionItem); const found = wrapper.find(ThreadPanelHeaderFilterOptionItem);
expect(found.length).toEqual(2); expect(found.length).toEqual(2);
const foundButton = found.find('[aria-selected=true]').first(); const foundButton = found.find('[aria-checked=true]').first();
expect(foundButton.text()).toEqual(`${_t("All threads")}${_t('Shows all threads from current room')}`); expect(foundButton.text()).toEqual(`${_t("All threads")}${_t('Shows all threads from current room')}`);
expect(foundButton).toMatchSnapshot(); expect(foundButton).toMatchSnapshot();
}); });

View file

@ -46,21 +46,21 @@ exports[`ThreadPanel Header expect that My filter for ThreadPanelHeader properly
exports[`ThreadPanel Header expect that ThreadPanelHeader has the correct option selected in the context menu 1`] = ` exports[`ThreadPanel Header expect that ThreadPanelHeader has the correct option selected in the context menu 1`] = `
<AccessibleButton <AccessibleButton
aria-selected={true} aria-checked={true}
className="mx_ThreadPanel_Header_FilterOptionItem" className="mx_ThreadPanel_Header_FilterOptionItem"
element="div" element="div"
onClick={[Function]} onClick={[Function]}
role="button" role="menuitemradio"
tabIndex={0} tabIndex={-1}
> >
<div <div
aria-selected={true} aria-checked={true}
className="mx_AccessibleButton mx_ThreadPanel_Header_FilterOptionItem" className="mx_AccessibleButton mx_ThreadPanel_Header_FilterOptionItem"
onClick={[Function]} onClick={[Function]}
onKeyDown={[Function]} onKeyDown={[Function]}
onKeyUp={[Function]} onKeyUp={[Function]}
role="button" role="menuitemradio"
tabIndex={0} tabIndex={-1}
> >
<span> <span>
All threads All threads