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

Commit 3d0982e

Browse files
authored
Space panel accessibility improvements (#9157)
* Move the UserMenu out of the SpacePanel ul list * Apply aria-selected to the spacepanel treeview * Fix typing
1 parent 350341d commit 3d0982e

File tree

7 files changed

+44
-39
lines changed

7 files changed

+44
-39
lines changed

res/css/structures/_SpacePanel.pcss

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,6 @@ $activeBorderColor: $primary-content;
7878
margin: 0;
7979
list-style: none;
8080
padding: 0;
81-
82-
> .mx_SpaceItem {
83-
padding-left: 16px;
84-
}
8581
}
8682

8783
.mx_SpaceButton_toggleCollapse {

src/components/structures/AutoHideScrollbar.tsx

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,28 @@ See the License for the specific language governing permissions and
1515
limitations under the License.
1616
*/
1717

18-
import React, { HTMLAttributes, WheelEvent } from "react";
18+
import classNames from "classnames";
19+
import React, { HTMLAttributes, ReactHTML, WheelEvent } from "react";
1920

20-
interface IProps extends Omit<HTMLAttributes<HTMLDivElement>, "onScroll"> {
21+
type DynamicHtmlElementProps<T extends keyof JSX.IntrinsicElements> =
22+
JSX.IntrinsicElements[T] extends HTMLAttributes<{}> ? DynamicElementProps<T> : DynamicElementProps<"div">;
23+
type DynamicElementProps<T extends keyof JSX.IntrinsicElements> = Partial<Omit<JSX.IntrinsicElements[T], 'ref'>>;
24+
25+
export type IProps<T extends keyof JSX.IntrinsicElements> = DynamicHtmlElementProps<T> & {
26+
element?: T;
2127
className?: string;
2228
onScroll?: (event: Event) => void;
2329
onWheel?: (event: WheelEvent) => void;
2430
style?: React.CSSProperties;
2531
tabIndex?: number;
2632
wrappedRef?: (ref: HTMLDivElement) => void;
27-
}
33+
};
34+
35+
export default class AutoHideScrollbar<T extends keyof JSX.IntrinsicElements> extends React.Component<IProps<T>> {
36+
static defaultProps = {
37+
element: 'div' as keyof ReactHTML,
38+
};
2839

29-
export default class AutoHideScrollbar extends React.Component<IProps> {
3040
public readonly containerRef: React.RefObject<HTMLDivElement> = React.createRef();
3141

3242
public componentDidMount() {
@@ -36,9 +46,7 @@ export default class AutoHideScrollbar extends React.Component<IProps> {
3646
this.containerRef.current.addEventListener("scroll", this.props.onScroll, { passive: true });
3747
}
3848

39-
if (this.props.wrappedRef) {
40-
this.props.wrappedRef(this.containerRef.current);
41-
}
49+
this.props.wrappedRef?.(this.containerRef.current);
4250
}
4351

4452
public componentWillUnmount() {
@@ -49,19 +57,15 @@ export default class AutoHideScrollbar extends React.Component<IProps> {
4957

5058
public render() {
5159
// eslint-disable-next-line @typescript-eslint/no-unused-vars
52-
const { className, onScroll, onWheel, style, tabIndex, wrappedRef, children, ...otherProps } = this.props;
60+
const { element, className, onScroll, tabIndex, wrappedRef, children, ...otherProps } = this.props;
5361

54-
return (<div
55-
{...otherProps}
56-
ref={this.containerRef}
57-
style={style}
58-
className={["mx_AutoHideScrollbar", className].join(" ")}
59-
onWheel={onWheel}
62+
return React.createElement(element, {
63+
...otherProps,
64+
ref: this.containerRef,
65+
className: classNames("mx_AutoHideScrollbar", className),
6066
// Firefox sometimes makes this element focusable due to
6167
// overflow:scroll;, so force it out of tab order by default.
62-
tabIndex={tabIndex ?? -1}
63-
>
64-
{ children }
65-
</div>);
68+
tabIndex: tabIndex ?? -1,
69+
}, children);
6670
}
6771
}

src/components/structures/IndicatorScrollbar.tsx

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
import React, { ComponentProps, createRef } from "react";
17+
import React, { createRef } from "react";
1818

19-
import AutoHideScrollbar from "./AutoHideScrollbar";
19+
import AutoHideScrollbar, { IProps as AutoHideScrollbarProps } from "./AutoHideScrollbar";
2020
import UIStore, { UI_EVENTS } from "../../stores/UIStore";
2121

22-
interface IProps extends Omit<ComponentProps<typeof AutoHideScrollbar>, "onWheel"> {
22+
export type IProps<T extends keyof JSX.IntrinsicElements> = Omit<AutoHideScrollbarProps<T>, "onWheel"> & {
2323
// If true, the scrollbar will append mx_IndicatorScrollbar_leftOverflowIndicator
2424
// and mx_IndicatorScrollbar_rightOverflowIndicator elements to the list for positioning
2525
// by the parent element.
@@ -31,21 +31,22 @@ interface IProps extends Omit<ComponentProps<typeof AutoHideScrollbar>, "onWheel
3131
verticalScrollsHorizontally?: boolean;
3232

3333
children: React.ReactNode;
34-
className: string;
35-
}
34+
};
3635

3736
interface IState {
3837
leftIndicatorOffset: string;
3938
rightIndicatorOffset: string;
4039
}
4140

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

48-
constructor(props: IProps) {
49+
constructor(props: IProps<T>) {
4950
super(props);
5051

5152
this.state = {
@@ -65,7 +66,7 @@ export default class IndicatorScrollbar extends React.Component<IProps, IState>
6566
}
6667
};
6768

68-
public componentDidUpdate(prevProps: IProps): void {
69+
public componentDidUpdate(prevProps: IProps<T>): void {
6970
const prevLen = React.Children.count(prevProps.children);
7071
const curLen = React.Children.count(this.props.children);
7172
// check overflow only if amount of children changes.

src/components/views/dialogs/AddExistingToSpaceDialog.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
130130
const cli = useContext(MatrixClientContext);
131131
const visibleRooms = useMemo(() => cli.getVisibleRooms().filter(r => r.getMyMembership() === "join"), [cli]);
132132

133-
const scrollRef = useRef<AutoHideScrollbar>();
133+
const scrollRef = useRef<AutoHideScrollbar<"div">>();
134134
const [scrollState, setScrollState] = useState<IScrollState>({
135135
// these are estimates which update as soon as it mounts
136136
scrollTop: 0,

src/components/views/emojipicker/EmojiPicker.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ class EmojiPicker extends React.Component<IProps, IState> {
5555
private readonly memoizedDataByCategory: Record<CategoryKey, IEmoji[]>;
5656
private readonly categories: ICategory[];
5757

58-
private scrollRef = React.createRef<AutoHideScrollbar>();
58+
private scrollRef = React.createRef<AutoHideScrollbar<"div">>();
5959

6060
constructor(props: IProps) {
6161
super(props);

src/components/views/spaces/SpacePanel.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ const MetaSpaceButton = ({ selected, isPanelCollapsed, ...props }: IMetaSpaceBut
132132
"collapsed": isPanelCollapsed,
133133
})}
134134
role="treeitem"
135+
aria-selected={selected}
135136
>
136137
<SpaceButton {...props} selected={selected} isNarrow={isPanelCollapsed} />
137138
</li>;
@@ -282,6 +283,9 @@ const InnerSpacePanel = React.memo<IInnerSpacePanelProps>(({
282283
style={isDraggingOver ? {
283284
pointerEvents: "none",
284285
} : undefined}
286+
element="ul"
287+
role="tree"
288+
aria-label={_t("Spaces")}
285289
>
286290
{ metaSpacesSection }
287291
{ invites.map(s => (
@@ -321,7 +325,7 @@ const InnerSpacePanel = React.memo<IInnerSpacePanelProps>(({
321325

322326
const SpacePanel = () => {
323327
const [isPanelCollapsed, setPanelCollapsed] = useState(true);
324-
const ref = useRef<HTMLUListElement>();
328+
const ref = useRef<HTMLDivElement>();
325329
useLayoutEffect(() => {
326330
UIStore.instance.trackElementDimensions("SpacePanel", ref.current);
327331
return () => UIStore.instance.stopTrackingElementDimensions("SpacePanel");
@@ -340,11 +344,9 @@ const SpacePanel = () => {
340344
}}>
341345
<RovingTabIndexProvider handleHomeEnd handleUpDown>
342346
{ ({ onKeyDownHandler }) => (
343-
<ul
347+
<div
344348
className={classNames("mx_SpacePanel", { collapsed: isPanelCollapsed })}
345349
onKeyDown={onKeyDownHandler}
346-
role="tree"
347-
aria-label={_t("Spaces")}
348350
ref={ref}
349351
>
350352
<UserMenu isPanelCollapsed={isPanelCollapsed}>
@@ -381,7 +383,7 @@ const SpacePanel = () => {
381383
</Droppable>
382384

383385
<QuickSettingsButton isPanelCollapsed={isPanelCollapsed} />
384-
</ul>
386+
</div>
385387
) }
386388
</RovingTabIndexProvider>
387389
</DragDropContext>

src/components/views/spaces/SpaceTreeLevel.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -315,20 +315,22 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
315315

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

319320
return (
320321
<li
321322
{...otherProps}
322323
className={itemClasses}
323324
ref={innerRef}
324325
aria-expanded={hasChildren ? !collapsed : undefined}
326+
aria-selected={selected}
325327
role="treeitem"
326328
>
327329
<SpaceButton
328330
{...restDragHandleProps}
329331
space={space}
330332
className={isInvite ? "mx_SpaceButton_invite" : undefined}
331-
selected={activeSpaces.includes(space.roomId)}
333+
selected={selected}
332334
label={this.state.name}
333335
contextMenuTooltip={_t("Space options")}
334336
notificationState={notificationState}

0 commit comments

Comments
 (0)