mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2025-12-30 11:22:31 +00:00
Improve accessibility in breakout rooms list (#13669)
Improve accessibility in breakout rooms list
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user