Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit d5581f0

Browse files
authored
Merge pull request #6239 from matrix-org/t3chguy/fix/17605
2 parents 50a5c03 + 9f20d66 commit d5581f0

File tree

3 files changed

+91
-40
lines changed

3 files changed

+91
-40
lines changed

src/components/views/elements/AccessibleButton.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ export default function AccessibleButton({
6262
disabled,
6363
inputRef,
6464
className,
65+
onKeyDown,
66+
onKeyUp,
6567
...restProps
6668
}: IProps) {
6769
const newProps: IAccessibleButtonProps = restProps;
@@ -83,6 +85,8 @@ export default function AccessibleButton({
8385
if (e.key === Key.SPACE) {
8486
e.stopPropagation();
8587
e.preventDefault();
88+
} else {
89+
onKeyDown?.(e);
8690
}
8791
};
8892
newProps.onKeyUp = (e) => {
@@ -94,6 +98,8 @@ export default function AccessibleButton({
9498
if (e.key === Key.ENTER) {
9599
e.stopPropagation();
96100
e.preventDefault();
101+
} else {
102+
onKeyUp?.(e);
97103
}
98104
};
99105
}

src/components/views/spaces/SpaceTreeLevel.tsx

Lines changed: 83 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,14 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
import React, { InputHTMLAttributes, LegacyRef } from "react";
17+
import React, { createRef, InputHTMLAttributes, LegacyRef } from "react";
1818
import classNames from "classnames";
1919
import { Room } from "matrix-js-sdk/src/models/room";
2020

2121
import RoomAvatar from "../avatars/RoomAvatar";
2222
import SpaceStore from "../../../stores/SpaceStore";
2323
import SpaceTreeLevelLayoutStore from "../../../stores/SpaceTreeLevelLayoutStore";
2424
import NotificationBadge from "../rooms/NotificationBadge";
25-
import { RovingAccessibleButton } from "../../../accessibility/roving/RovingAccessibleButton";
2625
import { RovingAccessibleTooltipButton } from "../../../accessibility/roving/RovingAccessibleTooltipButton";
2726
import IconizedContextMenu, {
2827
IconizedContextMenuOption,
@@ -48,6 +47,7 @@ import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
4847
import { EventType } from "matrix-js-sdk/src/@types/event";
4948
import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState";
5049
import { NotificationColor } from "../../../stores/notifications/NotificationColor";
50+
import { getKeyBindingsManager, RoomListAction } from "../../../KeyBindingsManager";
5151

5252
interface IItemProps extends InputHTMLAttributes<HTMLLIElement> {
5353
space?: Room;
@@ -62,11 +62,14 @@ interface IItemProps extends InputHTMLAttributes<HTMLLIElement> {
6262
interface IItemState {
6363
collapsed: boolean;
6464
contextMenuPosition: Pick<DOMRect, "right" | "top" | "height">;
65+
childSpaces: Room[];
6566
}
6667

6768
export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
6869
static contextType = MatrixClientContext;
6970

71+
private buttonRef = createRef<HTMLDivElement>();
72+
7073
constructor(props) {
7174
super(props);
7275

@@ -79,14 +82,36 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
7982
this.state = {
8083
collapsed: collapsed,
8184
contextMenuPosition: null,
85+
childSpaces: this.childSpaces,
8286
};
87+
88+
SpaceStore.instance.on(this.props.space.roomId, this.onSpaceUpdate);
89+
}
90+
91+
componentWillUnmount() {
92+
SpaceStore.instance.off(this.props.space.roomId, this.onSpaceUpdate);
93+
}
94+
95+
private onSpaceUpdate = () => {
96+
this.setState({
97+
childSpaces: this.childSpaces,
98+
});
99+
};
100+
101+
private get childSpaces() {
102+
return SpaceStore.instance.getChildSpaces(this.props.space.roomId)
103+
.filter(s => !this.props.parents?.has(s.roomId));
104+
}
105+
106+
private get isCollapsed() {
107+
return this.state.collapsed || this.props.isPanelCollapsed;
83108
}
84109

85-
private toggleCollapse(evt) {
86-
if (this.props.onExpand && this.state.collapsed) {
110+
private toggleCollapse = evt => {
111+
if (this.props.onExpand && this.isCollapsed) {
87112
this.props.onExpand();
88113
}
89-
const newCollapsedState = !this.state.collapsed;
114+
const newCollapsedState = !this.isCollapsed;
90115

91116
SpaceTreeLevelLayoutStore.instance.setSpaceCollapsedState(
92117
this.props.space.roomId,
@@ -97,7 +122,7 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
97122
// don't bubble up so encapsulating button for space
98123
// doesn't get triggered
99124
evt.stopPropagation();
100-
}
125+
};
101126

102127
private onContextMenu = (ev: React.MouseEvent) => {
103128
if (this.props.space.getMyMembership() !== "join") return;
@@ -112,6 +137,43 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
112137
});
113138
}
114139

140+
private onKeyDown = (ev: React.KeyboardEvent) => {
141+
let handled = true;
142+
const action = getKeyBindingsManager().getRoomListAction(ev);
143+
const hasChildren = this.state.childSpaces?.length;
144+
switch (action) {
145+
case RoomListAction.CollapseSection:
146+
if (hasChildren && !this.isCollapsed) {
147+
this.toggleCollapse(ev);
148+
} else {
149+
const parentItem = this.buttonRef?.current?.parentElement?.parentElement;
150+
const parentButton = parentItem?.previousElementSibling as HTMLElement;
151+
parentButton?.focus();
152+
}
153+
break;
154+
155+
case RoomListAction.ExpandSection:
156+
if (hasChildren) {
157+
if (this.isCollapsed) {
158+
this.toggleCollapse(ev);
159+
} else {
160+
const childLevel = this.buttonRef?.current?.nextElementSibling;
161+
const firstSpaceItemChild = childLevel?.querySelector<HTMLLIElement>(".mx_SpaceItem");
162+
firstSpaceItemChild?.querySelector<HTMLDivElement>(".mx_SpaceButton")?.focus();
163+
}
164+
}
165+
break;
166+
167+
default:
168+
handled = false;
169+
}
170+
171+
if (handled) {
172+
ev.stopPropagation();
173+
ev.preventDefault();
174+
}
175+
};
176+
115177
private onClick = (ev: React.MouseEvent) => {
116178
ev.preventDefault();
117179
ev.stopPropagation();
@@ -305,16 +367,14 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
305367
const { space, activeSpaces, isNested, isPanelCollapsed, onExpand, parents, innerRef,
306368
...otherProps } = this.props;
307369

308-
const collapsed = this.state.collapsed || isPanelCollapsed;
370+
const collapsed = this.isCollapsed;
309371

310-
const childSpaces = SpaceStore.instance.getChildSpaces(space.roomId)
311-
.filter(s => !parents?.has(s.roomId));
312372
const isActive = activeSpaces.includes(space);
313373
const itemClasses = classNames(this.props.className, {
314374
"mx_SpaceItem": true,
315375
"mx_SpaceItem_narrow": isPanelCollapsed,
316376
"collapsed": collapsed,
317-
"hasSubSpaces": childSpaces && childSpaces.length,
377+
"hasSubSpaces": this.state.childSpaces?.length,
318378
});
319379

320380
const isInvite = space.getMyMembership() === "invite";
@@ -329,9 +389,9 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
329389
: SpaceStore.instance.getNotificationState(space.roomId);
330390

331391
let childItems;
332-
if (childSpaces && !collapsed) {
392+
if (this.state.childSpaces?.length && !collapsed) {
333393
childItems = <SpaceTreeLevel
334-
spaces={childSpaces}
394+
spaces={this.state.childSpaces}
335395
activeSpaces={activeSpaces}
336396
isNested={true}
337397
parents={new Set(parents).add(space.roomId)}
@@ -347,53 +407,36 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
347407

348408
const avatarSize = isNested ? 24 : 32;
349409

350-
const toggleCollapseButton = childSpaces && childSpaces.length ?
410+
const toggleCollapseButton = this.state.childSpaces?.length ?
351411
<AccessibleButton
352412
className="mx_SpaceButton_toggleCollapse"
353-
onClick={evt => this.toggleCollapse(evt)}
413+
onClick={this.toggleCollapse}
414+
tabIndex={-1}
415+
aria-label={collapsed ? _t("Expand") : _t("Collapse")}
354416
/> : null;
355417

356-
let button;
357-
if (isPanelCollapsed) {
358-
button = (
418+
return (
419+
<li {...otherProps} className={itemClasses} ref={innerRef}>
359420
<RovingAccessibleTooltipButton
360421
className={classes}
361422
title={space.name}
362423
onClick={this.onClick}
363424
onContextMenu={this.onContextMenu}
364-
forceHide={!!this.state.contextMenuPosition}
425+
forceHide={!isPanelCollapsed || !!this.state.contextMenuPosition}
365426
role="treeitem"
427+
aria-expanded={!collapsed}
428+
inputRef={this.buttonRef}
429+
onKeyDown={this.onKeyDown}
366430
>
367431
{ toggleCollapseButton }
368432
<div className="mx_SpaceButton_selectionWrapper">
369433
<RoomAvatar width={avatarSize} height={avatarSize} room={space} />
434+
{ !isPanelCollapsed && <span className="mx_SpaceButton_name">{ space.name }</span> }
370435
{ notifBadge }
371436
{ this.renderContextMenu() }
372437
</div>
373438
</RovingAccessibleTooltipButton>
374-
);
375-
} else {
376-
button = (
377-
<RovingAccessibleButton
378-
className={classes}
379-
onClick={this.onClick}
380-
onContextMenu={this.onContextMenu}
381-
role="treeitem"
382-
>
383-
{ toggleCollapseButton }
384-
<div className="mx_SpaceButton_selectionWrapper">
385-
<RoomAvatar width={avatarSize} height={avatarSize} room={space} />
386-
<span className="mx_SpaceButton_name">{ space.name }</span>
387-
{ notifBadge }
388-
{ this.renderContextMenu() }
389-
</div>
390-
</RovingAccessibleButton>
391-
);
392-
}
393439

394-
return (
395-
<li {...otherProps} className={itemClasses} ref={innerRef}>
396-
{ button }
397440
{ childItems }
398441
</li>
399442
);

src/i18n/strings/en_EN.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1067,6 +1067,8 @@
10671067
"Manage & explore rooms": "Manage & explore rooms",
10681068
"Explore rooms": "Explore rooms",
10691069
"Space options": "Space options",
1070+
"Expand": "Expand",
1071+
"Collapse": "Collapse",
10701072
"Remove": "Remove",
10711073
"This bridge was provisioned by <user />.": "This bridge was provisioned by <user />.",
10721074
"This bridge is managed by <user />.": "This bridge is managed by <user />.",

0 commit comments

Comments
 (0)