Improve RovingTabIndex & Room List filtering performance (#6987)

This commit is contained in:
Michael Telatynski 2021-10-26 12:16:50 +01:00 committed by GitHub
parent 39e61c4fa3
commit 04c06b6aa8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 471 additions and 327 deletions

View file

@ -249,6 +249,8 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
let handled = true;
switch (ev.key) {
// XXX: this is imitating roving behaviour, it should really use the RovingTabIndex utils
// to inherit proper handling of unmount edge cases
case Key.TAB:
case Key.ESCAPE:
case Key.ARROW_LEFT: // close on left and right arrows too for when it is a context menu on a <Toolbar />

View file

@ -40,6 +40,7 @@ import { replaceableComponent } from "../../utils/replaceableComponent";
import SpaceStore, { UPDATE_SELECTED_SPACE } from "../../stores/SpaceStore";
import { getKeyBindingsManager, RoomListAction } from "../../KeyBindingsManager";
import UIStore from "../../stores/UIStore";
import { findSiblingElement, IState as IRovingTabIndexState } from "../../accessibility/RovingTabIndex";
interface IProps {
isMinimized: boolean;
@ -51,19 +52,12 @@ interface IState {
activeSpace?: Room;
}
// List of CSS classes which should be included in keyboard navigation within the room list
const cssClasses = [
"mx_RoomSearch_input",
"mx_RoomSearch_minimizedHandle", // minimized <RoomSearch />
"mx_RoomSublist_headerText",
"mx_RoomTile",
"mx_RoomSublist_showNButton",
];
@replaceableComponent("structures.LeftPanel")
export default class LeftPanel extends React.Component<IProps, IState> {
private ref: React.RefObject<HTMLDivElement> = createRef();
private listContainerRef: React.RefObject<HTMLDivElement> = createRef();
private ref = createRef<HTMLDivElement>();
private listContainerRef = createRef<HTMLDivElement>();
private roomSearchRef = createRef<RoomSearch>();
private roomListRef = createRef<RoomList>();
private focusedElement = null;
private isDoingStickyHeaders = false;
@ -283,16 +277,25 @@ export default class LeftPanel extends React.Component<IProps, IState> {
this.focusedElement = null;
};
private onKeyDown = (ev: React.KeyboardEvent) => {
private onKeyDown = (ev: React.KeyboardEvent, state?: IRovingTabIndexState) => {
if (!this.focusedElement) return;
const action = getKeyBindingsManager().getRoomListAction(ev);
switch (action) {
case RoomListAction.NextRoom:
if (!state) {
ev.stopPropagation();
ev.preventDefault();
this.roomListRef.current?.focus();
}
break;
case RoomListAction.PrevRoom:
ev.stopPropagation();
ev.preventDefault();
this.onMoveFocus(action === RoomListAction.PrevRoom);
if (state && state.activeRef === findSiblingElement(state.refs, 0)) {
ev.stopPropagation();
ev.preventDefault();
this.roomSearchRef.current?.focus();
}
break;
}
};
@ -305,45 +308,6 @@ export default class LeftPanel extends React.Component<IProps, IState> {
}
};
private onMoveFocus = (up: boolean) => {
let element = this.focusedElement;
let descending = false; // are we currently descending or ascending through the DOM tree?
let classes: DOMTokenList;
do {
const child = up ? element.lastElementChild : element.firstElementChild;
const sibling = up ? element.previousElementSibling : element.nextElementSibling;
if (descending) {
if (child) {
element = child;
} else if (sibling) {
element = sibling;
} else {
descending = false;
element = element.parentElement;
}
} else {
if (sibling) {
element = sibling;
descending = true;
} else {
element = element.parentElement;
}
}
if (element) {
classes = element.classList;
}
} while (element && (!cssClasses.some(c => classes.contains(c)) || element.offsetParent === null));
if (element) {
element.focus();
this.focusedElement = element;
}
};
private renderHeader(): React.ReactNode {
return (
<div className="mx_LeftPanel_userHeader">
@ -388,7 +352,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
>
<RoomSearch
isMinimized={this.props.isMinimized}
onKeyDown={this.onKeyDown}
ref={this.roomSearchRef}
onSelectRoom={this.selectRoom}
/>
@ -417,6 +381,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
activeSpace={this.state.activeSpace}
onResize={this.refreshStickyHeaders}
onListCollapse={this.refreshStickyHeaders}
ref={this.roomListRef}
/>;
const containerClasses = classNames({

View file

@ -32,7 +32,6 @@ import SpaceStore, { UPDATE_SELECTED_SPACE, UPDATE_TOP_LEVEL_SPACES } from "../.
interface IProps {
isMinimized: boolean;
onKeyDown(ev: React.KeyboardEvent): void;
/**
* @returns true if a room has been selected and the search field should be cleared
*/
@ -133,11 +132,6 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
this.clearInput();
defaultDispatcher.fire(Action.FocusSendMessageComposer);
break;
case RoomListAction.NextRoom:
case RoomListAction.PrevRoom:
// we don't handle these actions here put pass the event on to the interested party (LeftPanel)
this.props.onKeyDown(ev);
break;
case RoomListAction.SelectRoom: {
const shouldClear = this.props.onSelectRoom();
if (shouldClear) {
@ -151,6 +145,10 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
}
};
public focus(): void {
this.inputRef.current?.focus();
}
public render(): React.ReactNode {
const classes = classNames({
'mx_RoomSearch': true,

View file

@ -14,14 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { ReactComponentElement } from "react";
import React, { createRef, ReactComponentElement } from "react";
import { Dispatcher } from "flux";
import { Room } from "matrix-js-sdk/src/models/room";
import * as fbEmitter from "fbemitter";
import { EventType } from "matrix-js-sdk/src/@types/event";
import { _t, _td } from "../../../languageHandler";
import { RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex";
import { RovingTabIndexProvider, IState as IRovingTabIndexState } from "../../../accessibility/RovingTabIndex";
import ResizeNotifier from "../../../utils/ResizeNotifier";
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore";
import RoomViewStore from "../../../stores/RoomViewStore";
@ -54,7 +54,7 @@ import { UIComponent } from "../../../settings/UIFeature";
import { JoinRule } from "matrix-js-sdk/src/@types/partials";
interface IProps {
onKeyDown: (ev: React.KeyboardEvent) => void;
onKeyDown: (ev: React.KeyboardEvent, state: IRovingTabIndexState) => void;
onFocus: (ev: React.FocusEvent) => void;
onBlur: (ev: React.FocusEvent) => void;
onResize: () => void;
@ -249,6 +249,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
private dispatcherRef;
private customTagStoreRef;
private roomStoreToken: fbEmitter.EventSubscription;
private treeRef = createRef<HTMLDivElement>();
constructor(props: IProps) {
super(props);
@ -505,6 +506,12 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
});
}
public focus(): void {
// focus the first focusable element in this aria treeview widget
[...this.treeRef.current?.querySelectorAll<HTMLElement>('[role="treeitem"]')]
.find(e => e.offsetParent !== null)?.focus();
}
public render() {
const cli = MatrixClientPeg.get();
const userId = cli.getUserId();
@ -584,7 +591,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
const sublists = this.renderSublists();
return (
<RovingTabIndexProvider handleHomeEnd={true} onKeyDown={this.props.onKeyDown}>
<RovingTabIndexProvider handleHomeEnd handleUpDown onKeyDown={this.props.onKeyDown}>
{ ({ onKeyDownHandler }) => (
<div
onFocus={this.props.onFocus}
@ -593,6 +600,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
className="mx_RoomList"
role="tree"
aria-label={_t("Rooms")}
ref={this.treeRef}
>
{ sublists }
{ explorePrompt }

View file

@ -43,7 +43,6 @@ import SpaceStore, {
} from "../../../stores/SpaceStore";
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
import { RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex";
import { Key } from "../../../Keyboard";
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
import SpaceContextMenu from "../context_menus/SpaceContextMenu";
import IconizedContextMenu, {
@ -228,75 +227,12 @@ const SpacePanel = () => {
return () => UIStore.instance.stopTrackingElementDimensions("SpacePanel");
}, []);
const onKeyDown = (ev: React.KeyboardEvent) => {
if (ev.defaultPrevented) return;
let handled = true;
switch (ev.key) {
case Key.ARROW_UP:
onMoveFocus(ev.target as Element, true);
break;
case Key.ARROW_DOWN:
onMoveFocus(ev.target as Element, false);
break;
default:
handled = false;
}
if (handled) {
// consume all other keys in context menu
ev.stopPropagation();
ev.preventDefault();
}
};
const onMoveFocus = (element: Element, up: boolean) => {
let descending = false; // are we currently descending or ascending through the DOM tree?
let classes: DOMTokenList;
do {
const child = up ? element.lastElementChild : element.firstElementChild;
const sibling = up ? element.previousElementSibling : element.nextElementSibling;
if (descending) {
if (child) {
element = child;
} else if (sibling) {
element = sibling;
} else {
descending = false;
element = element.parentElement;
}
} else {
if (sibling) {
element = sibling;
descending = true;
} else {
element = element.parentElement;
}
}
if (element) {
if (element.classList.contains("mx_ContextualMenu")) { // we hit the top
element = up ? element.lastElementChild : element.firstElementChild;
descending = true;
}
classes = element.classList;
}
} while (element && !classes.contains("mx_SpaceButton"));
if (element) {
(element as HTMLElement).focus();
}
};
return (
<DragDropContext onDragEnd={result => {
if (!result.destination) return; // dropped outside the list
SpaceStore.instance.moveRootSpace(result.source.index, result.destination.index);
}}>
<RovingTabIndexProvider handleHomeEnd={true} onKeyDown={onKeyDown}>
<RovingTabIndexProvider handleHomeEnd handleUpDown>
{ ({ onKeyDownHandler }) => (
<ul
className={classNames("mx_SpacePanel", { collapsed: isPanelCollapsed })}