Fix keyboard accessibility of the space panel
This commit is contained in:
parent
9fa7ac21bd
commit
66b3feb802
2 changed files with 98 additions and 53 deletions
|
@ -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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,23 +14,22 @@ 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";
|
||||||
|
|
||||||
import RoomAvatar from "../avatars/RoomAvatar";
|
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,
|
||||||
IconizedContextMenuOptionList,
|
IconizedContextMenuOptionList,
|
||||||
} from "../context_menus/IconizedContextMenu";
|
} from "../context_menus/IconizedContextMenu";
|
||||||
import {_t} from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import {ContextMenuTooltipButton} from "../../../accessibility/context_menu/ContextMenuTooltipButton";
|
import { ContextMenuTooltipButton } from "../../../accessibility/context_menu/ContextMenuTooltipButton";
|
||||||
import {toRightOf} from "../../structures/ContextMenu";
|
import { toRightOf } from "../../structures/ContextMenu";
|
||||||
import {
|
import {
|
||||||
shouldShowSpaceSettings,
|
shouldShowSpaceSettings,
|
||||||
showAddExistingRooms,
|
showAddExistingRooms,
|
||||||
|
@ -39,15 +38,16 @@ import {
|
||||||
showSpaceSettings,
|
showSpaceSettings,
|
||||||
} from "../../../utils/space";
|
} from "../../../utils/space";
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import AccessibleButton, {ButtonEvent} from "../elements/AccessibleButton";
|
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
|
||||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||||
import {Action} from "../../../dispatcher/actions";
|
import { Action } from "../../../dispatcher/actions";
|
||||||
import RoomViewStore from "../../../stores/RoomViewStore";
|
import RoomViewStore from "../../../stores/RoomViewStore";
|
||||||
import {SetRightPanelPhasePayload} from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
|
import { SetRightPanelPhasePayload } from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
|
||||||
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
|
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>
|
||||||
);
|
);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue