Fix keyboard accessibility of the space panel

This commit is contained in:
Michael Telatynski 2021-06-22 11:50:00 +01:00
parent 9fa7ac21bd
commit 66b3feb802
2 changed files with 98 additions and 53 deletions

View file

@ -62,6 +62,8 @@ export default function AccessibleButton({
disabled, disabled,
inputRef, inputRef,
className, className,
onKeyDown,
onKeyUp,
...restProps ...restProps
}: IProps) { }: IProps) {
const newProps: IAccessibleButtonProps = restProps; const newProps: IAccessibleButtonProps = restProps;
@ -83,6 +85,8 @@ export default function AccessibleButton({
if (e.key === Key.SPACE) { if (e.key === Key.SPACE) {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
} else {
onKeyDown?.(e);
} }
}; };
newProps.onKeyUp = (e) => { newProps.onKeyUp = (e) => {
@ -94,6 +98,8 @@ export default function AccessibleButton({
if (e.key === Key.ENTER) { if (e.key === Key.ENTER) {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
} else {
onKeyUp?.(e);
} }
}; };
} }

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from "react"; import React, { createRef } from "react";
import classNames from "classnames"; import classNames from "classnames";
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
@ -22,7 +22,6 @@ import RoomAvatar from "../avatars/RoomAvatar";
import SpaceStore from "../../../stores/SpaceStore"; import SpaceStore from "../../../stores/SpaceStore";
import SpaceTreeLevelLayoutStore from "../../../stores/SpaceTreeLevelLayoutStore"; import SpaceTreeLevelLayoutStore from "../../../stores/SpaceTreeLevelLayoutStore";
import NotificationBadge from "../rooms/NotificationBadge"; import NotificationBadge from "../rooms/NotificationBadge";
import {RovingAccessibleButton} from "../../../accessibility/roving/RovingAccessibleButton";
import { RovingAccessibleTooltipButton } from "../../../accessibility/roving/RovingAccessibleTooltipButton"; import { RovingAccessibleTooltipButton } from "../../../accessibility/roving/RovingAccessibleTooltipButton";
import IconizedContextMenu, { import IconizedContextMenu, {
IconizedContextMenuOption, IconizedContextMenuOption,
@ -48,6 +47,7 @@ import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
import { EventType } from "matrix-js-sdk/src/@types/event"; import { EventType } from "matrix-js-sdk/src/@types/event";
import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState"; import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState";
import { NotificationColor } from "../../../stores/notifications/NotificationColor"; import { NotificationColor } from "../../../stores/notifications/NotificationColor";
import { getKeyBindingsManager, RoomListAction } from "../../../KeyBindingsManager";
interface IItemProps { interface IItemProps {
space?: Room; space?: Room;
@ -61,11 +61,14 @@ interface IItemProps {
interface IItemState { interface IItemState {
collapsed: boolean; collapsed: boolean;
contextMenuPosition: Pick<DOMRect, "right" | "top" | "height">; contextMenuPosition: Pick<DOMRect, "right" | "top" | "height">;
childSpaces: Room[];
} }
export class SpaceItem extends React.PureComponent<IItemProps, IItemState> { export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
static contextType = MatrixClientContext; static contextType = MatrixClientContext;
private buttonRef = createRef<HTMLDivElement>();
constructor(props) { constructor(props) {
super(props); super(props);
@ -78,14 +81,36 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
this.state = { this.state = {
collapsed: collapsed, collapsed: collapsed,
contextMenuPosition: null, contextMenuPosition: null,
childSpaces: this.childSpaces,
}; };
SpaceStore.instance.on(this.props.space.roomId, this.onSpaceUpdate);
} }
private toggleCollapse(evt) { componentWillUnmount() {
if (this.props.onExpand && this.state.collapsed) { SpaceStore.instance.off(this.props.space.roomId, this.onSpaceUpdate);
}
private onSpaceUpdate = () => {
this.setState({
childSpaces: this.childSpaces,
});
};
private get childSpaces() {
return SpaceStore.instance.getChildSpaces(this.props.space.roomId)
.filter(s => !this.props.parents?.has(s.roomId));
}
private get isCollapsed() {
return this.state.collapsed || this.props.isPanelCollapsed;
}
private toggleCollapse = evt => {
if (this.props.onExpand && this.isCollapsed) {
this.props.onExpand(); this.props.onExpand();
} }
const newCollapsedState = !this.state.collapsed; const newCollapsedState = !this.isCollapsed;
SpaceTreeLevelLayoutStore.instance.setSpaceCollapsedState( SpaceTreeLevelLayoutStore.instance.setSpaceCollapsedState(
this.props.space.roomId, this.props.space.roomId,
@ -96,7 +121,7 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
// don't bubble up so encapsulating button for space // don't bubble up so encapsulating button for space
// doesn't get triggered // doesn't get triggered
evt.stopPropagation(); evt.stopPropagation();
} };
private onContextMenu = (ev: React.MouseEvent) => { private onContextMenu = (ev: React.MouseEvent) => {
if (this.props.space.getMyMembership() !== "join") return; if (this.props.space.getMyMembership() !== "join") return;
@ -111,6 +136,43 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
}); });
} }
private onKeyDown = (ev: React.KeyboardEvent) => {
let handled = true;
const action = getKeyBindingsManager().getRoomListAction(ev);
const hasChildren = this.state.childSpaces?.length;
switch (action) {
case RoomListAction.CollapseSection:
if (hasChildren && !this.isCollapsed) {
this.toggleCollapse(ev);
} else {
const parentItem = this.buttonRef?.current?.parentElement?.parentElement;
const parentButton = parentItem?.previousElementSibling as HTMLElement;
parentButton?.focus();
}
break;
case RoomListAction.ExpandSection:
if (hasChildren) {
if (this.isCollapsed) {
this.toggleCollapse(ev);
} else {
const childLevel = this.buttonRef?.current?.nextElementSibling;
const firstSpaceItemChild = childLevel?.querySelector<HTMLLIElement>(".mx_SpaceItem");
firstSpaceItemChild?.querySelector<HTMLDivElement>(".mx_SpaceButton")?.focus();
}
}
break;
default:
handled = false;
}
if (handled) {
ev.stopPropagation();
ev.preventDefault();
}
};
private onClick = (ev: React.MouseEvent) => { private onClick = (ev: React.MouseEvent) => {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
@ -302,18 +364,15 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
render() { render() {
const {space, activeSpaces, isNested} = this.props; const {space, activeSpaces, isNested} = this.props;
const forceCollapsed = this.props.isPanelCollapsed;
const isNarrow = this.props.isPanelCollapsed; const isNarrow = this.props.isPanelCollapsed;
const collapsed = this.state.collapsed || forceCollapsed; const collapsed = this.isCollapsed;
const childSpaces = SpaceStore.instance.getChildSpaces(space.roomId)
.filter(s => !this.props.parents?.has(s.roomId));
const isActive = activeSpaces.includes(space); const isActive = activeSpaces.includes(space);
const itemClasses = classNames({ const itemClasses = classNames({
"mx_SpaceItem": true, "mx_SpaceItem": true,
"mx_SpaceItem_narrow": isNarrow, "mx_SpaceItem_narrow": isNarrow,
"collapsed": collapsed, "collapsed": collapsed,
"hasSubSpaces": childSpaces && childSpaces.length, "hasSubSpaces": this.state.childSpaces?.length,
}); });
const isInvite = space.getMyMembership() === "invite"; const isInvite = space.getMyMembership() === "invite";
@ -328,9 +387,9 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
: SpaceStore.instance.getNotificationState(space.roomId); : SpaceStore.instance.getNotificationState(space.roomId);
let childItems; let childItems;
if (childSpaces && !collapsed) { if (this.state.childSpaces?.length && !collapsed) {
childItems = <SpaceTreeLevel childItems = <SpaceTreeLevel
spaces={childSpaces} spaces={this.state.childSpaces}
activeSpaces={activeSpaces} activeSpaces={activeSpaces}
isNested={true} isNested={true}
parents={new Set(this.props.parents).add(this.props.space.roomId)} parents={new Set(this.props.parents).add(this.props.space.roomId)}
@ -346,53 +405,33 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
const avatarSize = isNested ? 24 : 32; const avatarSize = isNested ? 24 : 32;
const toggleCollapseButton = childSpaces && childSpaces.length ? const toggleCollapseButton = this.state.childSpaces?.length ?
<AccessibleButton <AccessibleButton
className="mx_SpaceButton_toggleCollapse" className="mx_SpaceButton_toggleCollapse"
onClick={evt => this.toggleCollapse(evt)} onClick={this.toggleCollapse}
/> : null; /> : null;
let button; return (
if (isNarrow) { <li className={itemClasses}>
button = (
<RovingAccessibleTooltipButton <RovingAccessibleTooltipButton
className={classes} className={classes}
title={space.name} title={space.name}
onClick={this.onClick} onClick={this.onClick}
onContextMenu={this.onContextMenu} onContextMenu={this.onContextMenu}
forceHide={!!this.state.contextMenuPosition} forceHide={!isNarrow || !!this.state.contextMenuPosition}
role="treeitem" role="treeitem"
inputRef={this.buttonRef}
onKeyDown={this.onKeyDown}
> >
{ toggleCollapseButton } { toggleCollapseButton }
<div className="mx_SpaceButton_selectionWrapper"> <div className="mx_SpaceButton_selectionWrapper">
<RoomAvatar width={avatarSize} height={avatarSize} room={space} /> <RoomAvatar width={avatarSize} height={avatarSize} room={space} />
{ !isNarrow && <span className="mx_SpaceButton_name">{ space.name }</span> }
{ notifBadge } { notifBadge }
{ this.renderContextMenu() } { this.renderContextMenu() }
</div> </div>
</RovingAccessibleTooltipButton> </RovingAccessibleTooltipButton>
);
} else {
button = (
<RovingAccessibleButton
className={classes}
onClick={this.onClick}
onContextMenu={this.onContextMenu}
role="treeitem"
>
{ toggleCollapseButton }
<div className="mx_SpaceButton_selectionWrapper">
<RoomAvatar width={avatarSize} height={avatarSize} room={space} />
<span className="mx_SpaceButton_name">{ space.name }</span>
{ notifBadge }
{ this.renderContextMenu() }
</div>
</RovingAccessibleButton>
);
}
return (
<li className={itemClasses}>
{ button }
{ childItems } { childItems }
</li> </li>
); );