Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 780a903

Browse files
authored
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
1 parent 6a125d5 commit 780a903

File tree

12 files changed

+632
-283
lines changed

12 files changed

+632
-283
lines changed

res/css/_components.scss

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@
8989
@import "./views/context_menus/_DeviceContextMenu.scss";
9090
@import "./views/context_menus/_IconizedContextMenu.scss";
9191
@import "./views/context_menus/_MessageContextMenu.scss";
92+
@import "./views/context_menus/_RoomGeneralContextMenu.scss";
93+
@import "./views/context_menus/_RoomNotificationContextMenu.scss";
9294
@import "./views/dialogs/_AddExistingToSpaceDialog.scss";
9395
@import "./views/dialogs/_AnalyticsLearnMoreDialog.scss";
9496
@import "./views/dialogs/_BugReportDialog.scss";
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
.mx_RoomGeneralContextMenu_iconStar::before {
2+
mask-image: url('$(res)/img/element-icons/roomlist/favorite.svg');
3+
}
4+
5+
.mx_RoomGeneralContextMenu_iconArrowDown::before {
6+
mask-image: url('$(res)/img/element-icons/roomlist/low-priority.svg');
7+
}
8+
9+
.mx_RoomGeneralContextMenu_iconNotificationsDefault::before {
10+
mask-image: url('$(res)/img/element-icons/notifications.svg');
11+
}
12+
13+
.mx_RoomGeneralContextMenu_iconNotificationsAllMessages::before {
14+
mask-image: url('$(res)/img/element-icons/roomlist/notifications-default.svg');
15+
}
16+
17+
.mx_RoomGeneralContextMenu_iconNotificationsMentionsKeywords::before {
18+
mask-image: url('$(res)/img/element-icons/roomlist/notifications-dm.svg');
19+
}
20+
21+
.mx_RoomGeneralContextMenu_iconNotificationsNone::before {
22+
mask-image: url('$(res)/img/element-icons/roomlist/notifications-off.svg');
23+
}
24+
25+
.mx_RoomGeneralContextMenu_iconPeople::before {
26+
mask-image: url('$(res)/img/element-icons/room/members.svg');
27+
}
28+
29+
.mx_RoomGeneralContextMenu_iconFiles::before {
30+
mask-image: url('$(res)/img/element-icons/room/files.svg');
31+
}
32+
33+
.mx_RoomGeneralContextMenu_iconPins::before {
34+
mask-image: url('$(res)/img/element-icons/room/pin-upright.svg');
35+
}
36+
37+
.mx_RoomGeneralContextMenu_iconWidgets::before {
38+
mask-image: url('$(res)/img/element-icons/room/apps.svg');
39+
}
40+
41+
.mx_RoomGeneralContextMenu_iconSettings::before {
42+
mask-image: url('$(res)/img/element-icons/settings.svg');
43+
}
44+
45+
.mx_RoomGeneralContextMenu_iconExport::before {
46+
mask-image: url('$(res)/img/element-icons/export.svg');
47+
}
48+
49+
.mx_RoomGeneralContextMenu_iconDeveloperTools::before {
50+
mask-image: url('$(res)/img/element-icons/settings/flask.svg');
51+
}
52+
53+
.mx_RoomGeneralContextMenu_iconCopyLink::before {
54+
mask-image: url('$(res)/img/element-icons/link.svg');
55+
}
56+
57+
.mx_RoomGeneralContextMenu_iconInvite::before {
58+
mask-image: url('$(res)/img/element-icons/room/invite.svg');
59+
}
60+
61+
.mx_RoomGeneralContextMenu_iconSignOut::before {
62+
mask-image: url('$(res)/img/element-icons/leave.svg');
63+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
.mx_RoomNotificationContextMenu_iconBell::before {
2+
mask-image: url('$(res)/img/element-icons/notifications.svg');
3+
}
4+
.mx_RoomNotificationContextMenu_iconBellDot::before {
5+
mask-image: url('$(res)/img/element-icons/roomlist/notifications-default.svg');
6+
}
7+
.mx_RoomNotificationContextMenu_iconBellMentions::before {
8+
mask-image: url('$(res)/img/element-icons/roomlist/notifications-dm.svg');
9+
}
10+
.mx_RoomNotificationContextMenu_iconBellCrossed::before {
11+
mask-image: url('$(res)/img/element-icons/roomlist/notifications-off.svg');
12+
}

res/css/views/dialogs/_SpotlightDialog.scss

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,13 @@ limitations under the License.
239239
text-overflow: ellipsis;
240240
overflow: hidden;
241241

242+
.mx_SpotlightDialog_option--endAdornment {
243+
display: inline-flex;
244+
flex-direction: row;
245+
margin-left: auto;
246+
align-items: start;
247+
}
248+
242249
&.mx_SpotlightDialog_result_multiline {
243250
align-items: start;
244251

@@ -309,8 +316,45 @@ limitations under the License.
309316
margin-left: $spacing-8;
310317
}
311318

319+
.mx_SpotlightDialog_option--menu,
320+
.mx_SpotlightDialog_option--notifications {
321+
width: 20px;
322+
min-width: 20px;
323+
height: 20px;
324+
margin-top: auto;
325+
margin-bottom: auto;
326+
position: relative;
327+
display: none;
328+
329+
&::before {
330+
top: 2px;
331+
left: 2px;
332+
content: '';
333+
width: 16px;
334+
height: 16px;
335+
position: absolute;
336+
mask-position: center;
337+
mask-size: contain;
338+
mask-repeat: no-repeat;
339+
background: $tertiary-content;
340+
}
341+
342+
&:hover::before, &[aria-selected=true]::before {
343+
background-color: $secondary-content;
344+
}
345+
}
346+
347+
.mx_SpotlightDialog_option--menu::before {
348+
mask-image: url('$(res)/img/element-icons/context-menu.svg');
349+
}
350+
312351
&:hover, &[aria-selected=true] {
313352
background-color: $system;
353+
354+
.mx_SpotlightDialog_option--menu,
355+
.mx_SpotlightDialog_option--notifications {
356+
display: block;
357+
}
314358
}
315359

316360
&[aria-selected=true] .mx_SpotlightDialog_enterPrompt {
@@ -436,7 +480,7 @@ limitations under the License.
436480
color: $tertiary-content;
437481
border-radius: 6px;
438482
background-color: $quinary-content;
439-
margin: 0 $spacing-4 0 auto;
483+
margin-right: $spacing-4;
440484
display: none;
441485
}
442486

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
/*
2+
Copyright 2021 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { logger } from "matrix-js-sdk/src/logger";
18+
import { Room } from "matrix-js-sdk/src/models/room";
19+
import React, { useContext } from "react";
20+
21+
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
22+
import RoomListActions from "../../../actions/RoomListActions";
23+
import MatrixClientContext from "../../../contexts/MatrixClientContext";
24+
import dis from "../../../dispatcher/dispatcher";
25+
import { useEventEmitterState } from "../../../hooks/useEventEmitter";
26+
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
27+
import { _t } from "../../../languageHandler";
28+
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
29+
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore";
30+
import DMRoomMap from "../../../utils/DMRoomMap";
31+
import { IProps as IContextMenuProps } from "../../structures/ContextMenu";
32+
import IconizedContextMenu, {
33+
IconizedContextMenuCheckbox,
34+
IconizedContextMenuOption,
35+
IconizedContextMenuOptionList,
36+
} from "../context_menus/IconizedContextMenu";
37+
import { ButtonEvent } from "../elements/AccessibleButton";
38+
39+
interface IProps extends IContextMenuProps {
40+
room: Room;
41+
onPostFavoriteClick?: (event: ButtonEvent) => void;
42+
onPostLowPriorityClick?: (event: ButtonEvent) => void;
43+
onPostInviteClick?: (event: ButtonEvent) => void;
44+
onPostCopyLinkClick?: (event: ButtonEvent) => void;
45+
onPostSettingsClick?: (event: ButtonEvent) => void;
46+
onPostForgetClick?: (event: ButtonEvent) => void;
47+
onPostLeaveClick?: (event: ButtonEvent) => void;
48+
}
49+
50+
export const RoomGeneralContextMenu = ({
51+
room, onFinished,
52+
onPostFavoriteClick, onPostLowPriorityClick, onPostInviteClick, onPostCopyLinkClick, onPostSettingsClick,
53+
onPostLeaveClick, onPostForgetClick, ...props
54+
}: IProps) => {
55+
const cli = useContext(MatrixClientContext);
56+
const roomTags = useEventEmitterState(
57+
RoomListStore.instance,
58+
LISTS_UPDATE_EVENT,
59+
() => RoomListStore.instance.getTagsForRoom(room),
60+
);
61+
const isDm = DMRoomMap.shared().getUserIdForRoomId(room.roomId);
62+
const wrapHandler = (
63+
handler: (ev: ButtonEvent) => void,
64+
postHandler?: (ev: ButtonEvent) => void,
65+
persistent = false,
66+
): (ev: ButtonEvent) => void => {
67+
return (ev: ButtonEvent) => {
68+
ev.preventDefault();
69+
ev.stopPropagation();
70+
71+
handler(ev);
72+
73+
const action = getKeyBindingsManager().getAccessibilityAction(ev as React.KeyboardEvent);
74+
if (!persistent || action === KeyBindingAction.Enter) {
75+
onFinished();
76+
}
77+
postHandler?.(ev);
78+
};
79+
};
80+
81+
const onTagRoom = (ev: ButtonEvent, tagId: TagID) => {
82+
if (tagId === DefaultTagID.Favourite || tagId === DefaultTagID.LowPriority) {
83+
const inverseTag = tagId === DefaultTagID.Favourite ? DefaultTagID.LowPriority : DefaultTagID.Favourite;
84+
const isApplied = RoomListStore.instance.getTagsForRoom(room).includes(tagId);
85+
const removeTag = isApplied ? tagId : inverseTag;
86+
const addTag = isApplied ? null : tagId;
87+
dis.dispatch(RoomListActions.tagRoom(cli, room, removeTag, addTag, undefined, 0));
88+
} else {
89+
logger.warn(`Unexpected tag ${tagId} applied to ${room.roomId}`);
90+
}
91+
};
92+
93+
const isFavorite = roomTags.includes(DefaultTagID.Favourite);
94+
const favoriteOption: JSX.Element = <IconizedContextMenuCheckbox
95+
onClick={wrapHandler((ev) =>
96+
onTagRoom(ev, DefaultTagID.Favourite), onPostFavoriteClick, true)}
97+
active={isFavorite}
98+
label={isFavorite ? _t("Favourited") : _t("Favourite")}
99+
iconClassName="mx_RoomGeneralContextMenu_iconStar"
100+
/>;
101+
102+
const isLowPriority = roomTags.includes(DefaultTagID.LowPriority);
103+
const lowPriorityOption: JSX.Element = <IconizedContextMenuCheckbox
104+
onClick={wrapHandler((ev) =>
105+
onTagRoom(ev, DefaultTagID.LowPriority), onPostLowPriorityClick, true)}
106+
active={isLowPriority}
107+
label={_t("Low Priority")}
108+
iconClassName="mx_RoomGeneralContextMenu_iconArrowDown"
109+
/>;
110+
111+
let inviteOption: JSX.Element;
112+
if (room.canInvite(cli.getUserId()) && !isDm) {
113+
inviteOption = <IconizedContextMenuOption
114+
onClick={wrapHandler(() => dis.dispatch({
115+
action: "view_invite",
116+
roomId: room.roomId,
117+
}), onPostInviteClick)}
118+
label={_t("Invite")}
119+
iconClassName="mx_RoomGeneralContextMenu_iconInvite"
120+
/>;
121+
}
122+
123+
let copyLinkOption: JSX.Element;
124+
if (!isDm) {
125+
copyLinkOption = <IconizedContextMenuOption
126+
onClick={wrapHandler(() => dis.dispatch({
127+
action: "copy_room",
128+
room_id: room.roomId,
129+
}), onPostCopyLinkClick)}
130+
label={_t("Copy room link")}
131+
iconClassName="mx_RoomGeneralContextMenu_iconCopyLink"
132+
/>;
133+
}
134+
135+
const settingsOption: JSX.Element = <IconizedContextMenuOption
136+
onClick={wrapHandler(() => dis.dispatch({
137+
action: "open_room_settings",
138+
room_id: room.roomId,
139+
}), onPostSettingsClick)}
140+
label={_t("Settings")}
141+
iconClassName="mx_RoomGeneralContextMenu_iconSettings"
142+
/>;
143+
144+
let leaveOption: JSX.Element;
145+
if (roomTags.includes(DefaultTagID.Archived)) {
146+
leaveOption = <IconizedContextMenuOption
147+
iconClassName="mx_RoomGeneralContextMenu_iconSignOut"
148+
label={_t("Forget Room")}
149+
className="mx_IconizedContextMenu_option_red"
150+
onClick={wrapHandler(() => dis.dispatch({
151+
action: "forget_room",
152+
room_id: room.roomId,
153+
}), onPostForgetClick)}
154+
/>;
155+
} else {
156+
leaveOption = <IconizedContextMenuOption
157+
onClick={wrapHandler(() => dis.dispatch({
158+
action: "leave_room",
159+
room_id: room.roomId,
160+
}), onPostLeaveClick)}
161+
label={_t("Leave")}
162+
className="mx_IconizedContextMenu_option_red"
163+
iconClassName="mx_RoomGeneralContextMenu_iconSignOut"
164+
/>;
165+
}
166+
167+
return <IconizedContextMenu
168+
{...props}
169+
onFinished={onFinished}
170+
className="mx_RoomGeneralContextMenu"
171+
compact
172+
>
173+
{ !roomTags.includes(DefaultTagID.Archived) && (
174+
<IconizedContextMenuOptionList>
175+
{ favoriteOption }
176+
{ lowPriorityOption }
177+
{ inviteOption }
178+
{ copyLinkOption }
179+
{ settingsOption }
180+
</IconizedContextMenuOptionList>
181+
) }
182+
<IconizedContextMenuOptionList red>
183+
{ leaveOption }
184+
</IconizedContextMenuOptionList>
185+
</IconizedContextMenu>;
186+
};

0 commit comments

Comments
 (0)