Merge pull request #6569 from matrix-org/t3chguy/fix/spaces-a11y
This commit is contained in:
commit
09ffad96ff
11 changed files with 357 additions and 217 deletions
|
@ -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 {
|
||||||
|
|
|
@ -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 }) }
|
||||||
|
|
|
@ -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>);
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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" />
|
||||||
|
|
|
@ -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>;
|
||||||
|
|
|
@ -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) => (
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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]"
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue