Improve accessibility in breakout rooms list (#13669)

Improve accessibility in breakout rooms list
This commit is contained in:
AHMAD KADRI
2023-09-27 11:13:03 +02:00
committed by GitHub
parent e1dc573c3c
commit 58115477a2
10 changed files with 214 additions and 32 deletions

View File

@@ -1,4 +1,6 @@
import React, { ReactNode, useEffect, useLayoutEffect, useRef, useState } from 'react';
import React, { KeyboardEvent, ReactNode,
useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
import { FocusOn } from 'react-focus-on';
import { useSelector } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
@@ -9,6 +11,7 @@ import participantsPaneTheme from '../../../components/themes/participantsPaneTh
import { withPixelLineHeight } from '../../../styles/functions.web';
import { spacing } from '../../Tokens';
/**
* Get a style property from a style declaration as a float.
*
@@ -45,6 +48,11 @@ interface IProps {
*/
accessibilityLabel?: string;
/**
* To activate the FocusOn component.
*/
activateFocusTrap?: boolean;
/**
* Children of the context menu.
*/
@@ -163,6 +171,7 @@ const useStyles = makeStyles()(theme => {
const ContextMenu = ({
accessibilityLabel,
activateFocusTrap = false,
children,
className,
entity,
@@ -227,6 +236,133 @@ const ContextMenu = ({
}
}, [ hidden ]);
const handleKeyDown = useCallback((event: KeyboardEvent) => {
const { current: listRef } = containerRef;
const currentFocusElement = document.activeElement;
const moveFocus = (
list: Element | null,
currentFocus: Element | null,
traversalFunction: (
list: Element | null,
currentFocus: Element | null
) => Element | null
) => {
let wrappedOnce = false;
let nextFocus = traversalFunction(list, currentFocus);
/* eslint-disable no-unmodified-loop-condition */
while (list && nextFocus) {
// Prevent infinite loop.
if (nextFocus === list.firstChild) {
if (wrappedOnce) {
return;
}
wrappedOnce = true;
}
// Same logic as useAutocomplete.js
const nextFocusDisabled
/* eslint-disable no-extra-parens */
= (nextFocus as HTMLInputElement).disabled
|| nextFocus.getAttribute('aria-disabled') === 'true';
if (!nextFocus.hasAttribute('tabindex') || nextFocusDisabled) {
// Move to the next element.
nextFocus = traversalFunction(list, nextFocus);
} else {
/* eslint-disable no-extra-parens */
(nextFocus as HTMLElement).focus();
return;
}
}
};
const previousItem = (
list: Element | null,
item: Element | null
): Element | null => {
/**
* To find the last child of the list.
*
* @param {Element | null} element - Element.
* @returns {Element | null}
*/
function lastChild(element: Element | null): Element | null {
while (element?.lastElementChild) {
/* eslint-disable no-param-reassign */
element = element.lastElementChild;
}
return element;
}
if (!list) {
return null;
}
if (list === item) {
return list.lastElementChild;
}
if (item?.previousElementSibling) {
return lastChild(item.previousElementSibling);
}
if (item && item?.parentElement !== list) {
return item.parentElement;
}
return lastChild(list.lastElementChild);
};
const nextItem = (
list: Element | null,
item: Element | null
): Element | null => {
if (!list) {
return null;
}
if (list === item) {
return list.firstElementChild;
}
if (item?.firstElementChild) {
return item.firstElementChild;
}
if (item?.nextElementSibling) {
return item.nextElementSibling;
}
while (item && item.parentElement !== list) {
/* eslint-disable no-param-reassign */
item = item.parentElement;
if (item?.nextElementSibling) {
return item.nextElementSibling;
}
}
return list?.firstElementChild;
};
if (event.key === 'Escape') {
// Close the menu
event.preventDefault();
setIsHidden(true);
} else if (event.key === 'ArrowUp') {
// Move focus to the previous menu item
event.preventDefault();
moveFocus(listRef, currentFocusElement, previousItem);
} else if (event.key === 'ArrowDown') {
// Move focus to the next menu item
event.preventDefault();
moveFocus(listRef, currentFocusElement, nextItem);
}
}, [ containerRef ]);
const removeFocus = useCallback(() => {
onDrawerClose?.();
}, [ onMouseLeave ]);
if (_overflowDrawer && inDrawer) {
return (<div
className = { styles.drawer }
@@ -247,23 +383,32 @@ const ContextMenu = ({
</div>
</Drawer>
</JitsiPortal>
: <div
{ ...aria }
aria-label = { accessibilityLabel }
className = { cx(styles.contextMenu,
: <FocusOn
// Use the `enabled` prop instead of conditionally rendering ReactFocusOn
// to prevent UI stutter on dialog appearance. It seems the focus guards generated annoy
// our DialogPortal positioning calculations.
enabled = { activateFocusTrap && !isHidden }
onClickOutside = { removeFocus }
onEscapeKey = { removeFocus }>
<div
{ ...aria }
aria-label = { accessibilityLabel }
className = { cx(styles.contextMenu,
isHidden && styles.contextMenuHidden,
className
) }
id = { id }
onClick = { onClick }
onKeyDown = { onKeyDown }
onMouseEnter = { onMouseEnter }
onMouseLeave = { onMouseLeave }
ref = { containerRef }
role = { role }
tabIndex = { tabIndex }>
{children}
</div>;
) }
id = { id }
onClick = { onClick }
onKeyDown = { onKeyDown ?? handleKeyDown }
onMouseEnter = { onMouseEnter }
onMouseLeave = { onMouseLeave }
ref = { containerRef }
role = { role }
tabIndex = { tabIndex }>
{children}
</div>
</FocusOn >;
};
export default ContextMenu;

View File

@@ -85,7 +85,7 @@ export interface IProps {
* If no onClick handler is provided, we assume the context menu item is
* not interactive and no role will be set.
*/
role?: 'tab' | 'button';
role?: 'tab' | 'button' | 'menuitem';
/**
* Whether the item is marked as selected.

View File

@@ -18,6 +18,11 @@ interface IProps {
*/
className?: string;
/**
* The breakout name for aria-label.
*/
defaultName?: string;
/**
* Whether or not the actions should be hidden.
*/
@@ -111,7 +116,15 @@ const useStyles = makeStyles()(theme => {
},
highlighted: {
backgroundColor: theme.palette.ui02
backgroundColor: theme.palette.ui02,
'& .actions': {
display: 'flex',
position: 'relative',
top: 'auto',
boxShadow: `-15px 0px 10px -5px ${theme.palette.ui02}`,
backgroundColor: theme.palette.ui02
}
},
detailsContainer: {
@@ -179,6 +192,7 @@ const useStyles = makeStyles()(theme => {
const ListItem = ({
actions,
className,
defaultName,
icon,
id,
hideActions = false,
@@ -229,6 +243,7 @@ const ListItem = ({
return (
<div
aria-label = { defaultName }
className = { cx('list-item-container',
classes.container,
isHighlighted && classes.highlighted,
@@ -237,6 +252,7 @@ const ListItem = ({
data-testid = { testId }
id = { id }
onClick = { onClick }
role = 'listitem'
{ ...(isMobile
? {
onTouchEnd: _onTouchEnd,