Improve spotlight accessibility by adding context menus (#8907)
* Extract room general context menu from roomtile * Create hook to access and change a room’s notification state * Extract room notification context menu from roomtile * Add room context menus to rooms in spotlight * Make arrow movement apply to the whole dialog, not just the input box
This commit is contained in:
parent
6a125d5a1d
commit
780a903e2f
12 changed files with 632 additions and 283 deletions
|
@ -37,7 +37,9 @@ export const Option: React.FC<OptionProps> = ({ inputRef, children, endAdornment
|
|||
role="option"
|
||||
>
|
||||
{ children }
|
||||
<kbd className="mx_SpotlightDialog_enterPrompt" aria-hidden>↵</kbd>
|
||||
{ endAdornment }
|
||||
<div className="mx_SpotlightDialog_option--endAdornment">
|
||||
<kbd className="mx_SpotlightDialog_enterPrompt" aria-hidden>↵</kbd>
|
||||
{ endAdornment }
|
||||
</div>
|
||||
</AccessibleButton>;
|
||||
};
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
Copyright 2021-2022 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 classNames from "classnames";
|
||||
import { Room } from "matrix-js-sdk/src/matrix";
|
||||
import React, { Fragment, useState } from "react";
|
||||
|
||||
import { ContextMenuTooltipButton } from "../../../../accessibility/context_menu/ContextMenuTooltipButton";
|
||||
import { useNotificationState } from "../../../../hooks/useRoomNotificationState";
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import { RoomNotifState } from "../../../../RoomNotifs";
|
||||
import { RoomGeneralContextMenu } from "../../context_menus/RoomGeneralContextMenu";
|
||||
import { RoomNotificationContextMenu } from "../../context_menus/RoomNotificationContextMenu";
|
||||
import SpaceContextMenu from "../../context_menus/SpaceContextMenu";
|
||||
import { ButtonEvent } from "../../elements/AccessibleButton";
|
||||
import { contextMenuBelow } from "../../rooms/RoomTile";
|
||||
|
||||
interface Props {
|
||||
room: Room;
|
||||
}
|
||||
|
||||
export function RoomResultContextMenus({ room }: Props) {
|
||||
const [notificationState] = useNotificationState(room);
|
||||
|
||||
const [generalMenuPosition, setGeneralMenuPosition] = useState<DOMRect | null>(null);
|
||||
const [notificationMenuPosition, setNotificationMenuPosition] = useState<DOMRect | null>(null);
|
||||
|
||||
let generalMenu: JSX.Element;
|
||||
if (generalMenuPosition !== null) {
|
||||
if (room.isSpaceRoom()) {
|
||||
generalMenu = <SpaceContextMenu
|
||||
{...contextMenuBelow(generalMenuPosition)}
|
||||
space={room}
|
||||
onFinished={() => setGeneralMenuPosition(null)}
|
||||
/>;
|
||||
} else {
|
||||
generalMenu = <RoomGeneralContextMenu
|
||||
{...contextMenuBelow(generalMenuPosition)}
|
||||
room={room}
|
||||
onFinished={() => setGeneralMenuPosition(null)}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
||||
let notificationMenu: JSX.Element;
|
||||
if (notificationMenuPosition !== null) {
|
||||
notificationMenu = <RoomNotificationContextMenu
|
||||
{...contextMenuBelow(notificationMenuPosition)}
|
||||
room={room}
|
||||
onFinished={() => setNotificationMenuPosition(null)}
|
||||
/>;
|
||||
}
|
||||
|
||||
const notificationMenuClasses = classNames("mx_SpotlightDialog_option--notifications", {
|
||||
// Show bell icon for the default case too.
|
||||
mx_RoomNotificationContextMenu_iconBell: notificationState === RoomNotifState.AllMessages,
|
||||
mx_RoomNotificationContextMenu_iconBellDot: notificationState === RoomNotifState.AllMessagesLoud,
|
||||
mx_RoomNotificationContextMenu_iconBellMentions: notificationState === RoomNotifState.MentionsOnly,
|
||||
mx_RoomNotificationContextMenu_iconBellCrossed: notificationState === RoomNotifState.Mute,
|
||||
});
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<ContextMenuTooltipButton
|
||||
className="mx_SpotlightDialog_option--menu"
|
||||
onClick={(ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
const target = ev.target as HTMLElement;
|
||||
setGeneralMenuPosition(target.getBoundingClientRect());
|
||||
}}
|
||||
title={room.isSpaceRoom() ? _t("Space options") : _t("Room options")}
|
||||
isExpanded={generalMenuPosition !== null}
|
||||
/>
|
||||
{ !room.isSpaceRoom() && (
|
||||
<ContextMenuTooltipButton
|
||||
className={notificationMenuClasses}
|
||||
onClick={(ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
const target = ev.target as HTMLElement;
|
||||
setNotificationMenuPosition(target.getBoundingClientRect());
|
||||
}}
|
||||
title={_t("Notification options")}
|
||||
isExpanded={notificationMenuPosition !== null}
|
||||
/>
|
||||
) }
|
||||
{ generalMenu }
|
||||
{ notificationMenu }
|
||||
</Fragment>
|
||||
);
|
||||
}
|
|
@ -88,6 +88,7 @@ import FeedbackDialog from "../FeedbackDialog";
|
|||
import { IDialogProps } from "../IDialogProps";
|
||||
import { Option } from "./Option";
|
||||
import { PublicRoomResultDetails } from "./PublicRoomResultDetails";
|
||||
import { RoomResultContextMenus } from "./RoomResultContextMenus";
|
||||
import { RoomContextDetails } from "../../rooms/RoomContextDetails";
|
||||
import { TooltipOption } from "./TooltipOption";
|
||||
|
||||
|
@ -506,8 +507,11 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
|
|||
let otherSearchesSection: JSX.Element;
|
||||
if (trimmedQuery || filter !== Filter.PublicRooms) {
|
||||
otherSearchesSection = (
|
||||
<div className="mx_SpotlightDialog_section mx_SpotlightDialog_otherSearches" role="group">
|
||||
<h4>
|
||||
<div
|
||||
className="mx_SpotlightDialog_section mx_SpotlightDialog_otherSearches"
|
||||
role="group"
|
||||
aria-labelledby="mx_SpotlightDialog_section_otherSearches">
|
||||
<h4 id="mx_SpotlightDialog_section_otherSearches">
|
||||
{ trimmedQuery
|
||||
? _t('Use "%(query)s" to search', { query })
|
||||
: _t("Search for") }
|
||||
|
@ -544,7 +548,7 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
|
|||
const unreadLabel = roomAriaUnreadLabel(result.room, notification);
|
||||
const ariaProperties = {
|
||||
"aria-label": unreadLabel ? `${result.room.name} ${unreadLabel}` : result.room.name,
|
||||
"aria-details": `mx_SpotlightDialog_button_result_${result.room.roomId}_details`,
|
||||
"aria-describedby": `mx_SpotlightDialog_button_result_${result.room.roomId}_details`,
|
||||
};
|
||||
return (
|
||||
<Option
|
||||
|
@ -553,6 +557,7 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
|
|||
onClick={(ev) => {
|
||||
viewRoom(result.room.roomId, true, ev?.type !== "click");
|
||||
}}
|
||||
endAdornment={<RoomResultContextMenus room={result.room} />}
|
||||
{...ariaProperties}
|
||||
>
|
||||
<DecoratedRoomAvatar
|
||||
|
@ -948,8 +953,10 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
|
|||
tabIndex={-1}
|
||||
aria-labelledby="mx_SpotlightDialog_section_recentSearches"
|
||||
>
|
||||
<h4 id="mx_SpotlightDialog_section_recentSearches">
|
||||
{ _t("Recent searches") }
|
||||
<h4>
|
||||
<span id="mx_SpotlightDialog_section_recentSearches">
|
||||
{ _t("Recent searches") }
|
||||
</span>
|
||||
<AccessibleButton kind="link" onClick={clearRecentSearches}>
|
||||
{ _t("Clear") }
|
||||
</AccessibleButton>
|
||||
|
@ -960,7 +967,7 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
|
|||
const unreadLabel = roomAriaUnreadLabel(room, notification);
|
||||
const ariaProperties = {
|
||||
"aria-label": unreadLabel ? `${room.name} ${unreadLabel}` : room.name,
|
||||
"aria-details": `mx_SpotlightDialog_button_recentSearch_${room.roomId}_details`,
|
||||
"aria-describedby": `mx_SpotlightDialog_button_recentSearch_${room.roomId}_details`,
|
||||
};
|
||||
return (
|
||||
<Option
|
||||
|
@ -969,6 +976,7 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
|
|||
onClick={(ev) => {
|
||||
viewRoom(room.roomId, true, ev?.type !== "click");
|
||||
}}
|
||||
endAdornment={<RoomResultContextMenus room={room} />}
|
||||
{...ariaProperties}
|
||||
>
|
||||
<DecoratedRoomAvatar
|
||||
|
@ -1034,6 +1042,7 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
|
|||
break;
|
||||
}
|
||||
|
||||
let ref: RefObject<HTMLElement>;
|
||||
const accessibilityAction = getKeyBindingsManager().getAccessibilityAction(ev);
|
||||
switch (accessibilityAction) {
|
||||
case KeyBindingAction.Escape:
|
||||
|
@ -1041,22 +1050,6 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
|
|||
ev.preventDefault();
|
||||
onFinished();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const onKeyDown = (ev: KeyboardEvent) => {
|
||||
let ref: RefObject<HTMLElement>;
|
||||
|
||||
const action = getKeyBindingsManager().getAccessibilityAction(ev);
|
||||
|
||||
switch (action) {
|
||||
case KeyBindingAction.Backspace:
|
||||
if (!query && filter !== null) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
setFilter(null);
|
||||
}
|
||||
break;
|
||||
case KeyBindingAction.ArrowUp:
|
||||
case KeyBindingAction.ArrowDown:
|
||||
ev.stopPropagation();
|
||||
|
@ -1075,7 +1068,7 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
|
|||
}
|
||||
|
||||
const idx = refs.indexOf(rovingContext.state.activeRef);
|
||||
ref = findSiblingElement(refs, idx + (action === KeyBindingAction.ArrowUp ? -1 : 1));
|
||||
ref = findSiblingElement(refs, idx + (accessibilityAction === KeyBindingAction.ArrowUp ? -1 : 1));
|
||||
}
|
||||
break;
|
||||
|
||||
|
@ -1092,14 +1085,9 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
|
|||
|
||||
const refs = rovingContext.state.refs.filter(refIsForRecentlyViewed);
|
||||
const idx = refs.indexOf(rovingContext.state.activeRef);
|
||||
ref = findSiblingElement(refs, idx + (action === KeyBindingAction.ArrowLeft ? -1 : 1));
|
||||
ref = findSiblingElement(refs, idx + (accessibilityAction === KeyBindingAction.ArrowLeft ? -1 : 1));
|
||||
}
|
||||
break;
|
||||
case KeyBindingAction.Enter:
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
rovingContext.state.activeRef?.current?.click();
|
||||
break;
|
||||
}
|
||||
|
||||
if (ref) {
|
||||
|
@ -1113,6 +1101,25 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
|
|||
}
|
||||
};
|
||||
|
||||
const onKeyDown = (ev: KeyboardEvent) => {
|
||||
const action = getKeyBindingsManager().getAccessibilityAction(ev);
|
||||
|
||||
switch (action) {
|
||||
case KeyBindingAction.Backspace:
|
||||
if (!query && filter !== null) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
setFilter(null);
|
||||
}
|
||||
break;
|
||||
case KeyBindingAction.Enter:
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
rovingContext.state.activeRef?.current?.click();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const openFeedback = SdkConfig.get().bug_report_endpoint_url ? () => {
|
||||
Modal.createDialog(FeedbackDialog, {
|
||||
feature: "spotlight",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue