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 all 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