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
|
@ -18,7 +18,6 @@ limitations under the License.
|
|||
import React, { createRef } from "react";
|
||||
import { Room, RoomEvent } from "matrix-js-sdk/src/models/room";
|
||||
import classNames from "classnames";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
|
||||
import AccessibleButton, { ButtonEvent } from "../../views/elements/AccessibleButton";
|
||||
|
@ -32,9 +31,8 @@ import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewSto
|
|||
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
||||
import { RoomNotifState } from "../../../RoomNotifs";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { RoomNotificationContextMenu } from "../context_menus/RoomNotificationContextMenu";
|
||||
import NotificationBadge from "./NotificationBadge";
|
||||
import RoomListStore from "../../../stores/room-list/RoomListStore";
|
||||
import RoomListActions from "../../../actions/RoomListActions";
|
||||
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
|
||||
import { NotificationState, NotificationStateEvents } from "../../../stores/notifications/NotificationState";
|
||||
|
@ -42,18 +40,13 @@ import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
|||
import { EchoChamber } from "../../../stores/local-echo/EchoChamber";
|
||||
import { CachedRoomKey, RoomEchoChamber } from "../../../stores/local-echo/RoomEchoChamber";
|
||||
import { PROPERTY_UPDATED } from "../../../stores/local-echo/GenericEchoChamber";
|
||||
import IconizedContextMenu, {
|
||||
IconizedContextMenuCheckbox,
|
||||
IconizedContextMenuOption,
|
||||
IconizedContextMenuOptionList,
|
||||
IconizedContextMenuRadio,
|
||||
} from "../context_menus/IconizedContextMenu";
|
||||
import PosthogTrackers from "../../../PosthogTrackers";
|
||||
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
|
||||
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
|
||||
import { RoomViewStore } from "../../../stores/RoomViewStore";
|
||||
import VideoRoomSummary from "./VideoRoomSummary";
|
||||
import { RoomGeneralContextMenu } from "../context_menus/RoomGeneralContextMenu";
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
|
@ -267,118 +260,6 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
this.setState({ generalMenuPosition: null });
|
||||
};
|
||||
|
||||
private onTagRoom = (ev: ButtonEvent, tagId: TagID) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
if (tagId === DefaultTagID.Favourite || tagId === DefaultTagID.LowPriority) {
|
||||
const inverseTag = tagId === DefaultTagID.Favourite ? DefaultTagID.LowPriority : DefaultTagID.Favourite;
|
||||
const isApplied = RoomListStore.instance.getTagsForRoom(this.props.room).includes(tagId);
|
||||
const removeTag = isApplied ? tagId : inverseTag;
|
||||
const addTag = isApplied ? null : tagId;
|
||||
defaultDispatcher.dispatch(RoomListActions.tagRoom(
|
||||
MatrixClientPeg.get(),
|
||||
this.props.room,
|
||||
removeTag,
|
||||
addTag,
|
||||
undefined,
|
||||
0,
|
||||
));
|
||||
} else {
|
||||
logger.warn(`Unexpected tag ${tagId} applied to ${this.props.room.roomId}`);
|
||||
}
|
||||
|
||||
const action = getKeyBindingsManager().getAccessibilityAction(ev as React.KeyboardEvent);
|
||||
switch (action) {
|
||||
case KeyBindingAction.Enter:
|
||||
// Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12
|
||||
this.setState({ generalMenuPosition: null }); // hide the menu
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
private onLeaveRoomClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
defaultDispatcher.dispatch({
|
||||
action: 'leave_room',
|
||||
room_id: this.props.room.roomId,
|
||||
});
|
||||
this.setState({ generalMenuPosition: null }); // hide the menu
|
||||
|
||||
PosthogTrackers.trackInteraction("WebRoomListRoomTileContextMenuLeaveItem", ev);
|
||||
};
|
||||
|
||||
private onForgetRoomClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
defaultDispatcher.dispatch({
|
||||
action: 'forget_room',
|
||||
room_id: this.props.room.roomId,
|
||||
});
|
||||
this.setState({ generalMenuPosition: null }); // hide the menu
|
||||
};
|
||||
|
||||
private onOpenRoomSettings = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
defaultDispatcher.dispatch({
|
||||
action: 'open_room_settings',
|
||||
room_id: this.props.room.roomId,
|
||||
});
|
||||
this.setState({ generalMenuPosition: null }); // hide the menu
|
||||
|
||||
PosthogTrackers.trackInteraction("WebRoomListRoomTileContextMenuSettingsItem", ev);
|
||||
};
|
||||
|
||||
private onCopyRoomClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
defaultDispatcher.dispatch({
|
||||
action: 'copy_room',
|
||||
room_id: this.props.room.roomId,
|
||||
});
|
||||
this.setState({ generalMenuPosition: null }); // hide the menu
|
||||
};
|
||||
|
||||
private onInviteClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
defaultDispatcher.dispatch({
|
||||
action: 'view_invite',
|
||||
roomId: this.props.room.roomId,
|
||||
});
|
||||
this.setState({ generalMenuPosition: null }); // hide the menu
|
||||
|
||||
PosthogTrackers.trackInteraction("WebRoomListRoomTileContextMenuInviteItem", ev);
|
||||
};
|
||||
|
||||
private async saveNotifState(ev: ButtonEvent, newState: RoomNotifState) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
if (MatrixClientPeg.get().isGuest()) return;
|
||||
|
||||
this.roomProps.notificationVolume = newState;
|
||||
|
||||
const action = getKeyBindingsManager().getAccessibilityAction(ev as React.KeyboardEvent);
|
||||
switch (action) {
|
||||
case KeyBindingAction.Enter:
|
||||
// Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12
|
||||
this.setState({ notificationsMenuPosition: null }); // hide the menu
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private onClickAllNotifs = ev => this.saveNotifState(ev, RoomNotifState.AllMessages);
|
||||
private onClickAlertMe = ev => this.saveNotifState(ev, RoomNotifState.AllMessagesLoud);
|
||||
private onClickMentions = ev => this.saveNotifState(ev, RoomNotifState.MentionsOnly);
|
||||
private onClickMute = ev => this.saveNotifState(ev, RoomNotifState.Mute);
|
||||
|
||||
private renderNotificationsMenu(isActive: boolean): React.ReactElement {
|
||||
if (MatrixClientPeg.get().isGuest() || this.props.tag === DefaultTagID.Archived ||
|
||||
!this.showContextMenu || this.props.isMinimized
|
||||
|
@ -389,49 +270,12 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
|
||||
const state = this.roomProps.notificationVolume;
|
||||
|
||||
let contextMenu = null;
|
||||
if (this.state.notificationsMenuPosition) {
|
||||
contextMenu = <IconizedContextMenu
|
||||
{...contextMenuBelow(this.state.notificationsMenuPosition)}
|
||||
onFinished={this.onCloseNotificationsMenu}
|
||||
className="mx_RoomTile_contextMenu"
|
||||
compact
|
||||
>
|
||||
<IconizedContextMenuOptionList first>
|
||||
<IconizedContextMenuRadio
|
||||
label={_t("Use default")}
|
||||
active={state === RoomNotifState.AllMessages}
|
||||
iconClassName="mx_RoomTile_iconBell"
|
||||
onClick={this.onClickAllNotifs}
|
||||
/>
|
||||
<IconizedContextMenuRadio
|
||||
label={_t("All messages")}
|
||||
active={state === RoomNotifState.AllMessagesLoud}
|
||||
iconClassName="mx_RoomTile_iconBellDot"
|
||||
onClick={this.onClickAlertMe}
|
||||
/>
|
||||
<IconizedContextMenuRadio
|
||||
label={_t("Mentions & Keywords")}
|
||||
active={state === RoomNotifState.MentionsOnly}
|
||||
iconClassName="mx_RoomTile_iconBellMentions"
|
||||
onClick={this.onClickMentions}
|
||||
/>
|
||||
<IconizedContextMenuRadio
|
||||
label={_t("None")}
|
||||
active={state === RoomNotifState.Mute}
|
||||
iconClassName="mx_RoomTile_iconBellCrossed"
|
||||
onClick={this.onClickMute}
|
||||
/>
|
||||
</IconizedContextMenuOptionList>
|
||||
</IconizedContextMenu>;
|
||||
}
|
||||
|
||||
const classes = classNames("mx_RoomTile_notificationsButton", {
|
||||
// Show bell icon for the default case too.
|
||||
mx_RoomTile_iconBell: state === RoomNotifState.AllMessages,
|
||||
mx_RoomTile_iconBellDot: state === RoomNotifState.AllMessagesLoud,
|
||||
mx_RoomTile_iconBellMentions: state === RoomNotifState.MentionsOnly,
|
||||
mx_RoomTile_iconBellCrossed: state === RoomNotifState.Mute,
|
||||
mx_RoomNotificationContextMenu_iconBell: state === RoomNotifState.AllMessages,
|
||||
mx_RoomNotificationContextMenu_iconBellDot: state === RoomNotifState.AllMessagesLoud,
|
||||
mx_RoomNotificationContextMenu_iconBellMentions: state === RoomNotifState.MentionsOnly,
|
||||
mx_RoomNotificationContextMenu_iconBellCrossed: state === RoomNotifState.Mute,
|
||||
|
||||
// Only show the icon by default if the room is overridden to muted.
|
||||
// TODO: [FTUE Notifications] Probably need to detect global mute state
|
||||
|
@ -447,93 +291,19 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
isExpanded={!!this.state.notificationsMenuPosition}
|
||||
tabIndex={isActive ? 0 : -1}
|
||||
/>
|
||||
{ contextMenu }
|
||||
{ this.state.notificationsMenuPosition && (
|
||||
<RoomNotificationContextMenu
|
||||
{...contextMenuBelow(this.state.notificationsMenuPosition)}
|
||||
onFinished={this.onCloseNotificationsMenu}
|
||||
room={this.props.room}
|
||||
/>
|
||||
) }
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
private renderGeneralMenu(): React.ReactElement {
|
||||
if (!this.showContextMenu) return null; // no menu to show
|
||||
|
||||
let contextMenu = null;
|
||||
if (this.state.generalMenuPosition && this.props.tag === DefaultTagID.Archived) {
|
||||
contextMenu = <IconizedContextMenu
|
||||
{...contextMenuBelow(this.state.generalMenuPosition)}
|
||||
onFinished={this.onCloseGeneralMenu}
|
||||
className="mx_RoomTile_contextMenu"
|
||||
compact
|
||||
>
|
||||
<IconizedContextMenuOptionList red>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_RoomTile_iconSignOut"
|
||||
label={_t("Forget Room")}
|
||||
onClick={this.onForgetRoomClick}
|
||||
/>
|
||||
</IconizedContextMenuOptionList>
|
||||
</IconizedContextMenu>;
|
||||
} else if (this.state.generalMenuPosition) {
|
||||
const roomTags = RoomListStore.instance.getTagsForRoom(this.props.room);
|
||||
|
||||
const isFavorite = roomTags.includes(DefaultTagID.Favourite);
|
||||
const favouriteLabel = isFavorite ? _t("Favourited") : _t("Favourite");
|
||||
|
||||
const isLowPriority = roomTags.includes(DefaultTagID.LowPriority);
|
||||
const lowPriorityLabel = _t("Low Priority");
|
||||
|
||||
const isDm = roomTags.includes(DefaultTagID.DM);
|
||||
|
||||
const userId = MatrixClientPeg.get().getUserId();
|
||||
const canInvite = this.props.room.canInvite(userId) && !isDm; // hide invite in DMs from this quick menu
|
||||
contextMenu = <IconizedContextMenu
|
||||
{...contextMenuBelow(this.state.generalMenuPosition)}
|
||||
onFinished={this.onCloseGeneralMenu}
|
||||
className="mx_RoomTile_contextMenu"
|
||||
compact
|
||||
>
|
||||
<IconizedContextMenuOptionList>
|
||||
<IconizedContextMenuCheckbox
|
||||
onClick={(e) => {
|
||||
this.onTagRoom(e, DefaultTagID.Favourite);
|
||||
PosthogTrackers.trackInteraction("WebRoomListRoomTileContextMenuFavouriteToggle", e);
|
||||
}}
|
||||
active={isFavorite}
|
||||
label={favouriteLabel}
|
||||
iconClassName="mx_RoomTile_iconStar"
|
||||
/>
|
||||
<IconizedContextMenuCheckbox
|
||||
onClick={(e) => this.onTagRoom(e, DefaultTagID.LowPriority)}
|
||||
active={isLowPriority}
|
||||
label={lowPriorityLabel}
|
||||
iconClassName="mx_RoomTile_iconArrowDown"
|
||||
/>
|
||||
{ canInvite ? (
|
||||
<IconizedContextMenuOption
|
||||
onClick={this.onInviteClick}
|
||||
label={_t("Invite")}
|
||||
iconClassName="mx_RoomTile_iconInvite"
|
||||
/>
|
||||
) : null }
|
||||
{ !isDm ? <IconizedContextMenuOption
|
||||
onClick={this.onCopyRoomClick}
|
||||
label={_t("Copy room link")}
|
||||
iconClassName="mx_RoomTile_iconCopyLink"
|
||||
/> : null }
|
||||
<IconizedContextMenuOption
|
||||
onClick={this.onOpenRoomSettings}
|
||||
label={_t("Settings")}
|
||||
iconClassName="mx_RoomTile_iconSettings"
|
||||
/>
|
||||
</IconizedContextMenuOptionList>
|
||||
<IconizedContextMenuOptionList red>
|
||||
<IconizedContextMenuOption
|
||||
onClick={this.onLeaveRoomClick}
|
||||
label={_t("Leave")}
|
||||
iconClassName="mx_RoomTile_iconSignOut"
|
||||
/>
|
||||
</IconizedContextMenuOptionList>
|
||||
</IconizedContextMenu>;
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<ContextMenuTooltipButton
|
||||
|
@ -542,7 +312,25 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
title={_t("Room options")}
|
||||
isExpanded={!!this.state.generalMenuPosition}
|
||||
/>
|
||||
{ contextMenu }
|
||||
{ this.state.generalMenuPosition && (
|
||||
<RoomGeneralContextMenu
|
||||
{...contextMenuBelow(this.state.generalMenuPosition)}
|
||||
onFinished={this.onCloseGeneralMenu}
|
||||
room={this.props.room}
|
||||
onPostFavoriteClick={(ev: ButtonEvent) => PosthogTrackers.trackInteraction(
|
||||
"WebRoomListRoomTileContextMenuFavouriteToggle", ev,
|
||||
)}
|
||||
onPostInviteClick={(ev: ButtonEvent) => PosthogTrackers.trackInteraction(
|
||||
"WebRoomListRoomTileContextMenuInviteItem", ev,
|
||||
)}
|
||||
onPostSettingsClick={(ev: ButtonEvent) => PosthogTrackers.trackInteraction(
|
||||
"WebRoomListRoomTileContextMenuSettingsItem", ev,
|
||||
)}
|
||||
onPostLeaveClick={(ev: ButtonEvent) => PosthogTrackers.trackInteraction(
|
||||
"WebRoomListRoomTileContextMenuLeaveItem", ev,
|
||||
)}
|
||||
/>
|
||||
) }
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue