mirror of
https://github.com/element-hq/element-web
synced 2024-11-23 09:46:09 +03:00
First step towards a11y in the new room list
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
parent
0549ef83db
commit
289f40ce29
6 changed files with 158 additions and 19 deletions
|
@ -278,6 +278,7 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_RoomSublist2_hasMenuOpen,
|
&.mx_RoomSublist2_hasMenuOpen,
|
||||||
|
&:not(.mx_RoomSublist2_minimized) > .mx_RoomSublist2_headerContainer:focus-within,
|
||||||
&:not(.mx_RoomSublist2_minimized) > .mx_RoomSublist2_headerContainer:hover {
|
&:not(.mx_RoomSublist2_minimized) > .mx_RoomSublist2_headerContainer:hover {
|
||||||
.mx_RoomSublist2_menuButton {
|
.mx_RoomSublist2_menuButton {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
|
|
|
@ -24,7 +24,10 @@ limitations under the License.
|
||||||
// The tile is also a flexbox row itself
|
// The tile is also a flexbox row itself
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
&.mx_RoomTile2_selected, &:hover, &.mx_RoomTile2_hasMenuOpen {
|
&.mx_RoomTile2_selected,
|
||||||
|
&:hover,
|
||||||
|
&:focus-within,
|
||||||
|
&.mx_RoomTile2_hasMenuOpen {
|
||||||
background-color: $roomtile2-selected-bg-color;
|
background-color: $roomtile2-selected-bg-color;
|
||||||
border-radius: 32px;
|
border-radius: 32px;
|
||||||
}
|
}
|
||||||
|
@ -132,7 +135,9 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(.mx_RoomTile2_minimized) {
|
&:not(.mx_RoomTile2_minimized) {
|
||||||
&:hover, &.mx_RoomTile2_hasMenuOpen {
|
&:hover,
|
||||||
|
&:focus-within,
|
||||||
|
&.mx_RoomTile2_hasMenuOpen {
|
||||||
// Hide the badge container on hover because it'll be a menu button
|
// Hide the badge container on hover because it'll be a menu button
|
||||||
.mx_RoomTile2_badgeContainer {
|
.mx_RoomTile2_badgeContainer {
|
||||||
width: 0;
|
width: 0;
|
||||||
|
|
|
@ -30,7 +30,8 @@ import { BreadcrumbsStore } from "../../stores/BreadcrumbsStore";
|
||||||
import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
||||||
import ResizeNotifier from "../../utils/ResizeNotifier";
|
import ResizeNotifier from "../../utils/ResizeNotifier";
|
||||||
import SettingsStore from "../../settings/SettingsStore";
|
import SettingsStore from "../../settings/SettingsStore";
|
||||||
import RoomListStore, { RoomListStore2, LISTS_UPDATE_EVENT } from "../../stores/room-list/RoomListStore2";
|
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../stores/room-list/RoomListStore2";
|
||||||
|
import {Key} from "../../Keyboard";
|
||||||
|
|
||||||
// TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231
|
// TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231
|
||||||
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
|
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
|
||||||
|
@ -57,6 +58,7 @@ interface IState {
|
||||||
export default class LeftPanel2 extends React.Component<IProps, IState> {
|
export default class LeftPanel2 extends React.Component<IProps, IState> {
|
||||||
private listContainerRef: React.RefObject<HTMLDivElement> = createRef();
|
private listContainerRef: React.RefObject<HTMLDivElement> = createRef();
|
||||||
private tagPanelWatcherRef: string;
|
private tagPanelWatcherRef: string;
|
||||||
|
private focusedElement = null;
|
||||||
|
|
||||||
// TODO: a11y: https://github.com/vector-im/riot-web/issues/14180
|
// TODO: a11y: https://github.com/vector-im/riot-web/issues/14180
|
||||||
|
|
||||||
|
@ -150,6 +152,77 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
|
||||||
this.handleStickyHeaders(this.listContainerRef.current);
|
this.handleStickyHeaders(this.listContainerRef.current);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private onFocus = (ev: React.FocusEvent) => {
|
||||||
|
this.focusedElement = ev.target;
|
||||||
|
};
|
||||||
|
|
||||||
|
private onBlur = () => {
|
||||||
|
this.focusedElement = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
private onKeyDown = (ev: React.KeyboardEvent) => {
|
||||||
|
if (!this.focusedElement) return;
|
||||||
|
|
||||||
|
switch (ev.key) {
|
||||||
|
case Key.ARROW_UP:
|
||||||
|
case Key.ARROW_DOWN:
|
||||||
|
this.onMoveFocus(ev, ev.key === Key.ARROW_UP);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private onMoveFocus = (ev: React.KeyboardEvent, up: boolean) => {
|
||||||
|
let element = this.focusedElement;
|
||||||
|
|
||||||
|
// unclear why this isn't needed
|
||||||
|
// var descending = (up == this.focusDirection) ? this.focusDescending : !this.focusDescending;
|
||||||
|
// this.focusDirection = up;
|
||||||
|
|
||||||
|
let descending = false; // are we currently descending or ascending through the DOM tree?
|
||||||
|
let classes;
|
||||||
|
|
||||||
|
do {
|
||||||
|
const child = up ? element.lastElementChild : element.firstElementChild;
|
||||||
|
const sibling = up ? element.previousElementSibling : element.nextElementSibling;
|
||||||
|
|
||||||
|
if (descending) {
|
||||||
|
if (child) {
|
||||||
|
element = child;
|
||||||
|
} else if (sibling) {
|
||||||
|
element = sibling;
|
||||||
|
} else {
|
||||||
|
descending = false;
|
||||||
|
element = element.parentElement;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (sibling) {
|
||||||
|
element = sibling;
|
||||||
|
descending = true;
|
||||||
|
} else {
|
||||||
|
element = element.parentElement;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element) {
|
||||||
|
classes = element.classList;
|
||||||
|
}
|
||||||
|
} while (element && !(
|
||||||
|
classes.contains("mx_RoomTile2") ||
|
||||||
|
classes.contains("mx_RoomSublist2_headerText") ||
|
||||||
|
classes.contains("mx_RoomSearch_input")));
|
||||||
|
|
||||||
|
if (element) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
ev.preventDefault();
|
||||||
|
element.focus();
|
||||||
|
this.focusedElement = element;
|
||||||
|
} else {
|
||||||
|
// if navigation is via up/down arrow-keys, trap in the widget so it doesn't send to composer
|
||||||
|
ev.stopPropagation();
|
||||||
|
ev.preventDefault();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
private renderHeader(): React.ReactNode {
|
private renderHeader(): React.ReactNode {
|
||||||
let breadcrumbs;
|
let breadcrumbs;
|
||||||
if (this.state.showBreadcrumbs) {
|
if (this.state.showBreadcrumbs) {
|
||||||
|
@ -170,8 +243,12 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
private renderSearchExplore(): React.ReactNode {
|
private renderSearchExplore(): React.ReactNode {
|
||||||
return (
|
return (
|
||||||
<div className="mx_LeftPanel2_filterContainer">
|
<div className="mx_LeftPanel2_filterContainer" onFocus={this.onFocus} onBlur={this.onBlur}>
|
||||||
<RoomSearch onQueryUpdate={this.onSearch} isMinimized={this.props.isMinimized} />
|
<RoomSearch
|
||||||
|
onQueryUpdate={this.onSearch}
|
||||||
|
isMinimized={this.props.isMinimized}
|
||||||
|
onVerticalArrow={this.onKeyDown}
|
||||||
|
/>
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
className='mx_LeftPanel2_exploreButton'
|
className='mx_LeftPanel2_exploreButton'
|
||||||
|
@ -189,14 +266,13 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
// TODO: Determine what these onWhatever handlers do: https://github.com/vector-im/riot-web/issues/14180
|
|
||||||
const roomList = <RoomList2
|
const roomList = <RoomList2
|
||||||
onKeyDown={() => {/*TODO*/}}
|
onKeyDown={this.onKeyDown}
|
||||||
resizeNotifier={null}
|
resizeNotifier={null}
|
||||||
collapsed={false}
|
collapsed={false}
|
||||||
searchFilter={this.state.searchFilter}
|
searchFilter={this.state.searchFilter}
|
||||||
onFocus={() => {/*TODO*/}}
|
onFocus={this.onFocus}
|
||||||
onBlur={() => {/*TODO*/}}
|
onBlur={this.onBlur}
|
||||||
isMinimized={this.props.isMinimized}
|
isMinimized={this.props.isMinimized}
|
||||||
/>;
|
/>;
|
||||||
|
|
||||||
|
@ -223,7 +299,12 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
|
||||||
className={roomListClasses}
|
className={roomListClasses}
|
||||||
onScroll={this.onScroll}
|
onScroll={this.onScroll}
|
||||||
ref={this.listContainerRef}
|
ref={this.listContainerRef}
|
||||||
>{roomList}</div>
|
// Firefox sometimes makes this element focusable due to
|
||||||
|
// overflow:scroll;, so force it out of tab order.
|
||||||
|
tabIndex={-1}
|
||||||
|
>
|
||||||
|
{roomList}
|
||||||
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -38,6 +38,7 @@ import { Action } from "../../dispatcher/actions";
|
||||||
interface IProps {
|
interface IProps {
|
||||||
onQueryUpdate: (newQuery: string) => void;
|
onQueryUpdate: (newQuery: string) => void;
|
||||||
isMinimized: boolean;
|
isMinimized: boolean;
|
||||||
|
onVerticalArrow(ev: React.KeyboardEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
|
@ -111,6 +112,8 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
||||||
if (ev.key === Key.ESCAPE) {
|
if (ev.key === Key.ESCAPE) {
|
||||||
this.clearInput();
|
this.clearInput();
|
||||||
defaultDispatcher.fire(Action.FocusComposer);
|
defaultDispatcher.fire(Action.FocusComposer);
|
||||||
|
} else if (ev.key === Key.ARROW_UP || ev.key === Key.ARROW_DOWN) {
|
||||||
|
this.props.onVerticalArrow(ev);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -35,6 +35,7 @@ import { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
||||||
import dis from "../../../dispatcher/dispatcher";
|
import dis from "../../../dispatcher/dispatcher";
|
||||||
import NotificationBadge from "./NotificationBadge";
|
import NotificationBadge from "./NotificationBadge";
|
||||||
import { ListNotificationState } from "../../../stores/notifications/ListNotificationState";
|
import { ListNotificationState } from "../../../stores/notifications/ListNotificationState";
|
||||||
|
import { Key } from "../../../Keyboard";
|
||||||
|
|
||||||
// TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231
|
// TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231
|
||||||
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
|
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
|
||||||
|
@ -80,6 +81,9 @@ interface IState {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class RoomSublist2 extends React.Component<IProps, IState> {
|
export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||||
|
private headerButton = createRef<HTMLDivElement>();
|
||||||
|
private sublistRef = createRef<HTMLDivElement>();
|
||||||
|
|
||||||
constructor(props: IProps) {
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
|
@ -215,8 +219,52 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||||
sublist.scrollIntoView({behavior: 'smooth'});
|
sublist.scrollIntoView({behavior: 'smooth'});
|
||||||
} else {
|
} else {
|
||||||
// on screen - toggle collapse
|
// on screen - toggle collapse
|
||||||
|
this.toggleCollapsed();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private toggleCollapsed = () => {
|
||||||
this.props.layout.isCollapsed = !this.props.layout.isCollapsed;
|
this.props.layout.isCollapsed = !this.props.layout.isCollapsed;
|
||||||
this.forceUpdate(); // because the layout doesn't trigger an update
|
this.forceUpdate(); // because the layout doesn't trigger an update
|
||||||
|
};
|
||||||
|
|
||||||
|
private onHeaderKeyDown = (ev: React.KeyboardEvent) => {
|
||||||
|
const isCollapsed = this.props.layout && this.props.layout.isCollapsed;
|
||||||
|
switch (ev.key) {
|
||||||
|
case Key.ARROW_LEFT:
|
||||||
|
ev.stopPropagation();
|
||||||
|
if (!isCollapsed) {
|
||||||
|
// On ARROW_LEFT collapse the room sublist if it isn't already
|
||||||
|
this.toggleCollapsed();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Key.ARROW_RIGHT: {
|
||||||
|
ev.stopPropagation();
|
||||||
|
if (isCollapsed) {
|
||||||
|
// On ARROW_RIGHT expand the room sublist if it isn't already
|
||||||
|
this.toggleCollapsed();
|
||||||
|
} else if (this.sublistRef.current) {
|
||||||
|
// otherwise focus the first room
|
||||||
|
const element = this.sublistRef.current.querySelector(".mx_RoomTile2") as HTMLDivElement;
|
||||||
|
if (element) {
|
||||||
|
element.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private onKeyDown = (ev: React.KeyboardEvent) => {
|
||||||
|
switch (ev.key) {
|
||||||
|
// On ARROW_LEFT go to the sublist header
|
||||||
|
case Key.ARROW_LEFT:
|
||||||
|
ev.stopPropagation();
|
||||||
|
this.headerButton.current.focus();
|
||||||
|
break;
|
||||||
|
// Consume ARROW_RIGHT so it doesn't cause focus to get sent to composer
|
||||||
|
case Key.ARROW_RIGHT:
|
||||||
|
ev.stopPropagation();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -335,7 +383,6 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||||
return (
|
return (
|
||||||
<RovingTabIndexWrapper>
|
<RovingTabIndexWrapper>
|
||||||
{({onFocus, isActive, ref}) => {
|
{({onFocus, isActive, ref}) => {
|
||||||
// TODO: Use onFocus: https://github.com/vector-im/riot-web/issues/14180
|
|
||||||
const tabIndex = isActive ? 0 : -1;
|
const tabIndex = isActive ? 0 : -1;
|
||||||
|
|
||||||
const badge = (
|
const badge = (
|
||||||
|
@ -382,13 +429,13 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||||
// doesn't become sticky.
|
// doesn't become sticky.
|
||||||
// The same applies to the notification badge.
|
// The same applies to the notification badge.
|
||||||
return (
|
return (
|
||||||
<div className={classes}>
|
<div className={classes} onKeyDown={this.onHeaderKeyDown} onFocus={onFocus}>
|
||||||
<div className='mx_RoomSublist2_stickable'>
|
<div className="mx_RoomSublist2_stickable">
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
onFocus={onFocus}
|
onFocus={onFocus}
|
||||||
inputRef={ref}
|
inputRef={ref}
|
||||||
tabIndex={tabIndex}
|
tabIndex={tabIndex}
|
||||||
className={"mx_RoomSublist2_headerText"}
|
className="mx_RoomSublist2_headerText"
|
||||||
role="treeitem"
|
role="treeitem"
|
||||||
aria-level={1}
|
aria-level={1}
|
||||||
onClick={this.onHeaderClick}
|
onClick={this.onHeaderClick}
|
||||||
|
@ -516,12 +563,13 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: onKeyDown support: https://github.com/vector-im/riot-web/issues/14180
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
ref={this.sublistRef}
|
||||||
className={classes}
|
className={classes}
|
||||||
role="group"
|
role="group"
|
||||||
aria-label={this.props.label}
|
aria-label={this.props.label}
|
||||||
|
onKeyDown={this.onKeyDown}
|
||||||
>
|
>
|
||||||
{this.renderHeader()}
|
{this.renderHeader()}
|
||||||
{content}
|
{content}
|
||||||
|
|
|
@ -235,7 +235,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||||
private onClickMentions = ev => this.saveNotifState(ev, MENTIONS_ONLY);
|
private onClickMentions = ev => this.saveNotifState(ev, MENTIONS_ONLY);
|
||||||
private onClickMute = ev => this.saveNotifState(ev, MUTE);
|
private onClickMute = ev => this.saveNotifState(ev, MUTE);
|
||||||
|
|
||||||
private renderNotificationsMenu(): React.ReactElement {
|
private renderNotificationsMenu(isActive: boolean): React.ReactElement {
|
||||||
if (this.props.isMinimized || MatrixClientPeg.get().isGuest() || this.props.tag === DefaultTagID.Invite) {
|
if (this.props.isMinimized || MatrixClientPeg.get().isGuest() || this.props.tag === DefaultTagID.Invite) {
|
||||||
// the menu makes no sense in these cases so do not show one
|
// the menu makes no sense in these cases so do not show one
|
||||||
return null;
|
return null;
|
||||||
|
@ -296,6 +296,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||||
onClick={this.onNotificationsMenuOpenClick}
|
onClick={this.onNotificationsMenuOpenClick}
|
||||||
label={_t("Notification options")}
|
label={_t("Notification options")}
|
||||||
isExpanded={!!this.state.notificationsMenuPosition}
|
isExpanded={!!this.state.notificationsMenuPosition}
|
||||||
|
tabIndex={isActive ? 0 : -1}
|
||||||
/>
|
/>
|
||||||
{contextMenu}
|
{contextMenu}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
|
@ -434,7 +435,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||||
{roomAvatar}
|
{roomAvatar}
|
||||||
{nameContainer}
|
{nameContainer}
|
||||||
{badge}
|
{badge}
|
||||||
{this.renderNotificationsMenu()}
|
{this.renderNotificationsMenu(isActive)}
|
||||||
{this.renderGeneralMenu()}
|
{this.renderGeneralMenu()}
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue