Use tabs ARIA design pattern when using tabbed UI (#12994)

feat(a11y): use tabs ARIA design pattern when using tabbed UI
This commit is contained in:
Emmanuel Pelletier 2023-03-06 16:13:29 +01:00 committed by GitHub
parent 0d0bec3aad
commit f727b9295f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 338 additions and 239 deletions

View File

@ -82,6 +82,7 @@
}
.left-column {
order: -1;
display: flex;
flex-direction: column;
flex-grow: 0;
@ -92,6 +93,7 @@
.right-column {
display: flex;
flex-direction: column;
align-items: flex-start;
flex-grow: 1;
padding-left: 16px;
padding-top: 13px;
@ -99,11 +101,11 @@
}
.title {
font-size: 12px;
font-weight: 600;
line-height: 16px;
padding-bottom: 4px;
}
font-size: 12px;
font-weight: 600;
line-height: 16px;
margin-bottom: 4px;
}
.subtitle {
color: #5E6D7A;
@ -125,8 +127,7 @@
cursor: pointer;
}
&.with-click-handler:hover,
&.with-click-handler:focus {
&.with-click-handler:hover {
background-color: #c7ddff;
}

View File

@ -1,3 +1,3 @@
#polls-panel {
.polls-panel {
height: calc(100% - 119px);
}

View File

@ -167,7 +167,7 @@ body.welcome-page {
margin: 4px;
display: $welcomePageTabButtonsDisplay;
.tab {
[role="tab"] {
background-color: #c7ddff;
border-radius: 7px;
cursor: pointer;
@ -176,8 +176,10 @@ body.welcome-page {
margin: 2px;
padding: 7px 0;
text-align: center;
color: inherit;
border: 0;
&.selected {
&[aria-selected="true"] {
background-color: #FFF;
}
}

View File

@ -240,7 +240,9 @@
"WaitingForHostTitle": "Waiting for the host ...",
"Yes": "Yes",
"accessibilityLabel": {
"liveStreaming": "Live Stream"
"close": "Close dialog",
"liveStreaming": "Live Stream",
"sharingTabs": "Sharing options"
},
"add": "Add",
"addMeetingNote": "Add a note about this meeting",
@ -1387,6 +1389,7 @@
"microsoftLogo": "Microsoft logo",
"policyLogo": "Policy logo"
},
"meetingsAccessibilityLabel": "Meetings",
"mobileDownLoadLinkAndroid": "Download mobile app for Android",
"mobileDownLoadLinkFDroid": "Download mobile app for F-Droid",
"mobileDownLoadLinkIos": "Download mobile app for iOS",

View File

@ -105,17 +105,14 @@ class MeetingsList extends Component<Props> {
* @returns {React.ReactNode}
*/
render() {
const { listEmptyComponent, meetings, t } = this.props;
const { listEmptyComponent, meetings } = this.props;
/**
* If there are no recent meetings we don't want to display anything.
*/
if (meetings) {
return (
<Container
aria-label = { t('welcomepage.recentList') }
className = 'meetings-list'
tabIndex = '-1'>
<Container className = 'meetings-list'>
{
meetings.length === 0
? listEmptyComponent
@ -237,23 +234,16 @@ class MeetingsList extends Component<Props> {
return (
<Container
aria-label = { title }
className = { rootClassName }
key = { index }
onClick = { onPress }
onKeyPress = { onKeyPress }
role = 'button'
tabIndex = { 0 }>
<Container className = 'left-column'>
<Text className = 'title'>
{ _toDateString(date) }
</Text>
<Text className = 'subtitle'>
{ _toTimeString(time) }
</Text>
</Container>
onClick = { onPress }>
<Container className = 'right-column'>
<Text className = 'title'>
<Text
className = 'title'
onClick = { onPress }
onKeyPress = { onKeyPress }
role = 'button'
tabIndex = { 0 }>
{ title }
</Text>
{
@ -269,6 +259,14 @@ class MeetingsList extends Component<Props> {
</Text>) : null
}
</Container>
<Container className = 'left-column'>
<Text className = 'title'>
{ _toDateString(date) }
</Text>
<Text className = 'subtitle'>
{ _toTimeString(time) }
</Text>
</Container>
<Container className = 'actions'>
{ elementAfter || null }

View File

@ -26,6 +26,13 @@ export interface IProps {
*/
className?: string;
/**
* Id of dom element controlled by this item. Matches aria-controls.
* Useful if you need this item as a tab element.
*
*/
controls?: string;
/**
* Custom icon. If used, the icon prop is ignored.
* Used to allow custom children instead of just the default icons.
@ -55,7 +62,7 @@ export interface IProps {
/**
* Keydown handler.
*/
onKeyDown?: (e?: React.KeyboardEvent) => void;
onKeyDown?: (e: React.KeyboardEvent<HTMLDivElement>) => void;
/**
* Keypress handler.
@ -67,6 +74,11 @@ export interface IProps {
*/
overflowType?: TEXT_OVERFLOW_TYPES;
/**
* You can use this item as a tab. Defaults to button if not set.
*/
role?: 'tab' | 'button';
/**
* Whether the item is marked as selected.
*/
@ -150,6 +162,7 @@ const ContextMenuItem = ({
accessibilityLabel,
children,
className,
controls,
customIcon,
disabled,
id,
@ -158,6 +171,7 @@ const ContextMenuItem = ({
onKeyDown,
onKeyPress,
overflowType,
role = 'button',
selected,
testId,
text,
@ -167,8 +181,10 @@ const ContextMenuItem = ({
return (
<div
aria-controls = { controls }
aria-disabled = { disabled }
aria-label = { accessibilityLabel }
aria-selected = { role === 'tab' ? selected : undefined }
className = { cx(styles.contextMenuItem,
_overflowDrawer && styles.contextMenuItemDrawer,
disabled && styles.contextMenuItemDisabled,
@ -181,8 +197,11 @@ const ContextMenuItem = ({
onClick = { disabled ? undefined : onClick }
onKeyDown = { disabled ? undefined : onKeyDown }
onKeyPress = { disabled ? undefined : onKeyPress }
role = 'button'
tabIndex = { disabled ? undefined : 0 }>
role = { role }
tabIndex = { role === 'tab'
? selected ? 0 : -1
: disabled ? undefined : 0
}>
{customIcon ? customIcon
: icon && <Icon
className = { styles.contextMenuItemIcon }

View File

@ -131,7 +131,7 @@ const Dialog = ({
</p>
{!hideCloseButton && (
<ClickableIcon
accessibilityLabel = { t('dialog.close') }
accessibilityLabel = { t('dialog.accessibilityLabel.close') }
icon = { IconCloseLarge }
id = 'modal-header-close-button'
onClick = { onClose } />

View File

@ -1,4 +1,5 @@
import React, { ComponentType, useCallback, useEffect, useMemo, useState } from 'react';
import { MoveFocusInside } from 'react-focus-lock';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
@ -89,10 +90,6 @@ const useStyles = makeStyles()(theme => {
}
},
closeButtonContainer: {
paddingBottom: theme.spacing(4)
},
buttonContainer: {
width: '100%',
boxSizing: 'border-box',
@ -109,6 +106,7 @@ const useStyles = makeStyles()(theme => {
backContainer: {
display: 'flex',
flexDirection: 'row-reverse',
alignItems: 'center',
'& > button': {
@ -127,6 +125,11 @@ const useStyles = makeStyles()(theme => {
}
},
header: {
order: -1,
paddingBottom: theme.spacing(4)
},
footer: {
justifyContent: 'flex-end',
paddingTop: theme.spacing(4),
@ -168,6 +171,7 @@ const DialogWithTabs = ({
const dispatch = useDispatch();
const { t } = useTranslation();
const [ selectedTab, setSelectedTab ] = useState<string | undefined>(defaultTab ?? tabs[0].name);
const [ userSelected, setUserSelected ] = useState(false);
const [ tabStates, setTabStates ] = useState(tabs.map(tab => tab.props));
const clientWidth = useSelector((state: IReduxState) => state['features/base/responsive-ui'].clientWidth);
const [ isMobile, setIsMobile ] = useState(false);
@ -188,18 +192,58 @@ const DialogWithTabs = ({
}
}, [ isMobile ]);
const back = useCallback(() => {
setSelectedTab(undefined);
const onUserSelection = useCallback((tabName?: string) => {
setUserSelected(true);
setSelectedTab(tabName);
}, []);
const back = useCallback(() => {
onUserSelection(undefined);
}, []);
// the userSelected state is used to prevent setting focus when the user
// didn't actually interact (for the first rendering for example)
useEffect(() => {
if (userSelected) {
document.querySelector<HTMLElement>(isMobile
? `.${classes.title}`
: `#${`dialogtab-button-${selectedTab}`}`
)?.focus();
setUserSelected(false);
}
}, [ isMobile, userSelected, selectedTab ]);
const onClose = useCallback(() => {
dispatch(hideDialog());
}, []);
const onClick = useCallback((tabName: string) => () => {
setSelectedTab(tabName);
onUserSelection(tabName);
}, []);
const onTabKeyDown = useCallback((index: number) => (event: React.KeyboardEvent<HTMLDivElement>) => {
let newTab: IDialogTab<any> | null = null;
if (event.key === 'ArrowUp') {
newTab = index === 0 ? tabs[tabs.length - 1] : tabs[index - 1];
}
if (event.key === 'ArrowDown') {
newTab = index === tabs.length - 1 ? tabs[0] : tabs[index + 1];
}
if (newTab !== null) {
onUserSelection(newTab.name);
}
}, [ tabs.length ]);
const onMobileKeyDown = useCallback((tabName: string) => (event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === ' ' || event.key === 'Enter') {
onUserSelection(tabName);
}
}, [ classes.contentContainer ]);
const getTabProps = (tabId: number) => {
const tabConfiguration = tabs[tabId];
const currentTabState = tabStates[tabId];
@ -256,7 +300,7 @@ const DialogWithTabs = ({
const closeIcon = useMemo(() => (
<ClickableIcon
accessibilityLabel = { t('dialog.close') }
accessibilityLabel = { t('dialog.accessibilityLabel.close') }
icon = { IconCloseLarge }
id = 'modal-header-close-button'
onClick = { onClose } />
@ -268,21 +312,39 @@ const DialogWithTabs = ({
onClose = { onClose }
size = 'large'>
{(!isMobile || !selectedTab) && (
<div className = { classes.sidebar }>
<div
aria-orientation = 'vertical'
className = { classes.sidebar }
role = { isMobile ? undefined : 'tablist' }>
<div className = { classes.titleContainer }>
<h2 className = { classes.title }>{t(titleKey ?? '')}</h2>
<MoveFocusInside>
<h2
className = { classes.title }
tabIndex = { -1 }>
{t(titleKey ?? '')}
</h2>
</MoveFocusInside>
{isMobile && closeIcon}
</div>
{tabs.map(tab => {
{tabs.map((tab, index) => {
const label = t(tab.labelKey);
/**
* When not on mobile, the items behave as tabs,
* that's why we set `controls`, `role` and `selected` attributes
* only when not on mobile, they are useful only for the tab behavior.
*/
return (
<ContextMenuItem
accessibilityLabel = { label }
className = { cx(isMobile && classes.menuItemMobile) }
controls = { isMobile ? undefined : `dialogtab-content-${tab.name}` }
icon = { tab.icon }
id = { `dialogtab-button-${tab.name}` }
key = { tab.name }
onClick = { onClick(tab.name) }
onKeyDown = { isMobile ? onMobileKeyDown(tab.name) : onTabKeyDown(index) }
role = { isMobile ? undefined : 'tab' }
selected = { tab.name === selectedTab }
text = { label } />
);
@ -290,25 +352,45 @@ const DialogWithTabs = ({
</div>
)}
{(!isMobile || selectedTab) && (
<div className = { classes.contentContainer }>
<div className = { cx(classes.buttonContainer, classes.closeButtonContainer) }>
{isMobile && (
<div
className = { classes.contentContainer }
tabIndex = { isMobile ? -1 : undefined }>
{/* DOM order is important for keyboard users: show whole heading first when on mobile… */}
{isMobile && (
<div className = { cx(classes.buttonContainer, classes.header) }>
<span className = { classes.backContainer }>
<h2
className = { classes.title }
tabIndex = { -1 }>
{(selectedTabIndex !== null) && t(tabs[selectedTabIndex].labelKey)}
</h2>
<ClickableIcon
accessibilityLabel = { t('dialog.Back') }
icon = { IconArrowBack }
id = 'modal-header-back-button'
onClick = { back } />
<h2 className = { classes.title }>
{(selectedTabIndex !== null) && t(tabs[selectedTabIndex].labelKey)}
</h2>
</span>
)}
{closeIcon}
</div>
<div className = { classes.content }>
{selectedTabComponent}
</div>
{closeIcon}
</div>
)}
{tabs.map(tab => (
<div
aria-labelledby = { isMobile ? undefined : `${tab.name}-button` }
className = { cx(classes.content, tab.name !== selectedTab && 'hide') }
id = { `dialogtab-content-${tab.name}` }
key = { tab.name }
role = { isMobile ? undefined : 'tabpanel' }
tabIndex = { isMobile ? -1 : 0 }>
{ tab.name === selectedTab && selectedTabComponent }
</div>
))}
{/* But show the close button *after* tab panels when not on mobile (using tabs).
This is so that we can tab back and forth tab buttons and tab panels easily. */}
{!isMobile && (
<div className = { cx(classes.buttonContainer, classes.header) }>
{closeIcon}
</div>
)}
<div
className = { cx(classes.buttonContainer, classes.footer) }>
<Button

View File

@ -1,4 +1,4 @@
import React, { useCallback } from 'react';
import React, { useCallback, useEffect } from 'react';
import { makeStyles } from 'tss-react/mui';
import { isMobileBrowser } from '../../../environment/utils';
@ -11,6 +11,7 @@ interface ITabProps {
selected: string;
tabs: Array<{
accessibilityLabel: string;
controlsId: string;
countBadge?: number;
disabled?: boolean;
id: string;
@ -87,26 +88,52 @@ const Tabs = ({
}: ITabProps) => {
const { classes, cx } = useStyles();
const isMobile = isMobileBrowser();
const handleChange = useCallback((e: React.MouseEvent<HTMLButtonElement>) => {
onChange(e.currentTarget.id);
const onClick = useCallback(id => () => {
onChange(id);
}, []);
const onKeyDown = useCallback((index: number) => (event: React.KeyboardEvent<HTMLButtonElement>) => {
let newIndex: number | null = null;
if (event.key === 'ArrowLeft') {
event.preventDefault();
newIndex = index === 0 ? tabs.length - 1 : index - 1;
}
if (event.key === 'ArrowRight') {
event.preventDefault();
newIndex = index === tabs.length - 1 ? 0 : index + 1;
}
if (newIndex !== null) {
onChange(tabs[newIndex].id);
}
}, [ tabs ]);
useEffect(() => {
// this test is needed to make sure the effect is triggered because of user actually changing tab
if (document.activeElement?.getAttribute('role') === 'tab') {
document.querySelector<HTMLButtonElement>(`#${selected}`)?.focus();
}
}, [ selected ]);
return (
<div
aria-label = { accessibilityLabel }
className = { cx(classes.container, className) }
role = 'tablist'>
{tabs.map(tab => (
{tabs.map((tab, index) => (
<button
aria-controls = { tab.controlsId }
aria-label = { tab.accessibilityLabel }
aria-selected = { selected === tab.id }
className = { cx(classes.tab, selected === tab.id && 'selected', isMobile && 'is-mobile') }
disabled = { tab.disabled }
id = { tab.id }
key = { tab.id }
onClick = { handleChange }
role = 'tab'>
onClick = { onClick(tab.id) }
onKeyDown = { onKeyDown(index) }
role = 'tab'
tabIndex = { selected === tab.id ? undefined : -1 }>
{tab.label}
{tab.countBadge && <span className = { classes.badge }>{tab.countBadge}</span>}
</button>

View File

@ -202,7 +202,8 @@ class CalendarList extends AbstractPage<Props> {
className = 'meetings-list-empty-button'
onClick = { this._onOpenSettings }
onKeyPress = { this._onKeyPressOpenSettings }
role = 'button'>
role = 'button'
tabIndex = { 0 }>
<Icon
className = 'meetings-list-empty-icon'
src = { IconCalendar } />

View File

@ -134,35 +134,38 @@ class Chat extends AbstractChat<Props> {
_renderChat() {
const { _isPollsEnabled, _isPollsTabFocused } = this.props;
if (_isPollsTabFocused) {
return (
<>
{ _isPollsEnabled && this._renderTabs() }
<div
aria-labelledby = { CHAT_TABS.POLLS }
id = 'polls-panel'
role = 'tabpanel'>
<PollsPane />
</div>
<KeyboardAvoider />
</>
);
}
return (
<>
{ _isPollsEnabled && this._renderTabs() }
<div
aria-labelledby = { CHAT_TABS.CHAT }
className = { clsx('chat-panel', !_isPollsEnabled && 'chat-panel-no-tabs') }
id = 'chat-panel'
role = 'tabpanel'>
className = { clsx(
'chat-panel',
!_isPollsEnabled && 'chat-panel-no-tabs',
_isPollsTabFocused && 'hide'
) }
id = { `${CHAT_TABS.CHAT}-panel` }
role = 'tabpanel'
tabIndex = { 0 }>
<MessageContainer
messages = { this.props._messages } />
<MessageRecipient />
<ChatInput
onSend = { this._onSendMessage } />
</div>
{ _isPollsEnabled && (
<>
<div
aria-labelledby = { CHAT_TABS.POLLS }
className = { clsx('polls-panel', !_isPollsTabFocused && 'hide') }
id = { `${CHAT_TABS.POLLS}-panel` }
role = 'tabpanel'
tabIndex = { 0 }>
<PollsPane />
</div>
<KeyboardAvoider />
</>
)}
</>
);
}
@ -185,11 +188,13 @@ class Chat extends AbstractChat<Props> {
accessibilityLabel: t('chat.tabs.chat'),
countBadge: _isPollsTabFocused && _nbUnreadMessages > 0 ? _nbUnreadMessages : undefined,
id: CHAT_TABS.CHAT,
controlsId: `${CHAT_TABS.CHAT}-panel`,
label: t('chat.tabs.chat')
}, {
accessibilityLabel: t('chat.tabs.polls'),
countBadge: !_isPollsTabFocused && _nbUnreadPolls > 0 ? _nbUnreadPolls : undefined,
id: CHAT_TABS.POLLS,
controlsId: `${CHAT_TABS.POLLS}-panel`,
label: t('chat.tabs.polls')
}
] } />

View File

@ -115,7 +115,6 @@ class ChatInput extends Component<IProps, IState> {
</div>
)}
<Input
autoFocus = { true }
className = 'chat-input'
icon = { this.props._areSmileysDisabled ? undefined : IconFaceSmile }
iconClick = { this._toggleSmileysPanel }

View File

@ -191,7 +191,7 @@ class DesktopPicker extends PureComponent<IProps, IState> {
* @inheritdoc
*/
render() {
const { selectedTab, selectedSource, sources } = this.state;
const { selectedTab, selectedSource, sources, types } = this.state;
return (
<Dialog
@ -204,14 +204,27 @@ class DesktopPicker extends PureComponent<IProps, IState> {
size = 'large'
titleKey = 'dialog.shareYourScreen'>
{ this._renderTabs() }
<DesktopPickerPane
key = { selectedTab }
onClick = { this._onPreviewClick }
onDoubleClick = { this._onSubmit }
onShareAudioChecked = { this._onShareAudioChecked }
selectedSourceId = { selectedSource.id }
sources = { sources[selectedTab as keyof typeof sources] }
type = { selectedTab } />
{types.map(type => (
<div
aria-labelledby = { `${type}-button` }
className = { selectedTab === type ? undefined : 'hide' }
id = { `${type}-panel` }
key = { type }
role = 'tabpanel'
tabIndex = { 0 }>
{selectedTab === type && (
<DesktopPickerPane
key = { selectedTab }
onClick = { this._onPreviewClick }
onDoubleClick = { this._onSubmit }
onShareAudioChecked = { this._onShareAudioChecked }
selectedSourceId = { selectedSource.id }
sources = { sources[selectedTab as keyof typeof sources] }
type = { selectedTab } />
)}
</div>
))}
</Dialog>
);
}
@ -348,17 +361,18 @@ class DesktopPicker extends PureComponent<IProps, IState> {
type => {
return {
accessibilityLabel: t(TAB_LABELS[type as keyof typeof TAB_LABELS]),
id: type,
id: `${type}-tab`,
controlsId: `${type}-panel`,
label: t(TAB_LABELS[type as keyof typeof TAB_LABELS])
};
});
return (
<Tabs
accessibilityLabel = ''
accessibilityLabel = { t('dialog.sharingTabs') }
className = 'desktop-picker-tabs-container'
onChange = { this._onTabSelected }
selected = { this.state.selectedTab }
selected = { `${this.state.selectedTab}-tab` }
tabs = { tabs } />);
}

View File

@ -40,7 +40,6 @@ const PollsPane = ({ createMode, onCreate, setCreateMode, t }: AbstractProps) =>
<div className = { classes.footer }>
<Button
accessibilityLabel = { t('polls.create.create') }
autoFocus = { true }
fullWidth = { true }
labelKey = { 'polls.create.create' }
onClick = { onCreate } />

View File

@ -1,76 +0,0 @@
// @flow
import React, { Component } from 'react';
/**
* The type of the React {@code Component} props of {@link Tab}.
*/
type Props = {
/**
* The index of the tab.
*/
index: number,
/**
* Indicates if the tab is selected or not.
*/
isSelected: boolean,
/**
* The label of the tab.
*/
label: string,
/**
* Handler for selecting the tab.
*/
onSelect: Function
}
/**
* A React component that implements tabs.
*
*/
export default class Tab extends Component<Props> {
/**
* Initializes a new {@code Tab} instance.
*
* @inheritdoc
*/
constructor(props: Props) {
super(props);
this._onSelect = this._onSelect.bind(this);
}
_onSelect: () => void;
/**
* Selects a tab.
*
* @returns {void}
*/
_onSelect() {
const { index, onSelect } = this.props;
onSelect(index);
}
/**
* Implements the React Components's render method.
*
* @inheritdoc
*/
render() {
const { index, isSelected, label } = this.props;
const className = `tab${isSelected ? ' selected' : ''}`;
return (
<div
className = { className }
key = { index }
onClick = { this._onSelect }>
{ label }
</div>);
}
}

View File

@ -1,7 +1,5 @@
// @flow
import React, { Component } from 'react';
import Tab from './Tab';
import React, { useCallback, useEffect, useState } from 'react';
/**
* The type of the React {@code Component} props of {@link Tabs}.
@ -9,14 +7,10 @@ import Tab from './Tab';
type Props = {
/**
* Handler for selecting the tab.
* Accessibility label for the tabs container.
*
*/
onSelect: Function,
/**
* The index of the selected tab.
*/
selected: number,
accessibilityLabel: string,
/**
* Tabs information.
@ -27,44 +21,87 @@ type Props = {
/**
* A React component that implements tabs.
*
* @returns {ReactElement} The component.
*/
export default class Tabs extends Component<Props> {
static defaultProps = {
tabs: [],
selected: 0
};
const Tabs = ({ accessibilityLabel, tabs }: Props) => {
const [ current, setCurrent ] = useState(0);
/**
* Implements the React Components's render method.
*
* @inheritdoc
*/
render() {
const { onSelect, selected, tabs } = this.props;
const { content = null } = tabs.length
? tabs[Math.min(selected, tabs.length - 1)]
: {};
const onClick = useCallback(index => event => {
event.preventDefault();
setCurrent(index);
}, []);
return (
<div className = 'tab-container'>
{ tabs.length > 1 ? (
<div className = 'tab-buttons'>
{
tabs.map((tab, index) => (
<Tab
index = { index }
isSelected = { index === selected }
key = { index }
label = { tab.label }
onSelect = { onSelect } />
))
}
</div>) : null
}
<div className = 'tab-content'>
{ content }
</div>
</div>
);
}
}
const onKeyDown = useCallback(index => event => {
let newIndex = null;
if (event.key === 'ArrowLeft') {
event.preventDefault();
newIndex = index === 0 ? tabs.length - 1 : index - 1;
}
if (event.key === 'ArrowRight') {
event.preventDefault();
newIndex = index === tabs.length - 1 ? 0 : index + 1;
}
if (newIndex !== null) {
setCurrent(newIndex);
}
}, [ tabs ]);
useEffect(() => {
// this test is needed to make sure the effect is triggered because of user actually changing tab
if (document.activeElement?.getAttribute('role') === 'tab') {
document.querySelector(`#${`${tabs[current].id}-tab`}`)?.focus();
}
}, [ current, tabs ]);
return (
<div className = 'tab-container'>
{ tabs.length > 1
? (
<>
<div
aria-label = { accessibilityLabel }
className = 'tab-buttons'
role = 'tablist'>
{tabs.map((tab, index) => (
<button
aria-controls = { `${tab.id}-panel` }
aria-selected = { current === index ? 'true' : 'false' }
id = { `${tab.id}-tab` }
key = { tab.id }
onClick = { onClick(index) }
onKeyDown = { onKeyDown(index) }
role = 'tab'
tabIndex = { current === index ? undefined : -1 }>
{tab.label}
</button>
))}
</div>
{tabs.map((tab, index) => (
<div
aria-labelledby = { `${tab.id}-tab` }
className = { current === index ? 'tab-content' : 'hide' }
id = { `${tab.id}-panel` }
key = { tab.id }
role = 'tabpanel'
tabIndex = { 0 }>
{tab.content}
</div>
))}
</>
)
: (
<>
<h2 className = 'sr-only'>{accessibilityLabel}</h2>
<div className = 'tab-content'>{tabs[0].content}</div>
</>
)
}
</div>
);
};
export default Tabs;

View File

@ -49,8 +49,7 @@ class WelcomePage extends AbstractWelcomePage {
...this.state,
generateRoomnames:
interfaceConfig.GENERATE_ROOMNAMES_ON_WELCOME_PAGE,
selectedTab: 0
interfaceConfig.GENERATE_ROOMNAMES_ON_WELCOME_PAGE
};
/**
@ -114,7 +113,6 @@ class WelcomePage extends AbstractWelcomePage {
this._setRoomInputRef = this._setRoomInputRef.bind(this);
this._setAdditionalToolbarContentRef
= this._setAdditionalToolbarContentRef.bind(this);
this._onTabSelected = this._onTabSelected.bind(this);
this._renderFooter = this._renderFooter.bind(this);
}
@ -326,18 +324,6 @@ class WelcomePage extends AbstractWelcomePage {
super._onRoomChange(event.target.value);
}
/**
* Callback invoked when the desired tab to display should be changed.
*
* @param {number} tabIndex - The index of the tab within the array of
* displayed tabs.
* @private
* @returns {void}
*/
_onTabSelected(tabIndex) {
this.setState({ selectedTab: tabIndex });
}
/**
* Renders the footer.
*
@ -405,6 +391,7 @@ class WelcomePage extends AbstractWelcomePage {
if (_calendarEnabled) {
tabs.push({
id: 'calendar',
label: t('welcomepage.upcomingMeetings'),
content: <CalendarList />
});
@ -412,6 +399,7 @@ class WelcomePage extends AbstractWelcomePage {
if (_recentListEnabled) {
tabs.push({
id: 'recent',
label: t('welcomepage.recentMeetings'),
content: <RecentList />
});
@ -423,9 +411,9 @@ class WelcomePage extends AbstractWelcomePage {
return (
<Tabs
onSelect = { this._onTabSelected }
selected = { this.state.selectedTab }
tabs = { tabs } />);
accessibilityLabel = { t('welcomepage.meetingsAccessibilityLabel') }
tabs = { tabs } />
);
}
/**