mirror of https://github.com/vector-im/riot-web
Space panel accessibility improvements (#9157)
* Move the UserMenu out of the SpacePanel ul list * Apply aria-selected to the spacepanel treeview * Fix typingpull/28788/head^2
parent
350341d13d
commit
3d0982e9a6
|
@ -78,10 +78,6 @@ $activeBorderColor: $primary-content;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
||||||
> .mx_SpaceItem {
|
|
||||||
padding-left: 16px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_SpaceButton_toggleCollapse {
|
.mx_SpaceButton_toggleCollapse {
|
||||||
|
|
|
@ -15,18 +15,28 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { HTMLAttributes, WheelEvent } from "react";
|
import classNames from "classnames";
|
||||||
|
import React, { HTMLAttributes, ReactHTML, WheelEvent } from "react";
|
||||||
|
|
||||||
interface IProps extends Omit<HTMLAttributes<HTMLDivElement>, "onScroll"> {
|
type DynamicHtmlElementProps<T extends keyof JSX.IntrinsicElements> =
|
||||||
|
JSX.IntrinsicElements[T] extends HTMLAttributes<{}> ? DynamicElementProps<T> : DynamicElementProps<"div">;
|
||||||
|
type DynamicElementProps<T extends keyof JSX.IntrinsicElements> = Partial<Omit<JSX.IntrinsicElements[T], 'ref'>>;
|
||||||
|
|
||||||
|
export type IProps<T extends keyof JSX.IntrinsicElements> = DynamicHtmlElementProps<T> & {
|
||||||
|
element?: T;
|
||||||
className?: string;
|
className?: string;
|
||||||
onScroll?: (event: Event) => void;
|
onScroll?: (event: Event) => void;
|
||||||
onWheel?: (event: WheelEvent) => void;
|
onWheel?: (event: WheelEvent) => void;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
tabIndex?: number;
|
tabIndex?: number;
|
||||||
wrappedRef?: (ref: HTMLDivElement) => void;
|
wrappedRef?: (ref: HTMLDivElement) => void;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default class AutoHideScrollbar<T extends keyof JSX.IntrinsicElements> extends React.Component<IProps<T>> {
|
||||||
|
static defaultProps = {
|
||||||
|
element: 'div' as keyof ReactHTML,
|
||||||
|
};
|
||||||
|
|
||||||
export default class AutoHideScrollbar extends React.Component<IProps> {
|
|
||||||
public readonly containerRef: React.RefObject<HTMLDivElement> = React.createRef();
|
public readonly containerRef: React.RefObject<HTMLDivElement> = React.createRef();
|
||||||
|
|
||||||
public componentDidMount() {
|
public componentDidMount() {
|
||||||
|
@ -36,9 +46,7 @@ export default class AutoHideScrollbar extends React.Component<IProps> {
|
||||||
this.containerRef.current.addEventListener("scroll", this.props.onScroll, { passive: true });
|
this.containerRef.current.addEventListener("scroll", this.props.onScroll, { passive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.wrappedRef) {
|
this.props.wrappedRef?.(this.containerRef.current);
|
||||||
this.props.wrappedRef(this.containerRef.current);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
|
@ -49,19 +57,15 @@ export default class AutoHideScrollbar extends React.Component<IProps> {
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const { className, onScroll, onWheel, style, tabIndex, wrappedRef, children, ...otherProps } = this.props;
|
const { element, className, onScroll, tabIndex, wrappedRef, children, ...otherProps } = this.props;
|
||||||
|
|
||||||
return (<div
|
return React.createElement(element, {
|
||||||
{...otherProps}
|
...otherProps,
|
||||||
ref={this.containerRef}
|
ref: this.containerRef,
|
||||||
style={style}
|
className: classNames("mx_AutoHideScrollbar", className),
|
||||||
className={["mx_AutoHideScrollbar", className].join(" ")}
|
|
||||||
onWheel={onWheel}
|
|
||||||
// Firefox sometimes makes this element focusable due to
|
// Firefox sometimes makes this element focusable due to
|
||||||
// overflow:scroll;, so force it out of tab order by default.
|
// overflow:scroll;, so force it out of tab order by default.
|
||||||
tabIndex={tabIndex ?? -1}
|
tabIndex: tabIndex ?? -1,
|
||||||
>
|
}, children);
|
||||||
{ children }
|
|
||||||
</div>);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,12 +14,12 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { ComponentProps, createRef } from "react";
|
import React, { createRef } from "react";
|
||||||
|
|
||||||
import AutoHideScrollbar from "./AutoHideScrollbar";
|
import AutoHideScrollbar, { IProps as AutoHideScrollbarProps } from "./AutoHideScrollbar";
|
||||||
import UIStore, { UI_EVENTS } from "../../stores/UIStore";
|
import UIStore, { UI_EVENTS } from "../../stores/UIStore";
|
||||||
|
|
||||||
interface IProps extends Omit<ComponentProps<typeof AutoHideScrollbar>, "onWheel"> {
|
export type IProps<T extends keyof JSX.IntrinsicElements> = Omit<AutoHideScrollbarProps<T>, "onWheel"> & {
|
||||||
// If true, the scrollbar will append mx_IndicatorScrollbar_leftOverflowIndicator
|
// If true, the scrollbar will append mx_IndicatorScrollbar_leftOverflowIndicator
|
||||||
// and mx_IndicatorScrollbar_rightOverflowIndicator elements to the list for positioning
|
// and mx_IndicatorScrollbar_rightOverflowIndicator elements to the list for positioning
|
||||||
// by the parent element.
|
// by the parent element.
|
||||||
|
@ -31,21 +31,22 @@ interface IProps extends Omit<ComponentProps<typeof AutoHideScrollbar>, "onWheel
|
||||||
verticalScrollsHorizontally?: boolean;
|
verticalScrollsHorizontally?: boolean;
|
||||||
|
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
className: string;
|
};
|
||||||
}
|
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
leftIndicatorOffset: string;
|
leftIndicatorOffset: string;
|
||||||
rightIndicatorOffset: string;
|
rightIndicatorOffset: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class IndicatorScrollbar extends React.Component<IProps, IState> {
|
export default class IndicatorScrollbar<
|
||||||
private autoHideScrollbar = createRef<AutoHideScrollbar>();
|
T extends keyof JSX.IntrinsicElements,
|
||||||
|
> extends React.Component<IProps<T>, IState> {
|
||||||
|
private autoHideScrollbar = createRef<AutoHideScrollbar<any>>();
|
||||||
private scrollElement: HTMLDivElement;
|
private scrollElement: HTMLDivElement;
|
||||||
private likelyTrackpadUser: boolean = null;
|
private likelyTrackpadUser: boolean = null;
|
||||||
private checkAgainForTrackpad = 0; // ts in milliseconds to recheck this._likelyTrackpadUser
|
private checkAgainForTrackpad = 0; // ts in milliseconds to recheck this._likelyTrackpadUser
|
||||||
|
|
||||||
constructor(props: IProps) {
|
constructor(props: IProps<T>) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
|
@ -65,7 +66,7 @@ export default class IndicatorScrollbar extends React.Component<IProps, IState>
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public componentDidUpdate(prevProps: IProps): void {
|
public componentDidUpdate(prevProps: IProps<T>): void {
|
||||||
const prevLen = React.Children.count(prevProps.children);
|
const prevLen = React.Children.count(prevProps.children);
|
||||||
const curLen = React.Children.count(this.props.children);
|
const curLen = React.Children.count(this.props.children);
|
||||||
// check overflow only if amount of children changes.
|
// check overflow only if amount of children changes.
|
||||||
|
|
|
@ -130,7 +130,7 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
|
||||||
const cli = useContext(MatrixClientContext);
|
const cli = useContext(MatrixClientContext);
|
||||||
const visibleRooms = useMemo(() => cli.getVisibleRooms().filter(r => r.getMyMembership() === "join"), [cli]);
|
const visibleRooms = useMemo(() => cli.getVisibleRooms().filter(r => r.getMyMembership() === "join"), [cli]);
|
||||||
|
|
||||||
const scrollRef = useRef<AutoHideScrollbar>();
|
const scrollRef = useRef<AutoHideScrollbar<"div">>();
|
||||||
const [scrollState, setScrollState] = useState<IScrollState>({
|
const [scrollState, setScrollState] = useState<IScrollState>({
|
||||||
// these are estimates which update as soon as it mounts
|
// these are estimates which update as soon as it mounts
|
||||||
scrollTop: 0,
|
scrollTop: 0,
|
||||||
|
|
|
@ -55,7 +55,7 @@ class EmojiPicker extends React.Component<IProps, IState> {
|
||||||
private readonly memoizedDataByCategory: Record<CategoryKey, IEmoji[]>;
|
private readonly memoizedDataByCategory: Record<CategoryKey, IEmoji[]>;
|
||||||
private readonly categories: ICategory[];
|
private readonly categories: ICategory[];
|
||||||
|
|
||||||
private scrollRef = React.createRef<AutoHideScrollbar>();
|
private scrollRef = React.createRef<AutoHideScrollbar<"div">>();
|
||||||
|
|
||||||
constructor(props: IProps) {
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
|
@ -132,6 +132,7 @@ const MetaSpaceButton = ({ selected, isPanelCollapsed, ...props }: IMetaSpaceBut
|
||||||
"collapsed": isPanelCollapsed,
|
"collapsed": isPanelCollapsed,
|
||||||
})}
|
})}
|
||||||
role="treeitem"
|
role="treeitem"
|
||||||
|
aria-selected={selected}
|
||||||
>
|
>
|
||||||
<SpaceButton {...props} selected={selected} isNarrow={isPanelCollapsed} />
|
<SpaceButton {...props} selected={selected} isNarrow={isPanelCollapsed} />
|
||||||
</li>;
|
</li>;
|
||||||
|
@ -282,6 +283,9 @@ const InnerSpacePanel = React.memo<IInnerSpacePanelProps>(({
|
||||||
style={isDraggingOver ? {
|
style={isDraggingOver ? {
|
||||||
pointerEvents: "none",
|
pointerEvents: "none",
|
||||||
} : undefined}
|
} : undefined}
|
||||||
|
element="ul"
|
||||||
|
role="tree"
|
||||||
|
aria-label={_t("Spaces")}
|
||||||
>
|
>
|
||||||
{ metaSpacesSection }
|
{ metaSpacesSection }
|
||||||
{ invites.map(s => (
|
{ invites.map(s => (
|
||||||
|
@ -321,7 +325,7 @@ const InnerSpacePanel = React.memo<IInnerSpacePanelProps>(({
|
||||||
|
|
||||||
const SpacePanel = () => {
|
const SpacePanel = () => {
|
||||||
const [isPanelCollapsed, setPanelCollapsed] = useState(true);
|
const [isPanelCollapsed, setPanelCollapsed] = useState(true);
|
||||||
const ref = useRef<HTMLUListElement>();
|
const ref = useRef<HTMLDivElement>();
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
UIStore.instance.trackElementDimensions("SpacePanel", ref.current);
|
UIStore.instance.trackElementDimensions("SpacePanel", ref.current);
|
||||||
return () => UIStore.instance.stopTrackingElementDimensions("SpacePanel");
|
return () => UIStore.instance.stopTrackingElementDimensions("SpacePanel");
|
||||||
|
@ -340,11 +344,9 @@ const SpacePanel = () => {
|
||||||
}}>
|
}}>
|
||||||
<RovingTabIndexProvider handleHomeEnd handleUpDown>
|
<RovingTabIndexProvider handleHomeEnd handleUpDown>
|
||||||
{ ({ onKeyDownHandler }) => (
|
{ ({ onKeyDownHandler }) => (
|
||||||
<ul
|
<div
|
||||||
className={classNames("mx_SpacePanel", { collapsed: isPanelCollapsed })}
|
className={classNames("mx_SpacePanel", { collapsed: isPanelCollapsed })}
|
||||||
onKeyDown={onKeyDownHandler}
|
onKeyDown={onKeyDownHandler}
|
||||||
role="tree"
|
|
||||||
aria-label={_t("Spaces")}
|
|
||||||
ref={ref}
|
ref={ref}
|
||||||
>
|
>
|
||||||
<UserMenu isPanelCollapsed={isPanelCollapsed}>
|
<UserMenu isPanelCollapsed={isPanelCollapsed}>
|
||||||
|
@ -381,7 +383,7 @@ const SpacePanel = () => {
|
||||||
</Droppable>
|
</Droppable>
|
||||||
|
|
||||||
<QuickSettingsButton isPanelCollapsed={isPanelCollapsed} />
|
<QuickSettingsButton isPanelCollapsed={isPanelCollapsed} />
|
||||||
</ul>
|
</div>
|
||||||
) }
|
) }
|
||||||
</RovingTabIndexProvider>
|
</RovingTabIndexProvider>
|
||||||
</DragDropContext>
|
</DragDropContext>
|
||||||
|
|
|
@ -315,6 +315,7 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const { tabIndex, ...restDragHandleProps } = dragHandleProps || {};
|
const { tabIndex, ...restDragHandleProps } = dragHandleProps || {};
|
||||||
|
const selected = activeSpaces.includes(space.roomId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
|
@ -322,13 +323,14 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
|
||||||
className={itemClasses}
|
className={itemClasses}
|
||||||
ref={innerRef}
|
ref={innerRef}
|
||||||
aria-expanded={hasChildren ? !collapsed : undefined}
|
aria-expanded={hasChildren ? !collapsed : undefined}
|
||||||
|
aria-selected={selected}
|
||||||
role="treeitem"
|
role="treeitem"
|
||||||
>
|
>
|
||||||
<SpaceButton
|
<SpaceButton
|
||||||
{...restDragHandleProps}
|
{...restDragHandleProps}
|
||||||
space={space}
|
space={space}
|
||||||
className={isInvite ? "mx_SpaceButton_invite" : undefined}
|
className={isInvite ? "mx_SpaceButton_invite" : undefined}
|
||||||
selected={activeSpaces.includes(space.roomId)}
|
selected={selected}
|
||||||
label={this.state.name}
|
label={this.state.name}
|
||||||
contextMenuTooltip={_t("Space options")}
|
contextMenuTooltip={_t("Space options")}
|
||||||
notificationState={notificationState}
|
notificationState={notificationState}
|
||||||
|
|
Loading…
Reference in New Issue