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

Space panel accessibility improvements #9157

Merged
merged 4 commits into from
Aug 10, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions res/css/structures/_SpacePanel.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,6 @@ $activeBorderColor: $primary-content;
margin: 0;
list-style: none;
padding: 0;

> .mx_SpaceItem {
padding-left: 16px;
}
}

.mx_SpaceButton_toggleCollapse {
Expand Down
40 changes: 22 additions & 18 deletions src/components/structures/AutoHideScrollbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,28 @@ See the License for the specific language governing permissions and
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;
onScroll?: (event: Event) => void;
onWheel?: (event: WheelEvent) => void;
style?: React.CSSProperties;
tabIndex?: number;
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 componentDidMount() {
Expand All @@ -36,9 +46,7 @@ export default class AutoHideScrollbar extends React.Component<IProps> {
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() {
Expand All @@ -49,19 +57,15 @@ export default class AutoHideScrollbar extends React.Component<IProps> {

public render() {
// 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
{...otherProps}
ref={this.containerRef}
style={style}
className={["mx_AutoHideScrollbar", className].join(" ")}
onWheel={onWheel}
return React.createElement(element, {
...otherProps,
ref: this.containerRef,
className: classNames("mx_AutoHideScrollbar", className),
// Firefox sometimes makes this element focusable due to
// overflow:scroll;, so force it out of tab order by default.
tabIndex={tabIndex ?? -1}
>
{ children }
</div>);
tabIndex: tabIndex ?? -1,
}, children);
}
}
19 changes: 10 additions & 9 deletions src/components/structures/IndicatorScrollbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ See the License for the specific language governing permissions and
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";

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
// and mx_IndicatorScrollbar_rightOverflowIndicator elements to the list for positioning
// by the parent element.
Expand All @@ -31,21 +31,22 @@ interface IProps extends Omit<ComponentProps<typeof AutoHideScrollbar>, "onWheel
verticalScrollsHorizontally?: boolean;

children: React.ReactNode;
className: string;
}
};

interface IState {
leftIndicatorOffset: string;
rightIndicatorOffset: string;
}

export default class IndicatorScrollbar extends React.Component<IProps, IState> {
private autoHideScrollbar = createRef<AutoHideScrollbar>();
export default class IndicatorScrollbar<
T extends keyof JSX.IntrinsicElements,
> extends React.Component<IProps<T>, IState> {
private autoHideScrollbar = createRef<AutoHideScrollbar<any>>();
private scrollElement: HTMLDivElement;
private likelyTrackpadUser: boolean = null;
private checkAgainForTrackpad = 0; // ts in milliseconds to recheck this._likelyTrackpadUser

constructor(props: IProps) {
constructor(props: IProps<T>) {
super(props);

this.state = {
Expand All @@ -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 curLen = React.Children.count(this.props.children);
// check overflow only if amount of children changes.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
const cli = useContext(MatrixClientContext);
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>({
// these are estimates which update as soon as it mounts
scrollTop: 0,
Expand Down
2 changes: 1 addition & 1 deletion src/components/views/emojipicker/EmojiPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ class EmojiPicker extends React.Component<IProps, IState> {
private readonly memoizedDataByCategory: Record<CategoryKey, IEmoji[]>;
private readonly categories: ICategory[];

private scrollRef = React.createRef<AutoHideScrollbar>();
private scrollRef = React.createRef<AutoHideScrollbar<"div">>();

constructor(props: IProps) {
super(props);
Expand Down
12 changes: 7 additions & 5 deletions src/components/views/spaces/SpacePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ const MetaSpaceButton = ({ selected, isPanelCollapsed, ...props }: IMetaSpaceBut
"collapsed": isPanelCollapsed,
})}
role="treeitem"
aria-selected={selected}
>
<SpaceButton {...props} selected={selected} isNarrow={isPanelCollapsed} />
</li>;
Expand Down Expand Up @@ -282,6 +283,9 @@ const InnerSpacePanel = React.memo<IInnerSpacePanelProps>(({
style={isDraggingOver ? {
pointerEvents: "none",
} : undefined}
element="ul"
role="tree"
aria-label={_t("Spaces")}
>
{ metaSpacesSection }
{ invites.map(s => (
Expand Down Expand Up @@ -321,7 +325,7 @@ const InnerSpacePanel = React.memo<IInnerSpacePanelProps>(({

const SpacePanel = () => {
const [isPanelCollapsed, setPanelCollapsed] = useState(true);
const ref = useRef<HTMLUListElement>();
const ref = useRef<HTMLDivElement>();
useLayoutEffect(() => {
UIStore.instance.trackElementDimensions("SpacePanel", ref.current);
return () => UIStore.instance.stopTrackingElementDimensions("SpacePanel");
Expand All @@ -340,11 +344,9 @@ const SpacePanel = () => {
}}>
<RovingTabIndexProvider handleHomeEnd handleUpDown>
{ ({ onKeyDownHandler }) => (
<ul
<div
className={classNames("mx_SpacePanel", { collapsed: isPanelCollapsed })}
onKeyDown={onKeyDownHandler}
role="tree"
aria-label={_t("Spaces")}
ref={ref}
>
<UserMenu isPanelCollapsed={isPanelCollapsed}>
Expand Down Expand Up @@ -381,7 +383,7 @@ const SpacePanel = () => {
</Droppable>

<QuickSettingsButton isPanelCollapsed={isPanelCollapsed} />
</ul>
</div>
) }
</RovingTabIndexProvider>
</DragDropContext>
Expand Down
4 changes: 3 additions & 1 deletion src/components/views/spaces/SpaceTreeLevel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -315,20 +315,22 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { tabIndex, ...restDragHandleProps } = dragHandleProps || {};
const selected = activeSpaces.includes(space.roomId);

return (
<li
{...otherProps}
className={itemClasses}
ref={innerRef}
aria-expanded={hasChildren ? !collapsed : undefined}
aria-selected={selected}
role="treeitem"
>
<SpaceButton
{...restDragHandleProps}
space={space}
className={isInvite ? "mx_SpaceButton_invite" : undefined}
selected={activeSpaces.includes(space.roomId)}
selected={selected}
label={this.state.name}
contextMenuTooltip={_t("Space options")}
notificationState={notificationState}
Expand Down