Improve RovingTabIndex & Room List filtering performance (#6987)
This commit is contained in:
parent
39e61c4fa3
commit
04c06b6aa8
9 changed files with 471 additions and 327 deletions
|
@ -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 />
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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 })}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue