Merge pull request #4896 from matrix-org/t3chguy/room-list/123
New Room List accessibilitypull/21833/head
commit
502a0d930d
|
@ -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 {
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in New Issue