mirror of
https://github.com/element-hq/element-web
synced 2024-11-22 17:25:50 +03:00
Use navigation treeview aria pattern for roomlist sublists and tiles
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
parent
2de88449aa
commit
3400808f6e
8 changed files with 81 additions and 25 deletions
|
@ -143,6 +143,8 @@ limitations under the License.
|
|||
|
||||
// toggle menuButton and badge on hover/menu displayed
|
||||
.mx_RoomTile_menuDisplayed,
|
||||
// or on keyboard focus of room tile
|
||||
.mx_RoomTile.focus-visible:focus-within,
|
||||
.mx_LeftPanel_container:not(.collapsed) .mx_RoomTile:hover {
|
||||
.mx_RoomTile_menuButton {
|
||||
display: block;
|
||||
|
|
|
@ -69,6 +69,8 @@ export const Key = {
|
|||
BACKSPACE: "Backspace",
|
||||
ARROW_UP: "ArrowUp",
|
||||
ARROW_DOWN: "ArrowDown",
|
||||
ARROW_LEFT: "ArrowLeft",
|
||||
ARROW_RIGHT: "ArrowRight",
|
||||
TAB: "Tab",
|
||||
ESCAPE: "Escape",
|
||||
ENTER: "Enter",
|
||||
|
|
|
@ -186,6 +186,7 @@ const LeftPanel = createReactClass({
|
|||
}
|
||||
} while (element && !(
|
||||
classes.contains("mx_RoomTile") ||
|
||||
classes.contains("mx_RoomSubList_label") ||
|
||||
classes.contains("mx_textinput_search")));
|
||||
|
||||
if (element) {
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018, 2019 New Vector Ltd
|
||||
Copyright 2019 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.
|
||||
|
@ -16,7 +17,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, {createRef} from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import classNames from 'classnames';
|
||||
import sdk from '../../index';
|
||||
|
@ -25,7 +26,7 @@ import Unread from '../../Unread';
|
|||
import * as RoomNotifs from '../../RoomNotifs';
|
||||
import * as FormattingUtils from '../../utils/FormattingUtils';
|
||||
import IndicatorScrollbar from './IndicatorScrollbar';
|
||||
import { KeyCode } from '../../Keyboard';
|
||||
import {Key, KeyCode} from '../../Keyboard';
|
||||
import { Group } from 'matrix-js-sdk';
|
||||
import PropTypes from 'prop-types';
|
||||
import RoomTile from "../views/rooms/RoomTile";
|
||||
|
@ -56,7 +57,6 @@ const RoomSubList = createReactClass({
|
|||
collapsed: PropTypes.bool.isRequired, // is LeftPanel collapsed?
|
||||
onHeaderClick: PropTypes.func,
|
||||
incomingCall: PropTypes.object,
|
||||
isFiltered: PropTypes.bool,
|
||||
extraTiles: PropTypes.arrayOf(PropTypes.node), // extra elements added beneath tiles
|
||||
forceExpand: PropTypes.bool,
|
||||
},
|
||||
|
@ -80,6 +80,7 @@ const RoomSubList = createReactClass({
|
|||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._headerButton = createRef();
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
},
|
||||
|
||||
|
@ -87,9 +88,9 @@ const RoomSubList = createReactClass({
|
|||
dis.unregister(this.dispatcherRef);
|
||||
},
|
||||
|
||||
// The header is collapsable if it is hidden or not stuck
|
||||
// The header is collapsible if it is hidden or not stuck
|
||||
// The dataset elements are added in the RoomList _initAndPositionStickyHeaders method
|
||||
isCollapsableOnClick: function() {
|
||||
isCollapsibleOnClick: function() {
|
||||
const stuck = this.refs.header.dataset.stuck;
|
||||
if (!this.props.forceExpand && (this.state.hidden || stuck === undefined || stuck === "none")) {
|
||||
return true;
|
||||
|
@ -114,8 +115,8 @@ const RoomSubList = createReactClass({
|
|||
},
|
||||
|
||||
onClick: function(ev) {
|
||||
if (this.isCollapsableOnClick()) {
|
||||
// The header isCollapsable, so the click is to be interpreted as collapse and truncation logic
|
||||
if (this.isCollapsibleOnClick()) {
|
||||
// The header isCollapsible, so the click is to be interpreted as collapse and truncation logic
|
||||
const isHidden = !this.state.hidden;
|
||||
this.setState({hidden: isHidden}, () => {
|
||||
this.props.onHeaderClick(isHidden);
|
||||
|
@ -124,6 +125,30 @@ const RoomSubList = createReactClass({
|
|||
// The header is stuck, so the click is to be interpreted as a scroll to the header
|
||||
this.props.onHeaderClick(this.state.hidden, this.refs.header.dataset.originalPosition);
|
||||
}
|
||||
this._headerButton.current.focus();
|
||||
},
|
||||
|
||||
onKeyDown: function(ev) {
|
||||
switch (ev.key) {
|
||||
case Key.TAB:
|
||||
// Prevent LeftPanel handling Tab if focus is on the sublist header itself
|
||||
ev.stopPropagation();
|
||||
break;
|
||||
case Key.ARROW_LEFT:
|
||||
ev.stopPropagation();
|
||||
if (!this.state.hidden && !this.props.forceExpand) {
|
||||
this.onClick();
|
||||
}
|
||||
break;
|
||||
case Key.ARROW_RIGHT:
|
||||
ev.stopPropagation();
|
||||
if (this.state.hidden && !this.props.forceExpand) {
|
||||
this.onClick();
|
||||
} else {
|
||||
// TODO go to first element in subtree
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
onRoomTileClick(roomId, ev) {
|
||||
|
@ -193,6 +218,11 @@ const RoomSubList = createReactClass({
|
|||
}
|
||||
},
|
||||
|
||||
onAddRoom: function(e) {
|
||||
e.stopPropagation();
|
||||
if (this.props.onAddRoom) this.props.onAddRoom();
|
||||
},
|
||||
|
||||
_getHeaderJsx: function(isCollapsed) {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const AccessibleTooltipButton = sdk.getComponent('elements.AccessibleTooltipButton');
|
||||
|
@ -209,12 +239,18 @@ const RoomSubList = createReactClass({
|
|||
'mx_RoomSubList_badgeHighlight': subListNotifHighlight,
|
||||
});
|
||||
if (subListNotifCount > 0) {
|
||||
badge = <div className={badgeClasses} onClick={this._onNotifBadgeClick}>
|
||||
{ FormattingUtils.formatCount(subListNotifCount) }
|
||||
</div>;
|
||||
badge = (
|
||||
<AccessibleButton className={badgeClasses} onClick={this._onNotifBadgeClick} aria-label={_t("Jump to first unread room.")}>
|
||||
{ FormattingUtils.formatCount(subListNotifCount) }
|
||||
</AccessibleButton>
|
||||
);
|
||||
} else if (this.props.isInvite && this.props.list.length) {
|
||||
// no notifications but highlight anyway because this is an invite badge
|
||||
badge = <div className={badgeClasses} onClick={this._onInviteBadgeClick}>{this.props.list.length}</div>;
|
||||
badge = (
|
||||
<AccessibleButton className={badgeClasses} onClick={this._onInviteBadgeClick} aria-label={_t("Jump to first invite.")}>
|
||||
{ this.props.list.length }
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -237,7 +273,7 @@ const RoomSubList = createReactClass({
|
|||
if (this.props.onAddRoom) {
|
||||
addRoomButton = (
|
||||
<AccessibleTooltipButton
|
||||
onClick={ this.props.onAddRoom }
|
||||
onClick={this.onAddRoom}
|
||||
className="mx_RoomSubList_addRoom"
|
||||
title={this.props.addRoomLabel || _t("Add room")}
|
||||
/>
|
||||
|
@ -255,10 +291,17 @@ const RoomSubList = createReactClass({
|
|||
chevron = (<div className={chevronClasses} />);
|
||||
}
|
||||
|
||||
const tabindex = this.props.isFiltered ? "0" : "-1";
|
||||
return (
|
||||
<div className="mx_RoomSubList_labelContainer" title={ title } ref="header">
|
||||
<AccessibleButton onClick={this.onClick} className="mx_RoomSubList_label" tabIndex={tabindex} aria-expanded={!isCollapsed}>
|
||||
<div className="mx_RoomSubList_labelContainer" title={title} ref="header">
|
||||
<AccessibleButton
|
||||
onClick={this.onClick}
|
||||
className="mx_RoomSubList_label"
|
||||
tabIndex={0}
|
||||
aria-expanded={!isCollapsed}
|
||||
inputRef={this._headerButton}
|
||||
// cancel out role so this button behaves as the toggle-header of this group
|
||||
role="none"
|
||||
>
|
||||
{ chevron }
|
||||
<span>{this.props.label}</span>
|
||||
{ incomingCall }
|
||||
|
@ -344,6 +387,7 @@ const RoomSubList = createReactClass({
|
|||
role="group"
|
||||
aria-label={this.props.label}
|
||||
aria-expanded={!isCollapsed}
|
||||
onKeyDown={this.onKeyDown}
|
||||
>
|
||||
{ this._getHeaderJsx(isCollapsed) }
|
||||
{ content }
|
||||
|
|
|
@ -67,8 +67,6 @@ export default function AccessibleButton(props) {
|
|||
restProps.ref = restProps.inputRef;
|
||||
delete restProps.inputRef;
|
||||
|
||||
restProps.tabIndex = restProps.tabIndex || "0";
|
||||
restProps.role = restProps.role || "button";
|
||||
restProps.className = (restProps.className ? restProps.className + " " : "") + "mx_AccessibleButton";
|
||||
|
||||
if (kind) {
|
||||
|
@ -93,19 +91,30 @@ export default function AccessibleButton(props) {
|
|||
*/
|
||||
AccessibleButton.propTypes = {
|
||||
children: PropTypes.node,
|
||||
inputRef: PropTypes.func,
|
||||
inputRef: PropTypes.oneOfType([
|
||||
// Either a function
|
||||
PropTypes.func,
|
||||
// Or the instance of a DOM native element
|
||||
PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
|
||||
]),
|
||||
element: PropTypes.string,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
|
||||
// The kind of button, similar to how Bootstrap works.
|
||||
// See available classes for AccessibleButton for options.
|
||||
kind: PropTypes.string,
|
||||
// The ARIA role
|
||||
role: PropTypes.string,
|
||||
// The tabIndex
|
||||
tabIndex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||||
|
||||
disabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
AccessibleButton.defaultProps = {
|
||||
element: 'div',
|
||||
role: 'button',
|
||||
tabIndex: "0",
|
||||
};
|
||||
|
||||
AccessibleButton.displayName = "AccessibleButton";
|
||||
|
|
|
@ -771,7 +771,7 @@ module.exports = createReactClass({
|
|||
const subListComponents = this._mapSubListProps(subLists);
|
||||
|
||||
return (
|
||||
<div ref={this._collectResizeContainer} className="mx_RoomList" role="listbox" aria-label={_t("Rooms")}
|
||||
<div ref={this._collectResizeContainer} className="mx_RoomList" role="tree" aria-label={_t("Rooms")}
|
||||
onMouseMove={this.onMouseMove} onMouseLeave={this.onMouseLeave}>
|
||||
{ subListComponents }
|
||||
</div>
|
||||
|
|
|
@ -398,7 +398,8 @@ module.exports = createReactClass({
|
|||
onMouseLeave={this.onMouseLeave}
|
||||
onContextMenu={this.onContextMenu}
|
||||
aria-label={ariaLabel}
|
||||
role="option"
|
||||
aria-selected={this.state.selected}
|
||||
role="treeitem"
|
||||
>
|
||||
<div className={avatarClasses}>
|
||||
<div className="mx_RoomTile_avatar_container">
|
||||
|
|
|
@ -901,11 +901,6 @@
|
|||
"Forget room": "Forget room",
|
||||
"Search": "Search",
|
||||
"Share room": "Share room",
|
||||
"Drop here to favourite": "Drop here to favourite",
|
||||
"Drop here to tag direct chat": "Drop here to tag direct chat",
|
||||
"Drop here to restore": "Drop here to restore",
|
||||
"Drop here to demote": "Drop here to demote",
|
||||
"Drop here to tag %(section)s": "Drop here to tag %(section)s",
|
||||
"Community Invites": "Community Invites",
|
||||
"Invites": "Invites",
|
||||
"Favourites": "Favourites",
|
||||
|
@ -1653,6 +1648,8 @@
|
|||
"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.",
|
||||
"Add room": "Add room",
|
||||
"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?",
|
||||
|
|
Loading…
Reference in a new issue