Merge pull request #3844 from matrix-org/t3chguy/roving

Implement Roving Tab Index and Room List as TreeView
This commit is contained in:
Michael Telatynski 2020-01-21 17:36:30 +00:00 committed by GitHub
commit 587ff6ad75
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 508 additions and 122 deletions

View file

@ -129,9 +129,6 @@ const LeftPanel = createReactClass({
if (!this.focusedElement) return;
switch (ev.key) {
case Key.TAB:
this._onMoveFocus(ev, ev.shiftKey);
break;
case Key.ARROW_UP:
this._onMoveFocus(ev, true, true);
break;

View file

@ -31,6 +31,7 @@ import PropTypes from 'prop-types';
import RoomTile from "../views/rooms/RoomTile";
import LazyRenderList from "../views/elements/LazyRenderList";
import {_t} from "../../languageHandler";
import {RovingTabIndexWrapper} from "../../accessibility/RovingTabIndex";
// turn this on for drop & drag console debugging galore
const debug = false;
@ -263,33 +264,6 @@ export default class RoomSubList extends React.PureComponent {
const subListNotifCount = subListNotifications.count;
const subListNotifHighlight = subListNotifications.highlight;
let badge;
if (!this.props.collapsed) {
const badgeClasses = classNames({
'mx_RoomSubList_badge': true,
'mx_RoomSubList_badgeHighlight': subListNotifHighlight,
});
// Wrap the contents in a div and apply styles to the child div so that the browser default outline works
if (subListNotifCount > 0) {
badge = (
<AccessibleButton className={badgeClasses} onClick={this._onNotifBadgeClick} aria-label={_t("Jump to first unread room.")}>
<div>
{ FormattingUtils.formatCount(subListNotifCount) }
</div>
</AccessibleButton>
);
} else if (this.props.isInvite && this.props.list.length) {
// no notifications but highlight anyway because this is an invite badge
badge = (
<AccessibleButton className={badgeClasses} onClick={this._onInviteBadgeClick} aria-label={_t("Jump to first invite.")}>
<div>
{ this.props.list.length }
</div>
</AccessibleButton>
);
}
}
// When collapsed, allow a long hover on the header to show user
// the full tag name and room count
let title;
@ -305,17 +279,6 @@ export default class RoomSubList extends React.PureComponent {
<IncomingCallBox className="mx_RoomSubList_incomingCall" incomingCall={this.props.incomingCall} />;
}
let addRoomButton;
if (this.props.onAddRoom) {
addRoomButton = (
<AccessibleTooltipButton
onClick={this.onAddRoom}
className="mx_RoomSubList_addRoom"
title={this.props.addRoomLabel || _t("Add room")}
/>
);
}
const len = this.props.list.length + this.props.extraTiles.length;
let chevron;
if (len) {
@ -327,25 +290,81 @@ export default class RoomSubList extends React.PureComponent {
chevron = (<div className={chevronClasses} />);
}
return (
<div className="mx_RoomSubList_labelContainer" title={title} ref={this._header} onKeyDown={this.onHeaderKeyDown}>
<AccessibleButton
onClick={this.onClick}
className="mx_RoomSubList_label"
tabIndex={0}
aria-expanded={!isCollapsed}
inputRef={this._headerButton}
role="treeitem"
aria-level="1"
>
{ chevron }
<span>{this.props.label}</span>
{ incomingCall }
</AccessibleButton>
{ badge }
{ addRoomButton }
</div>
);
return <RovingTabIndexWrapper inputRef={this._headerButton}>
{({onFocus, isActive, ref}) => {
const tabIndex = isActive ? 0 : -1;
let badge;
if (!this.props.collapsed) {
const badgeClasses = classNames({
'mx_RoomSubList_badge': true,
'mx_RoomSubList_badgeHighlight': subListNotifHighlight,
});
// Wrap the contents in a div and apply styles to the child div so that the browser default outline works
if (subListNotifCount > 0) {
badge = (
<AccessibleButton
tabIndex={tabIndex}
className={badgeClasses}
onClick={this._onNotifBadgeClick}
aria-label={_t("Jump to first unread room.")}
>
<div>
{ FormattingUtils.formatCount(subListNotifCount) }
</div>
</AccessibleButton>
);
} else if (this.props.isInvite && this.props.list.length) {
// no notifications but highlight anyway because this is an invite badge
badge = (
<AccessibleButton
tabIndex={tabIndex}
className={badgeClasses}
onClick={this._onInviteBadgeClick}
aria-label={_t("Jump to first invite.")}
>
<div>
{ this.props.list.length }
</div>
</AccessibleButton>
);
}
}
let addRoomButton;
if (this.props.onAddRoom) {
addRoomButton = (
<AccessibleTooltipButton
tabIndex={tabIndex}
onClick={this.onAddRoom}
className="mx_RoomSubList_addRoom"
title={this.props.addRoomLabel || _t("Add room")}
/>
);
}
return (
<div className="mx_RoomSubList_labelContainer" title={title} ref={this._header} onKeyDown={this.onHeaderKeyDown}>
<AccessibleButton
onFocus={onFocus}
tabIndex={tabIndex}
inputRef={ref}
onClick={this.onClick}
className="mx_RoomSubList_label"
aria-expanded={!isCollapsed}
role="treeitem"
aria-level="1"
>
{ chevron }
<span>{this.props.label}</span>
{ incomingCall }
</AccessibleButton>
{ badge }
{ addRoomButton }
</div>
);
} }
</RovingTabIndexWrapper>;
}
checkOverflow = () => {

View file

@ -133,9 +133,11 @@ export default createReactClass({
return null;
}
const clearButton = (!this.state.blurred || this.state.searchTerm) ?
(<AccessibleButton key="button"
className="mx_SearchBox_closeButton"
onClick={ () => {this._clearSearch("button"); } }>
(<AccessibleButton
key="button"
tabIndex={-1}
className="mx_SearchBox_closeButton"
onClick={ () => {this._clearSearch("button"); } }>
</AccessibleButton>) : undefined;
// show a shorter placeholder when blurred, if requested

View file

@ -26,6 +26,7 @@ import classNames from 'classnames';
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import {ContextMenu, ContextMenuButton, toRightOf} from "../../structures/ContextMenu";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {RovingTabIndexWrapper} from "../../../accessibility/RovingTabIndex";
// XXX this class copies a lot from RoomTile.js
export default createReactClass({
@ -137,16 +138,6 @@ export default createReactClass({
});
const badgeContent = badgeEllipsis ? '\u00B7\u00B7\u00B7' : '!';
const badge = (
<ContextMenuButton
className={badgeClasses}
onClick={this.onContextMenuButtonClick}
label={_t("Options")}
isExpanded={isMenuDisplayed}
>
{ badgeContent }
</ContextMenuButton>
);
let tooltip;
if (this.props.collapsed && this.state.hover) {
@ -171,22 +162,37 @@ export default createReactClass({
}
return <React.Fragment>
<AccessibleButton
className={classes}
onClick={this.onClick}
onMouseEnter={this.onMouseEnter}
onMouseLeave={this.onMouseLeave}
onContextMenu={this.onContextMenu}
>
<div className="mx_RoomTile_avatar">
{ av }
</div>
<div className="mx_RoomTile_nameContainer">
{ label }
{ badge }
</div>
{ tooltip }
</AccessibleButton>
<RovingTabIndexWrapper>
{({onFocus, isActive, ref}) =>
<AccessibleButton
onFocus={onFocus}
tabIndex={isActive ? 0 : -1}
inputRef={ref}
className={classes}
onClick={this.onClick}
onMouseEnter={this.onMouseEnter}
onMouseLeave={this.onMouseLeave}
onContextMenu={this.onContextMenu}
>
<div className="mx_RoomTile_avatar">
{ av }
</div>
<div className="mx_RoomTile_nameContainer">
{ label }
<ContextMenuButton
className={badgeClasses}
onClick={this.onContextMenuButtonClick}
label={_t("Options")}
isExpanded={isMenuDisplayed}
tabIndex={isActive ? 0 : -1}
>
{ badgeContent }
</ContextMenuButton>
</div>
{ tooltip }
</AccessibleButton>
}
</RovingTabIndexWrapper>
{ contextMenu }
</React.Fragment>;

View file

@ -39,6 +39,7 @@ import * as sdk from "../../../index";
import * as Receipt from "../../../utils/Receipt";
import {Resizer} from '../../../resizer';
import {Layout, Distributor} from '../../../resizer/distributors/roomsublist2';
import {RovingTabIndexProvider} from "../../../accessibility/RovingTabIndex";
const HIDE_CONFERENCE_CHANS = true;
const STANDARD_TAGS_REGEX = /^(m\.(favourite|lowpriority|server_notice)|im\.vector\.fake\.(invite|recent|direct|archived))$/;
@ -787,7 +788,9 @@ export default createReactClass({
onMouseMove={this.onMouseMove}
onMouseLeave={this.onMouseLeave}
>
{ subListComponents }
<RovingTabIndexProvider handleHomeEnd={true}>
{ subListComponents }
</RovingTabIndexProvider>
</div>
);
},

View file

@ -32,6 +32,7 @@ import ActiveRoomObserver from '../../../ActiveRoomObserver';
import RoomViewStore from '../../../stores/RoomViewStore';
import SettingsStore from "../../../settings/SettingsStore";
import {_t} from "../../../languageHandler";
import {RovingTabIndexWrapper} from "../../../accessibility/RovingTabIndex";
export default createReactClass({
displayName: 'RoomTile',
@ -432,36 +433,42 @@ export default createReactClass({
}
return <React.Fragment>
<AccessibleButton
tabIndex="0"
className={classes}
onClick={this.onClick}
onMouseEnter={this.onMouseEnter}
onMouseLeave={this.onMouseLeave}
onContextMenu={this.onContextMenu}
aria-label={ariaLabel}
aria-selected={this.state.selected}
role="treeitem"
>
<div className={avatarClasses}>
<div className="mx_RoomTile_avatar_container">
<RoomAvatar room={this.props.room} width={24} height={24} />
{ dmIndicator }
</div>
</div>
{ privateIcon }
<div className="mx_RoomTile_nameContainer">
<div className="mx_RoomTile_labelContainer">
{ label }
{ subtextLabel }
</div>
{ dmOnline }
{ contextMenuButton }
{ badge }
</div>
{ /* { incomingCallBox } */ }
{ tooltip }
</AccessibleButton>
<RovingTabIndexWrapper>
{({onFocus, isActive, ref}) =>
<AccessibleButton
onFocus={onFocus}
tabIndex={isActive ? 0 : -1}
inputRef={ref}
className={classes}
onClick={this.onClick}
onMouseEnter={this.onMouseEnter}
onMouseLeave={this.onMouseLeave}
onContextMenu={this.onContextMenu}
aria-label={ariaLabel}
aria-selected={this.state.selected}
role="treeitem"
>
<div className={avatarClasses}>
<div className="mx_RoomTile_avatar_container">
<RoomAvatar room={this.props.room} width={24} height={24} />
{ dmIndicator }
</div>
</div>
{ privateIcon }
<div className="mx_RoomTile_nameContainer">
<div className="mx_RoomTile_labelContainer">
{ label }
{ subtextLabel }
</div>
{ dmOnline }
{ contextMenuButton }
{ badge }
</div>
{ /* { incomingCallBox } */ }
{ tooltip }
</AccessibleButton>
}
</RovingTabIndexWrapper>
{ contextMenu }
</React.Fragment>;