Merge pull request #6569 from matrix-org/t3chguy/fix/spaces-a11y

This commit is contained in:
Michael Telatynski 2021-08-11 23:18:52 +01:00 committed by GitHub
commit 09ffad96ff
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 357 additions and 217 deletions

View file

@ -269,7 +269,7 @@ limitations under the License.
} }
} }
&:hover { &:hover, &:focus-within {
background-color: $groupFilterPanel-bg-color; background-color: $groupFilterPanel-bg-color;
.mx_AccessibleButton { .mx_AccessibleButton {
@ -278,6 +278,10 @@ limitations under the License.
} }
} }
li.mx_SpaceRoomDirectory_roomTileWrapper {
list-style: none;
}
.mx_SpaceRoomDirectory_roomTile, .mx_SpaceRoomDirectory_roomTile,
.mx_SpaceRoomDirectory_subspace_children { .mx_SpaceRoomDirectory_subspace_children {
&::before { &::before {

View file

@ -150,13 +150,14 @@ const reducer = (state: IState, action: IAction) => {
interface IProps { interface IProps {
handleHomeEnd?: boolean; handleHomeEnd?: boolean;
handleUpDown?: boolean;
children(renderProps: { children(renderProps: {
onKeyDownHandler(ev: React.KeyboardEvent); onKeyDownHandler(ev: React.KeyboardEvent);
}); });
onKeyDown?(ev: React.KeyboardEvent, state: IState); onKeyDown?(ev: React.KeyboardEvent, state: IState);
} }
export const RovingTabIndexProvider: React.FC<IProps> = ({ children, handleHomeEnd, onKeyDown }) => { export const RovingTabIndexProvider: React.FC<IProps> = ({ children, handleHomeEnd, handleUpDown, onKeyDown }) => {
const [state, dispatch] = useReducer<Reducer<IState, IAction>>(reducer, { const [state, dispatch] = useReducer<Reducer<IState, IAction>>(reducer, {
activeRef: null, activeRef: null,
refs: [], refs: [],
@ -167,22 +168,51 @@ export const RovingTabIndexProvider: React.FC<IProps> = ({ children, handleHomeE
const onKeyDownHandler = useCallback((ev) => { const onKeyDownHandler = useCallback((ev) => {
let handled = false; let handled = false;
// Don't interfere with input default keydown behaviour // Don't interfere with input default keydown behaviour
if (handleHomeEnd && ev.target.tagName !== "INPUT" && ev.target.tagName !== "TEXTAREA") { if (ev.target.tagName !== "INPUT" && ev.target.tagName !== "TEXTAREA") {
// check if we actually have any items // check if we actually have any items
switch (ev.key) { switch (ev.key) {
case Key.HOME: case Key.HOME:
if (handleHomeEnd) {
handled = true; handled = true;
// move focus to first item // move focus to first item
if (context.state.refs.length > 0) { if (context.state.refs.length > 0) {
context.state.refs[0].current.focus(); context.state.refs[0].current.focus();
} }
}
break; break;
case Key.END: case Key.END:
if (handleHomeEnd) {
handled = true; handled = true;
// move focus to last item // move focus to last item
if (context.state.refs.length > 0) { if (context.state.refs.length > 0) {
context.state.refs[context.state.refs.length - 1].current.focus(); context.state.refs[context.state.refs.length - 1].current.focus();
} }
}
break;
case Key.ARROW_UP:
if (handleUpDown) {
handled = true;
if (context.state.refs.length > 0) {
const idx = context.state.refs.indexOf(context.state.activeRef);
if (idx > 0) {
context.state.refs[idx - 1].current.focus();
}
}
}
break;
case Key.ARROW_DOWN:
if (handleUpDown) {
handled = true;
if (context.state.refs.length > 0) {
const idx = context.state.refs.indexOf(context.state.activeRef);
if (idx < context.state.refs.length - 1) {
context.state.refs[idx + 1].current.focus();
}
}
}
break; break;
} }
} }
@ -193,7 +223,7 @@ export const RovingTabIndexProvider: React.FC<IProps> = ({ children, handleHomeE
} else if (onKeyDown) { } else if (onKeyDown) {
return onKeyDown(ev, context.state); return onKeyDown(ev, context.state);
} }
}, [context.state, onKeyDown, handleHomeEnd]); }, [context.state, onKeyDown, handleHomeEnd, handleUpDown]);
return <RovingTabIndexContext.Provider value={context}> return <RovingTabIndexContext.Provider value={context}>
{ children({ onKeyDownHandler }) } { children({ onKeyDownHandler }) }

View file

@ -61,7 +61,9 @@ export default class AutoHideScrollbar extends React.Component<IProps> {
style={style} style={style}
className={["mx_AutoHideScrollbar", className].join(" ")} className={["mx_AutoHideScrollbar", className].join(" ")}
onWheel={onWheel} onWheel={onWheel}
tabIndex={tabIndex} // Firefox sometimes makes this element focusable due to
// overflow:scroll;, so force it out of tab order by default.
tabIndex={tabIndex ?? -1}
> >
{ children } { children }
</div>); </div>);

View file

@ -392,9 +392,6 @@ export default class LeftPanel extends React.Component<IProps, IState> {
<IndicatorScrollbar <IndicatorScrollbar
className="mx_LeftPanel_breadcrumbsContainer mx_AutoHideScrollbar" className="mx_LeftPanel_breadcrumbsContainer mx_AutoHideScrollbar"
verticalScrollsHorizontally={true} verticalScrollsHorizontally={true}
// Firefox sometimes makes this element focusable due to
// overflow:scroll;, so force it out of tab order.
tabIndex={-1}
> >
<RoomBreadcrumbs /> <RoomBreadcrumbs />
</IndicatorScrollbar> </IndicatorScrollbar>

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, { ReactNode, useMemo, useState } from "react"; import React, { ReactNode, KeyboardEvent, useMemo, useState, KeyboardEventHandler } from "react";
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import { EventType, RoomType } from "matrix-js-sdk/src/@types/event"; import { EventType, RoomType } from "matrix-js-sdk/src/@types/event";
import { ISpaceSummaryRoom, ISpaceSummaryEvent } from "matrix-js-sdk/src/@types/spaces"; import { ISpaceSummaryRoom, ISpaceSummaryEvent } from "matrix-js-sdk/src/@types/spaces";
@ -46,6 +46,8 @@ import { getDisplayAliasForAliasSet } from "../../Rooms";
import { useDispatcher } from "../../hooks/useDispatcher"; import { useDispatcher } from "../../hooks/useDispatcher";
import defaultDispatcher from "../../dispatcher/dispatcher"; import defaultDispatcher from "../../dispatcher/dispatcher";
import { Action } from "../../dispatcher/actions"; import { Action } from "../../dispatcher/actions";
import { Key } from "../../Keyboard";
import { IState, RovingTabIndexProvider, useRovingTabIndex } from "../../accessibility/RovingTabIndex";
interface IHierarchyProps { interface IHierarchyProps {
space: Room; space: Room;
@ -80,6 +82,7 @@ const Tile: React.FC<ITileProps> = ({
|| (room.room_type === RoomType.Space ? _t("Unnamed Space") : _t("Unnamed Room")); || (room.room_type === RoomType.Space ? _t("Unnamed Space") : _t("Unnamed Room"));
const [showChildren, toggleShowChildren] = useStateToggle(true); const [showChildren, toggleShowChildren] = useStateToggle(true);
const [onFocus, isActive, ref] = useRovingTabIndex();
const onPreviewClick = (ev: ButtonEvent) => { const onPreviewClick = (ev: ButtonEvent) => {
ev.preventDefault(); ev.preventDefault();
@ -94,11 +97,21 @@ const Tile: React.FC<ITileProps> = ({
let button; let button;
if (joinedRoom) { if (joinedRoom) {
button = <AccessibleButton onClick={onPreviewClick} kind="primary_outline"> button = <AccessibleButton
onClick={onPreviewClick}
kind="primary_outline"
onFocus={onFocus}
tabIndex={isActive ? 0 : -1}
>
{ _t("View") } { _t("View") }
</AccessibleButton>; </AccessibleButton>;
} else if (onJoinClick) { } else if (onJoinClick) {
button = <AccessibleButton onClick={onJoinClick} kind="primary"> button = <AccessibleButton
onClick={onJoinClick}
kind="primary"
onFocus={onFocus}
tabIndex={isActive ? 0 : -1}
>
{ _t("Join") } { _t("Join") }
</AccessibleButton>; </AccessibleButton>;
} }
@ -106,13 +119,13 @@ const Tile: React.FC<ITileProps> = ({
let checkbox; let checkbox;
if (onToggleClick) { if (onToggleClick) {
if (hasPermissions) { if (hasPermissions) {
checkbox = <StyledCheckbox checked={!!selected} onChange={onToggleClick} />; checkbox = <StyledCheckbox checked={!!selected} onChange={onToggleClick} tabIndex={isActive ? 0 : -1} />;
} else { } else {
checkbox = <TextWithTooltip checkbox = <TextWithTooltip
tooltip={_t("You don't have permission")} tooltip={_t("You don't have permission")}
onClick={ev => { ev.stopPropagation(); }} onClick={ev => { ev.stopPropagation(); }}
> >
<StyledCheckbox disabled={true} /> <StyledCheckbox disabled={true} tabIndex={isActive ? 0 : -1} />
</TextWithTooltip>; </TextWithTooltip>;
} }
} }
@ -172,8 +185,9 @@ const Tile: React.FC<ITileProps> = ({
</div> </div>
</React.Fragment>; </React.Fragment>;
let childToggle; let childToggle: JSX.Element;
let childSection; let childSection: JSX.Element;
let onKeyDown: KeyboardEventHandler;
if (children) { if (children) {
// the chevron is purposefully a div rather than a button as it should be ignored for a11y // the chevron is purposefully a div rather than a button as it should be ignored for a11y
childToggle = <div childToggle = <div
@ -185,25 +199,74 @@ const Tile: React.FC<ITileProps> = ({
toggleShowChildren(); toggleShowChildren();
}} }}
/>; />;
if (showChildren) { if (showChildren) {
childSection = <div className="mx_SpaceRoomDirectory_subspace_children"> const onChildrenKeyDown = (e) => {
if (e.key === Key.ARROW_LEFT) {
e.preventDefault();
e.stopPropagation();
ref.current?.focus();
}
};
childSection = <div
className="mx_SpaceRoomDirectory_subspace_children"
onKeyDown={onChildrenKeyDown}
role="group"
>
{ children } { children }
</div>; </div>;
} }
onKeyDown = (e) => {
let handled = false;
switch (e.key) {
case Key.ARROW_LEFT:
if (showChildren) {
handled = true;
toggleShowChildren();
}
break;
case Key.ARROW_RIGHT:
handled = true;
if (showChildren) {
const childSection = ref.current?.nextElementSibling;
childSection?.querySelector<HTMLDivElement>(".mx_SpaceRoomDirectory_roomTile")?.focus();
} else {
toggleShowChildren();
}
break;
} }
return <> if (handled) {
e.preventDefault();
e.stopPropagation();
}
};
}
return <li
className="mx_SpaceRoomDirectory_roomTileWrapper"
role="treeitem"
aria-expanded={children ? showChildren : undefined}
>
<AccessibleButton <AccessibleButton
className={classNames("mx_SpaceRoomDirectory_roomTile", { className={classNames("mx_SpaceRoomDirectory_roomTile", {
mx_SpaceRoomDirectory_subspace: room.room_type === RoomType.Space, mx_SpaceRoomDirectory_subspace: room.room_type === RoomType.Space,
})} })}
onClick={(hasPermissions && onToggleClick) ? onToggleClick : onPreviewClick} onClick={(hasPermissions && onToggleClick) ? onToggleClick : onPreviewClick}
onKeyDown={onKeyDown}
inputRef={ref}
onFocus={onFocus}
tabIndex={isActive ? 0 : -1}
> >
{ content } { content }
{ childToggle } { childToggle }
</AccessibleButton> </AccessibleButton>
{ childSection } { childSection }
</>; </li>;
}; };
export const showRoom = (room: ISpaceSummaryRoom, viaServers?: string[], autoJoin = false) => { export const showRoom = (room: ISpaceSummaryRoom, viaServers?: string[], autoJoin = false) => {
@ -414,6 +477,15 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
return <p>{ _t("Your server does not support showing space hierarchies.") }</p>; return <p>{ _t("Your server does not support showing space hierarchies.") }</p>;
} }
const onKeyDown = (ev: KeyboardEvent, state: IState) => {
if (ev.key === Key.ARROW_DOWN && ev.currentTarget.classList.contains("mx_SpaceRoomDirectory_search")) {
state.refs[0]?.current?.focus();
}
};
// TODO loading state/error state
return <RovingTabIndexProvider onKeyDown={onKeyDown} handleHomeEnd handleUpDown>
{ ({ onKeyDownHandler }) => {
let content; let content;
if (roomsMap) { if (roomsMap) {
const numRooms = Array.from(roomsMap.values()).filter(r => !r.room_type).length; const numRooms = Array.from(roomsMap.values()).filter(r => !r.room_type).length;
@ -429,9 +501,13 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
} }
let manageButtons; let manageButtons;
if (space.getMyMembership() === "join" && space.currentState.maySendStateEvent(EventType.SpaceChild, userId)) { if (space.getMyMembership() === "join" &&
space.currentState.maySendStateEvent(EventType.SpaceChild, userId)
) {
const selectedRelations = Array.from(selected.keys()).flatMap(parentId => { const selectedRelations = Array.from(selected.keys()).flatMap(parentId => {
return [...selected.get(parentId).values()].map(childId => [parentId, childId]) as [string, string][]; return [
...selected.get(parentId).values(),
].map(childId => [parentId, childId]) as [string, string][];
}); });
const selectionAllSuggested = selectedRelations.every(([parentId, childId]) => { const selectionAllSuggested = selectedRelations.every(([parentId, childId]) => {
@ -563,7 +639,12 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
{ error && <div className="mx_SpaceRoomDirectory_error"> { error && <div className="mx_SpaceRoomDirectory_error">
{ error } { error }
</div> } </div> }
<AutoHideScrollbar className="mx_SpaceRoomDirectory_list"> <AutoHideScrollbar
className="mx_SpaceRoomDirectory_list"
onKeyDown={onKeyDownHandler}
role="tree"
aria-label={_t("Space")}
>
{ results } { results }
{ children } { children }
</AutoHideScrollbar> </AutoHideScrollbar>
@ -572,18 +653,20 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
content = <Spinner />; content = <Spinner />;
} }
// TODO loading state/error state
return <> return <>
<SearchBox <SearchBox
className="mx_textinput_icon mx_textinput_search" className="mx_SpaceRoomDirectory_search mx_textinput_icon mx_textinput_search"
placeholder={_t("Search names and descriptions")} placeholder={_t("Search names and descriptions")}
onSearch={setQuery} onSearch={setQuery}
autoFocus={true} autoFocus={true}
initialValue={initialText} initialValue={initialText}
onKeyDown={onKeyDownHandler}
/> />
{ content } { content }
</>; </>;
} }
</RovingTabIndexProvider>;
}; };
interface IProps { interface IProps {

View file

@ -67,7 +67,9 @@ export default function AccessibleButton({
...restProps ...restProps
}: IProps) { }: IProps) {
const newProps: IAccessibleButtonProps = restProps; const newProps: IAccessibleButtonProps = restProps;
if (!disabled) { if (disabled) {
newProps["aria-disabled"] = true;
} else {
newProps.onClick = onClick; newProps.onClick = onClick;
// We need to consume enter onKeyDown and space onKeyUp // We need to consume enter onKeyDown and space onKeyUp
// otherwise we are risking also activating other keyboard focusable elements // otherwise we are risking also activating other keyboard focusable elements
@ -118,7 +120,7 @@ export default function AccessibleButton({
); );
// React.createElement expects InputHTMLAttributes // React.createElement expects InputHTMLAttributes
return React.createElement(element, restProps, children); return React.createElement(element, newProps, children);
} }
AccessibleButton.defaultProps = { AccessibleButton.defaultProps = {

View file

@ -18,7 +18,7 @@ limitations under the License.
import React, { ChangeEvent, createRef, CSSProperties, ReactElement, ReactNode, Ref } from 'react'; import React, { ChangeEvent, createRef, CSSProperties, ReactElement, ReactNode, Ref } from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
import AccessibleButton from './AccessibleButton'; import AccessibleButton, { ButtonEvent } from './AccessibleButton';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import { Key } from "../../../Keyboard"; import { Key } from "../../../Keyboard";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
@ -178,7 +178,7 @@ export default class Dropdown extends React.Component<IProps, IState> {
this.ignoreEvent = ev; this.ignoreEvent = ev;
}; };
private onInputClick = (ev: React.MouseEvent) => { private onAccessibleButtonClick = (ev: ButtonEvent) => {
if (this.props.disabled) return; if (this.props.disabled) return;
if (!this.state.expanded) { if (!this.state.expanded) {
@ -186,6 +186,10 @@ export default class Dropdown extends React.Component<IProps, IState> {
expanded: true, expanded: true,
}); });
ev.preventDefault(); ev.preventDefault();
} else if ((ev as React.KeyboardEvent).key === Key.ENTER) {
// the accessible button consumes enter onKeyDown for firing onClick, so handle it here
this.props.onOptionChange(this.state.highlightedOption);
this.close();
} }
}; };
@ -204,7 +208,7 @@ export default class Dropdown extends React.Component<IProps, IState> {
this.props.onOptionChange(dropdownKey); this.props.onOptionChange(dropdownKey);
}; };
private onInputKeyDown = (e: React.KeyboardEvent) => { private onKeyDown = (e: React.KeyboardEvent) => {
let handled = true; let handled = true;
// These keys don't generate keypress events and so needs to be on keyup // These keys don't generate keypress events and so needs to be on keyup
@ -269,7 +273,7 @@ export default class Dropdown extends React.Component<IProps, IState> {
private prevOption(optionKey: string): string { private prevOption(optionKey: string): string {
const keys = Object.keys(this.childrenByKey); const keys = Object.keys(this.childrenByKey);
const index = keys.indexOf(optionKey); const index = keys.indexOf(optionKey);
return keys[(index - 1) % keys.length]; return keys[index <= 0 ? keys.length - 1 : (index - 1) % keys.length];
} }
private scrollIntoView(node: Element) { private scrollIntoView(node: Element) {
@ -320,7 +324,6 @@ export default class Dropdown extends React.Component<IProps, IState> {
type="text" type="text"
autoFocus={true} autoFocus={true}
className="mx_Dropdown_option" className="mx_Dropdown_option"
onKeyDown={this.onInputKeyDown}
onChange={this.onInputChange} onChange={this.onInputChange}
value={this.state.searchQuery} value={this.state.searchQuery}
role="combobox" role="combobox"
@ -329,6 +332,7 @@ export default class Dropdown extends React.Component<IProps, IState> {
aria-owns={`${this.props.id}_listbox`} aria-owns={`${this.props.id}_listbox`}
aria-disabled={this.props.disabled} aria-disabled={this.props.disabled}
aria-label={this.props.label} aria-label={this.props.label}
onKeyDown={this.onKeyDown}
/> />
); );
} }
@ -361,13 +365,14 @@ export default class Dropdown extends React.Component<IProps, IState> {
return <div className={classnames(dropdownClasses)} ref={this.collectRoot}> return <div className={classnames(dropdownClasses)} ref={this.collectRoot}>
<AccessibleButton <AccessibleButton
className="mx_Dropdown_input mx_no_textinput" className="mx_Dropdown_input mx_no_textinput"
onClick={this.onInputClick} onClick={this.onAccessibleButtonClick}
aria-haspopup="listbox" aria-haspopup="listbox"
aria-expanded={this.state.expanded} aria-expanded={this.state.expanded}
disabled={this.props.disabled} disabled={this.props.disabled}
inputRef={this.buttonRef} inputRef={this.buttonRef}
aria-label={this.props.label} aria-label={this.props.label}
aria-describedby={`${this.props.id}_value`} aria-describedby={`${this.props.id}_value`}
onKeyDown={this.onKeyDown}
> >
{ currentValue } { currentValue }
<span className="mx_Dropdown_arrow" /> <span className="mx_Dropdown_arrow" />

View file

@ -65,6 +65,7 @@ export const SpaceAvatar = ({
}} }}
kind="link" kind="link"
className="mx_SpaceBasicSettings_avatar_remove" className="mx_SpaceBasicSettings_avatar_remove"
aria-label={_t("Delete avatar")}
> >
{ _t("Delete") } { _t("Delete") }
</AccessibleButton> </AccessibleButton>
@ -72,7 +73,11 @@ export const SpaceAvatar = ({
} else { } else {
avatarSection = <React.Fragment> avatarSection = <React.Fragment>
<div className="mx_SpaceBasicSettings_avatar" onClick={() => avatarUploadRef.current?.click()} /> <div className="mx_SpaceBasicSettings_avatar" onClick={() => avatarUploadRef.current?.click()} />
<AccessibleButton onClick={() => avatarUploadRef.current?.click()} kind="link"> <AccessibleButton
onClick={() => avatarUploadRef.current?.click()}
kind="link"
aria-label={_t("Upload avatar")}
>
{ _t("Upload") } { _t("Upload") }
</AccessibleButton> </AccessibleButton>
</React.Fragment>; </React.Fragment>;

View file

@ -100,9 +100,12 @@ const HomeButton = ({ selected, isPanelCollapsed }: IHomeButtonProps) => {
return SpaceStore.instance.allRoomsInHome; return SpaceStore.instance.allRoomsInHome;
}); });
return <li className={classNames("mx_SpaceItem", { return <li
className={classNames("mx_SpaceItem", {
"collapsed": isPanelCollapsed, "collapsed": isPanelCollapsed,
})}> })}
role="treeitem"
>
<SpaceButton <SpaceButton
className="mx_SpaceButton_home" className="mx_SpaceButton_home"
onClick={() => SpaceStore.instance.setActiveSpace(null)} onClick={() => SpaceStore.instance.setActiveSpace(null)}
@ -142,9 +145,12 @@ const CreateSpaceButton = ({
openMenu(); openMenu();
}; };
return <li className={classNames("mx_SpaceItem", { return <li
className={classNames("mx_SpaceItem", {
"collapsed": isPanelCollapsed, "collapsed": isPanelCollapsed,
})}> })}
role="treeitem"
>
<SpaceButton <SpaceButton
className={classNames("mx_SpaceButton_new", { className={classNames("mx_SpaceButton_new", {
mx_SpaceButton_newCancel: menuDisplayed, mx_SpaceButton_newCancel: menuDisplayed,
@ -272,6 +278,8 @@ const SpacePanel = () => {
<ul <ul
className={classNames("mx_SpacePanel", { collapsed: isPanelCollapsed })} className={classNames("mx_SpacePanel", { collapsed: isPanelCollapsed })}
onKeyDown={onKeyDownHandler} onKeyDown={onKeyDownHandler}
role="tree"
aria-label={_t("Spaces")}
> >
<Droppable droppableId="top-level-spaces"> <Droppable droppableId="top-level-spaces">
{ (provided, snapshot) => ( { (provided, snapshot) => (

View file

@ -77,11 +77,17 @@ export const SpaceButton: React.FC<IButtonProps> = ({
let notifBadge; let notifBadge;
if (notificationState) { if (notificationState) {
let ariaLabel = _t("Jump to first unread room.");
if (space?.getMyMembership() === "invite") {
ariaLabel = _t("Jump to first invite.");
}
notifBadge = <div className="mx_SpacePanel_badgeContainer"> notifBadge = <div className="mx_SpacePanel_badgeContainer">
<NotificationBadge <NotificationBadge
onClick={() => SpaceStore.instance.setActiveRoomInSpace(space || null)} onClick={() => SpaceStore.instance.setActiveRoomInSpace(space || null)}
forceCount={false} forceCount={false}
notification={notificationState} notification={notificationState}
aria-label={ariaLabel}
/> />
</div>; </div>;
} }
@ -107,7 +113,6 @@ export const SpaceButton: React.FC<IButtonProps> = ({
onClick={onClick} onClick={onClick}
onContextMenu={openMenu} onContextMenu={openMenu}
forceHide={!isNarrow || menuDisplayed} forceHide={!isNarrow || menuDisplayed}
role="treeitem"
inputRef={handle} inputRef={handle}
> >
{ children } { children }
@ -284,7 +289,7 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
/> : null; /> : null;
return ( return (
<li {...otherProps} className={itemClasses} ref={innerRef}> <li {...otherProps} className={itemClasses} ref={innerRef} aria-expanded={!collapsed} role="treeitem">
<SpaceButton <SpaceButton
space={space} space={space}
className={isInvite ? "mx_SpaceButton_invite" : undefined} className={isInvite ? "mx_SpaceButton_invite" : undefined}
@ -296,9 +301,7 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
avatarSize={isNested ? 24 : 32} avatarSize={isNested ? 24 : 32}
onClick={this.onClick} onClick={this.onClick}
onKeyDown={this.onKeyDown} onKeyDown={this.onKeyDown}
aria-expanded={!collapsed} ContextMenuComponent={this.props.space.getMyMembership() === "join" ? SpaceContextMenu : undefined}
ContextMenuComponent={this.props.space.getMyMembership() === "join"
? SpaceContextMenu : undefined}
> >
{ toggleCollapseButton } { toggleCollapseButton }
</SpaceButton> </SpaceButton>
@ -322,7 +325,7 @@ const SpaceTreeLevel: React.FC<ITreeLevelProps> = ({
isNested, isNested,
parents, parents,
}) => { }) => {
return <ul className="mx_SpaceTreeLevel"> return <ul className="mx_SpaceTreeLevel" role="group">
{ spaces.map(s => { { spaces.map(s => {
return (<SpaceItem return (<SpaceItem
key={s.roomId} key={s.roomId}

View file

@ -1015,7 +1015,9 @@
"Your server isn't responding to some <a>requests</a>.": "Your server isn't responding to some <a>requests</a>.", "Your server isn't responding to some <a>requests</a>.": "Your server isn't responding to some <a>requests</a>.",
"Decline (%(counter)s)": "Decline (%(counter)s)", "Decline (%(counter)s)": "Decline (%(counter)s)",
"Accept <policyLink /> to continue:": "Accept <policyLink /> to continue:", "Accept <policyLink /> to continue:": "Accept <policyLink /> to continue:",
"Delete avatar": "Delete avatar",
"Delete": "Delete", "Delete": "Delete",
"Upload avatar": "Upload avatar",
"Upload": "Upload", "Upload": "Upload",
"Name": "Name", "Name": "Name",
"Description": "Description", "Description": "Description",
@ -1073,6 +1075,8 @@
"Preview Space": "Preview Space", "Preview Space": "Preview Space",
"Allow people to preview your space before they join.": "Allow people to preview your space before they join.", "Allow people to preview your space before they join.": "Allow people to preview your space before they join.",
"Recommended for public spaces.": "Recommended for public spaces.", "Recommended for public spaces.": "Recommended for public spaces.",
"Jump to first unread room.": "Jump to first unread room.",
"Jump to first invite.": "Jump to first invite.",
"Expand": "Expand", "Expand": "Expand",
"Collapse": "Collapse", "Collapse": "Collapse",
"Space options": "Space options", "Space options": "Space options",
@ -1667,8 +1671,6 @@
"Activity": "Activity", "Activity": "Activity",
"A-Z": "A-Z", "A-Z": "A-Z",
"List options": "List options", "List options": "List options",
"Jump to first unread room.": "Jump to first unread room.",
"Jump to first invite.": "Jump to first invite.",
"Show %(count)s more|other": "Show %(count)s more", "Show %(count)s more|other": "Show %(count)s more",
"Show %(count)s more|one": "Show %(count)s more", "Show %(count)s more|one": "Show %(count)s more",
"Show less": "Show less", "Show less": "Show less",
@ -2721,7 +2723,6 @@
"Everyone": "Everyone", "Everyone": "Everyone",
"Your community hasn't got a Long Description, a HTML page to show to community members.<br />Click here to open settings and give it one!": "Your community hasn't got a Long Description, a HTML page to show to community members.<br />Click here to open settings and give it one!", "Your community hasn't got a Long Description, a HTML page to show to community members.<br />Click here to open settings and give it one!": "Your community hasn't got a Long Description, a HTML page to show to community members.<br />Click here to open settings and give it one!",
"Long Description (HTML)": "Long Description (HTML)", "Long Description (HTML)": "Long Description (HTML)",
"Upload avatar": "Upload avatar",
"Community %(groupId)s not found": "Community %(groupId)s not found", "Community %(groupId)s not found": "Community %(groupId)s not found",
"This homeserver does not support communities": "This homeserver does not support communities", "This homeserver does not support communities": "This homeserver does not support communities",
"Failed to load %(groupId)s": "Failed to load %(groupId)s", "Failed to load %(groupId)s": "Failed to load %(groupId)s",
@ -2831,6 +2832,7 @@
"Mark as suggested": "Mark as suggested", "Mark as suggested": "Mark as suggested",
"No results found": "No results found", "No results found": "No results found",
"You may want to try a different search or check for typos.": "You may want to try a different search or check for typos.", "You may want to try a different search or check for typos.": "You may want to try a different search or check for typos.",
"Space": "Space",
"Search names and descriptions": "Search names and descriptions", "Search names and descriptions": "Search names and descriptions",
"If you can't find the room you're looking for, ask for an invite or <a>create a new room</a>.": "If you can't find the room you're looking for, ask for an invite or <a>create a new room</a>.", "If you can't find the room you're looking for, ask for an invite or <a>create a new room</a>.": "If you can't find the room you're looking for, ask for an invite or <a>create a new room</a>.",
"Create room": "Create room", "Create room": "Create room",
@ -3112,7 +3114,6 @@
"Page Down": "Page Down", "Page Down": "Page Down",
"Esc": "Esc", "Esc": "Esc",
"Enter": "Enter", "Enter": "Enter",
"Space": "Space",
"End": "End", "End": "End",
"[number]": "[number]" "[number]": "[number]"
} }