Merge pull request #4896 from matrix-org/t3chguy/room-list/123

New Room List accessibility
pull/21833/head
Michael Telatynski 2020-07-07 14:51:41 +01:00 committed by GitHub
commit 502a0d930d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 267 additions and 53 deletions

View File

@ -86,11 +86,15 @@ $tagPanelWidth: 70px; // only applies in this file, used for calculations
.mx_RoomSearch_expanded + .mx_LeftPanel2_exploreButton {
// Cheaty way to return the occupied space to the filter input
flex-basis: 0;
margin: 0;
width: 0;
// Don't forget to hide the masked ::before icon
visibility: hidden;
// Don't forget to hide the masked ::before icon,
// using display:none or visibility:hidden would break accessibility
&::before {
content: none;
}
}
.mx_LeftPanel2_exploreButton {

24
src/RoomNotifsTypes.ts Normal file
View File

@ -0,0 +1,24 @@
/*
Copyright 2020 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 {
ALL_MESSAGES,
ALL_MESSAGES_LOUD,
MENTIONS_ONLY,
MUTE,
} from "./RoomNotifs";
export type Volume = ALL_MESSAGES_LOUD | ALL_MESSAGES | MENTIONS_ONLY | MUTE;

View File

@ -23,6 +23,8 @@ import classNames from 'classnames';
import {Key} from "../../Keyboard";
import * as sdk from "../../index";
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
// of doing reusable widgets like dialog boxes & menus where we go and
@ -421,6 +423,54 @@ MenuItemCheckbox.propTypes = {
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');
@ -439,6 +489,54 @@ MenuItemRadio.propTypes = {
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
export const toRightOf = (elementRect, chevronOffset=12) => {
const left = elementRect.right + window.pageXOffset + 3;

View File

@ -56,6 +56,15 @@ interface IState {
showTagPanel: boolean;
}
// List of CSS classes which should be included in keyboard navigation within the room list
const cssClasses = [
"mx_RoomSearch_input",
"mx_RoomSearch_icon", // minimized <RoomSearch />
"mx_RoomSublist2_headerText",
"mx_RoomTile2",
"mx_RoomSublist2_showNButton",
];
export default class LeftPanel2 extends React.Component<IProps, IState> {
private listContainerRef: React.RefObject<HTMLDivElement> = createRef();
private tagPanelWatcherRef: string;
@ -212,10 +221,7 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
if (element) {
classes = element.classList;
}
} while (element && !(
classes.contains("mx_RoomTile2") ||
classes.contains("mx_RoomSublist2_headerText") ||
classes.contains("mx_RoomSearch_input")));
} while (element && !cssClasses.some(c => classes.contains(c)));
if (element) {
element.focus();
@ -246,7 +252,12 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
private renderSearchExplore(): React.ReactNode {
return (
<div className="mx_LeftPanel2_filterContainer" onFocus={this.onFocus} onBlur={this.onBlur}>
<div
className="mx_LeftPanel2_filterContainer"
onFocus={this.onFocus}
onBlur={this.onBlur}
onKeyDown={this.onKeyDown}
>
<RoomSearch
onQueryUpdate={this.onSearch}
isMinimized={this.props.isMinimized}
@ -256,7 +267,7 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
// TODO fix the accessibility of this: https://github.com/vector-im/riot-web/issues/14180
className="mx_LeftPanel2_exploreButton"
onClick={this.onExplore}
alt={_t("Explore rooms")}
title={_t("Explore rooms")}
/>
</div>
);

View File

@ -149,7 +149,8 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
let clearButton = (
<AccessibleButton
tabIndex={-1}
className='mx_RoomSearch_clearButton'
title={_t("Clear filter")}
className="mx_RoomSearch_clearButton"
onClick={this.clearInput}
/>
);
@ -157,8 +158,8 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
if (this.props.isMinimized) {
icon = (
<AccessibleButton
tabIndex={-1}
className='mx_RoomSearch_icon'
title={_t("Search rooms")}
className="mx_RoomSearch_icon"
onClick={this.openSearch}
/>
);

View File

@ -329,7 +329,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
className={classes}
onClick={this.onOpenMenuClick}
inputRef={this.buttonRef}
label={_t("Account settings")}
label={_t("User menu")}
isExpanded={!!this.state.contextMenuPosition}
onContextMenu={this.onContextMenu}
>

View File

@ -132,7 +132,7 @@ const BaseAvatar = (props) => {
);
} else {
return (
<span className="mx_BaseAvatar" ref={inputRef} {...otherProps}>
<span className="mx_BaseAvatar" ref={inputRef} {...otherProps} role="presentation">
{ textNode }
{ imgNode }
</span>

View File

@ -274,9 +274,6 @@ export default class RoomList2 extends React.Component<IProps, IState> {
className="mx_RoomList2"
role="tree"
aria-label={_t("Rooms")}
// Firefox sometimes makes this element focusable due to
// overflow:scroll;, so force it out of tab order.
tabIndex={-1}
>{sublists}</div>
)}
</RovingTabIndexProvider>

View File

@ -26,8 +26,12 @@ import AccessibleButton from "../../views/elements/AccessibleButton";
import RoomTile2 from "./RoomTile2";
import { ResizableBox, ResizeCallbackData } from "react-resizable";
import { ListLayout } from "../../../stores/room-list/ListLayout";
import { ContextMenu, ContextMenuButton } from "../../structures/ContextMenu";
import StyledRadioButton from "../elements/StyledRadioButton";
import {
ContextMenu,
ContextMenuButton,
StyledMenuItemCheckbox,
StyledMenuItemRadio,
} from "../../structures/ContextMenu";
import RoomListStore from "../../../stores/room-list/RoomListStore2";
import { ListAlgorithm, SortAlgorithm } from "../../../stores/room-list/algorithms/models";
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
@ -62,7 +66,7 @@ interface IProps {
onAddRoom?: () => void;
addRoomLabel: string;
isInvite: boolean;
layout: ListLayout;
layout?: ListLayout;
isMinimized: boolean;
tagId: TagID;
onResize: () => void;
@ -136,11 +140,13 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
};
private onShowAllClick = () => {
// TODO a11y keep focus somewhere useful: https://github.com/vector-im/riot-web/issues/14180
this.props.layout.visibleTiles = this.props.layout.tilesWithPadding(this.numTiles, MAX_PADDING_HEIGHT);
this.forceUpdate(); // because the layout doesn't trigger a re-render
};
private onShowLessClick = () => {
// TODO a11y keep focus somewhere useful: https://github.com/vector-im/riot-web/issues/14180
this.props.layout.visibleTiles = this.props.layout.defaultVisibleTiles;
this.forceUpdate(); // because the layout doesn't trigger a re-render
};
@ -203,6 +209,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
dis.dispatch({
action: 'view_room',
room_id: room.roomId,
show_room_tile: true, // to make sure the room gets scrolled into view
});
}
};
@ -323,22 +330,24 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
<hr />
<div>
<div className='mx_RoomSublist2_contextMenu_title'>{_t("Unread rooms")}</div>
<StyledCheckbox
<StyledMenuItemCheckbox
onClose={this.onCloseMenu}
onChange={this.onUnreadFirstChanged}
checked={isUnreadFirst}
>
{_t("Always show first")}
</StyledCheckbox>
</StyledMenuItemCheckbox>
</div>
<hr />
<div>
<div className='mx_RoomSublist2_contextMenu_title'>{_t("Show")}</div>
<StyledCheckbox
<StyledMenuItemCheckbox
onClose={this.onCloseMenu}
onChange={this.onMessagePreviewChanged}
checked={this.props.layout.showPreviews}
>
{_t("Message preview")}
</StyledCheckbox>
</StyledMenuItemCheckbox>
</div>
</React.Fragment>
);
@ -354,20 +363,22 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
<div className="mx_RoomSublist2_contextMenu">
<div>
<div className='mx_RoomSublist2_contextMenu_title'>{_t("Sort by")}</div>
<StyledRadioButton
<StyledMenuItemRadio
onClose={this.onCloseMenu}
onChange={() => this.onTagSortChanged(SortAlgorithm.Recent)}
checked={!isAlphabetical}
name={`mx_${this.props.tagId}_sortBy`}
>
{_t("Activity")}
</StyledRadioButton>
<StyledRadioButton
</StyledMenuItemRadio>
<StyledMenuItemRadio
onClose={this.onCloseMenu}
onChange={() => this.onTagSortChanged(SortAlgorithm.Alphabetic)}
checked={isAlphabetical}
name={`mx_${this.props.tagId}_sortBy`}
>
{_t("A-Z")}
</StyledRadioButton>
</StyledMenuItemRadio>
</div>
{otherSections}
</div>
@ -390,16 +401,22 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
private renderHeader(): React.ReactElement {
return (
<RovingTabIndexWrapper>
<RovingTabIndexWrapper inputRef={this.headerButton}>
{({onFocus, isActive, ref}) => {
const tabIndex = isActive ? 0 : -1;
let ariaLabel = _t("Jump to first unread room.");
if (this.props.tagId === DefaultTagID.Invite) {
ariaLabel = _t("Jump to first invite.");
}
const badge = (
<NotificationBadge
forceCount={true}
notification={this.state.notificationState}
onClick={this.onBadgeClick}
tabIndex={tabIndex}
aria-label={ariaLabel}
/>
);
@ -440,7 +457,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
// doesn't become sticky.
// The same applies to the notification badge.
return (
<div className={classes} onKeyDown={this.onHeaderKeyDown} onFocus={onFocus}>
<div className={classes} onKeyDown={this.onHeaderKeyDown} onFocus={onFocus} aria-label={this.props.label}>
<div className="mx_RoomSublist2_stickable">
<AccessibleButton
onFocus={onFocus}
@ -448,6 +465,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
tabIndex={tabIndex}
className="mx_RoomSublist2_headerText"
role="treeitem"
aria-expanded={!this.props.layout || !this.props.layout.isCollapsed}
aria-level={1}
onClick={this.onHeaderClick}
onContextMenu={this.onContextMenu}
@ -503,12 +521,12 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
);
if (this.props.isMinimized) showMoreText = null;
showNButton = (
<div onClick={this.onShowAllClick} className={showMoreBtnClasses}>
<AccessibleButton onClick={this.onShowAllClick} className={showMoreBtnClasses} tabIndex={-1}>
<span className='mx_RoomSublist2_showMoreButtonChevron mx_RoomSublist2_showNButtonChevron'>
{/* set by CSS masking */}
</span>
{showMoreText}
</div>
</AccessibleButton>
);
} else if (this.numTiles <= visibleTiles.length && this.numTiles > this.props.layout.defaultVisibleTiles) {
// we have all tiles visible - add a button to show less
@ -518,13 +536,14 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
</span>
);
if (this.props.isMinimized) showLessText = null;
// TODO Roving tab index / treeitem?: https://github.com/vector-im/riot-web/issues/14180
showNButton = (
<div onClick={this.onShowLessClick} className={showMoreBtnClasses}>
<AccessibleButton onClick={this.onShowLessClick} className={showMoreBtnClasses} tabIndex={-1}>
<span className='mx_RoomSublist2_showLessButtonChevron mx_RoomSublist2_showNButtonChevron'>
{/* set by CSS masking */}
</span>
{showLessText}
</div>
</AccessibleButton>
);
}

View File

@ -26,17 +26,30 @@ import dis from '../../../dispatcher/dispatcher';
import { Key } from "../../../Keyboard";
import ActiveRoomObserver from "../../../ActiveRoomObserver";
import { _t } from "../../../languageHandler";
import { ContextMenu, ContextMenuButton, MenuItemRadio } from "../../structures/ContextMenu";
import {
ContextMenu,
ContextMenuButton,
MenuItemRadio,
MenuItemCheckbox,
MenuItem,
} from "../../structures/ContextMenu";
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore";
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
import { getRoomNotifsState, ALL_MESSAGES, ALL_MESSAGES_LOUD, MENTIONS_ONLY, MUTE } from "../../../RoomNotifs";
import {
getRoomNotifsState,
setRoomNotifsState,
ALL_MESSAGES,
ALL_MESSAGES_LOUD,
MENTIONS_ONLY,
MUTE,
} from "../../../RoomNotifs";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { setRoomNotifsState } from "../../../RoomNotifs";
import { TagSpecificNotificationState } from "../../../stores/notifications/TagSpecificNotificationState";
import { INotificationState } from "../../../stores/notifications/INotificationState";
import NotificationBadge from "./NotificationBadge";
import { NotificationColor } from "../../../stores/notifications/NotificationColor";
import { Volume } from "../../../RoomNotifsTypes";
// TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
@ -68,6 +81,8 @@ interface IState {
generalMenuPosition: PartialDOMRect;
}
const messagePreviewId = (roomId: string) => `mx_RoomTile2_messagePreview_${roomId}`;
const contextMenuBelow = (elementRect: PartialDOMRect) => {
// align the context menu's icons with the icon which opened the context menu
const left = elementRect.left + window.pageXOffset - 9;
@ -123,6 +138,10 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
return !this.props.isMinimized && this.props.tag !== DefaultTagID.Invite;
}
private get showMessagePreview(): boolean {
return !this.props.isMinimized && this.props.showMessagePreview;
}
public componentWillUnmount() {
if (this.props.room) {
ActiveRoomObserver.removeListener(this.props.room.roomId, this.onActiveRoomUpdate);
@ -195,6 +214,11 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
// TODO: Support tagging: https://github.com/vector-im/riot-web/issues/14211
// TODO: XOR favourites and low priority: https://github.com/vector-im/riot-web/issues/14210
if ((ev as React.KeyboardEvent).key === Key.ENTER) {
// Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12
this.setState({generalMenuPosition: null}); // hide the menu
}
};
private onLeaveRoomClick = (ev: ButtonEvent) => {
@ -219,11 +243,13 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
this.setState({generalMenuPosition: null}); // hide the menu
};
private async saveNotifState(ev: ButtonEvent, newState: ALL_MESSAGES_LOUD | ALL_MESSAGES | MENTIONS_ONLY | MUTE) {
private async saveNotifState(ev: ButtonEvent, newState: Volume) {
ev.preventDefault();
ev.stopPropagation();
if (MatrixClientPeg.get().isGuest()) return;
// get key before we go async and React discards the nativeEvent
const key = (ev as React.KeyboardEvent).key;
try {
// TODO add local echo - https://github.com/vector-im/riot-web/issues/14280
await setRoomNotifsState(this.props.room.roomId, newState);
@ -233,7 +259,10 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
console.error(error);
}
this.setState({notificationsMenuPosition: null}); // Close the context menu
if (key === Key.ENTER) {
// Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12
this.setState({notificationsMenuPosition: null}); // hide the menu
}
}
private onClickAllNotifs = ev => this.saveNotifState(ev, ALL_MESSAGES);
@ -322,20 +351,24 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
<ContextMenu {...contextMenuBelow(this.state.generalMenuPosition)} onFinished={this.onCloseGeneralMenu}>
<div className="mx_IconizedContextMenu mx_IconizedContextMenu_compact mx_RoomTile2_contextMenu">
<div className="mx_IconizedContextMenu_optionList">
<AccessibleButton onClick={(e) => this.onTagRoom(e, DefaultTagID.Favourite)}>
<MenuItemCheckbox
onClick={(e) => this.onTagRoom(e, DefaultTagID.Favourite)}
active={false} // TODO: https://github.com/vector-im/riot-web/issues/14283
label={_t("Favourite")}
>
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconStar" />
<span className="mx_IconizedContextMenu_label">{_t("Favourite")}</span>
</AccessibleButton>
<AccessibleButton onClick={this.onOpenRoomSettings}>
</MenuItemCheckbox>
<MenuItem onClick={this.onOpenRoomSettings} label={_t("Settings")}>
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconSettings" />
<span className="mx_IconizedContextMenu_label">{_t("Settings")}</span>
</AccessibleButton>
</MenuItem>
</div>
<div className="mx_IconizedContextMenu_optionList mx_RoomTile2_contextMenu_redRow">
<AccessibleButton onClick={this.onLeaveRoomClick}>
<MenuItem onClick={this.onLeaveRoomClick} label={_t("Leave Room")}>
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconSignOut" />
<span className="mx_IconizedContextMenu_label">{_t("Leave Room")}</span>
</AccessibleButton>
</MenuItem>
</div>
</div>
</ContextMenu>
@ -375,8 +408,9 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
let badge: React.ReactNode;
if (!this.props.isMinimized) {
// aria-hidden because we summarise the unread count/highlight status in a manual aria-label below
badge = (
<div className="mx_RoomTile2_badgeContainer">
<div className="mx_RoomTile2_badgeContainer" aria-hidden="true">
<NotificationBadge
notification={this.state.notificationState}
forceCount={false}
@ -392,24 +426,25 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
let messagePreview = null;
if (this.props.showMessagePreview && !this.props.isMinimized) {
if (this.showMessagePreview) {
// The preview store heavily caches this info, so should be safe to hammer.
const text = MessagePreviewStore.instance.getPreviewForRoom(this.props.room, this.props.tag);
// Only show the preview if there is one to show.
if (text) {
messagePreview = (
<div className="mx_RoomTile2_messagePreview">
<div className="mx_RoomTile2_messagePreview" id={messagePreviewId(this.props.room.roomId)}>
{text}
</div>
);
}
}
const notificationColor = this.state.notificationState.color;
const nameClasses = classNames({
"mx_RoomTile2_name": true,
"mx_RoomTile2_nameWithPreview": !!messagePreview,
"mx_RoomTile2_nameHasUnreadEvents": this.state.notificationState.color >= NotificationColor.Bold,
"mx_RoomTile2_nameHasUnreadEvents": notificationColor >= NotificationColor.Bold,
});
let nameContainer = (
@ -422,6 +457,27 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
);
if (this.props.isMinimized) nameContainer = null;
let ariaLabel = name;
// The following labels are written in such a fashion to increase screen reader efficiency (speed).
if (this.props.tag === DefaultTagID.Invite) {
// append nothing
} else if (notificationColor >= NotificationColor.Red) {
ariaLabel += " " + _t("%(count)s unread messages including mentions.", {
count: this.state.notificationState.count,
});
} else if (notificationColor >= NotificationColor.Grey) {
ariaLabel += " " + _t("%(count)s unread messages.", {
count: this.state.notificationState.count,
});
} else if (notificationColor >= NotificationColor.Bold) {
ariaLabel += " " + _t("Unread messages.");
}
let ariaDescribedBy: string;
if (this.showMessagePreview) {
ariaDescribedBy = messagePreviewId(this.props.room.roomId);
}
return (
<React.Fragment>
<RovingTabIndexWrapper>
@ -434,8 +490,11 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
onMouseEnter={this.onTileMouseEnter}
onMouseLeave={this.onTileMouseLeave}
onClick={this.onTileClick}
role="treeitem"
onContextMenu={this.onContextMenu}
role="treeitem"
aria-label={ariaLabel}
aria-selected={this.state.selected}
aria-describedby={ariaDescribedBy}
>
{roomAvatar}
{nameContainer}

View File

@ -1208,6 +1208,8 @@
"Activity": "Activity",
"A-Z": "A-Z",
"List options": "List options",
"Jump to first unread room.": "Jump to first unread room.",
"Jump to first invite.": "Jump to first invite.",
"Add room": "Add room",
"Show %(count)s more|other": "Show %(count)s more",
"Show %(count)s more|one": "Show %(count)s more",
@ -2089,6 +2091,8 @@
"Find a room…": "Find a room…",
"Find a room… (e.g. %(exampleRoom)s)": "Find a room… (e.g. %(exampleRoom)s)",
"If you can't find the room you're looking for, ask for an invite or <a>Create a new room</a>.": "If you can't find the room you're looking for, ask for an invite or <a>Create a new room</a>.",
"Clear filter": "Clear filter",
"Search rooms": "Search rooms",
"You can't send any messages until you review and agree to <consentLink>our terms and conditions</consentLink>.": "You can't send any messages until you review and agree to <consentLink>our terms and conditions</consentLink>.",
"Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please <a>contact your service administrator</a> to continue using the service.": "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please <a>contact your service administrator</a> to continue using the service.",
"Your message wasn't sent because this homeserver has exceeded a resource limit. Please <a>contact your service administrator</a> to continue using the service.": "Your message wasn't sent because this homeserver has exceeded a resource limit. Please <a>contact your service administrator</a> to continue using the service.",
@ -2100,8 +2104,6 @@
"Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.",
"Active call": "Active call",
"There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?": "There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?",
"Jump to first unread room.": "Jump to first unread room.",
"Jump to first invite.": "Jump to first invite.",
"You seem to be uploading files, are you sure you want to quit?": "You seem to be uploading files, are you sure you want to quit?",
"You seem to be in a call, are you sure you want to quit?": "You seem to be in a call, are you sure you want to quit?",
"Search failed": "Search failed",
@ -2116,7 +2118,6 @@
"Click to mute video": "Click to mute video",
"Click to unmute audio": "Click to unmute audio",
"Click to mute audio": "Click to mute audio",
"Clear filter": "Clear filter",
"Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.",
"Tried to load a specific point in this room's timeline, but was unable to find it.": "Tried to load a specific point in this room's timeline, but was unable to find it.",
"Failed to load timeline position": "Failed to load timeline position",
@ -2131,7 +2132,7 @@
"All settings": "All settings",
"Archived rooms": "Archived rooms",
"Feedback": "Feedback",
"Account settings": "Account settings",
"User menu": "User menu",
"Could not load user profile": "Could not load user profile",
"Verify this login": "Verify this login",
"Session verified": "Session verified",