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:
parent
0d0bec3aad
commit
f727b9295f
|
@ -82,6 +82,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.left-column {
|
.left-column {
|
||||||
|
order: -1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
|
@ -92,6 +93,7 @@
|
||||||
.right-column {
|
.right-column {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
padding-left: 16px;
|
padding-left: 16px;
|
||||||
padding-top: 13px;
|
padding-top: 13px;
|
||||||
|
@ -99,11 +101,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
padding-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.subtitle {
|
.subtitle {
|
||||||
color: #5E6D7A;
|
color: #5E6D7A;
|
||||||
|
@ -125,8 +127,7 @@
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.with-click-handler:hover,
|
&.with-click-handler:hover {
|
||||||
&.with-click-handler:focus {
|
|
||||||
background-color: #c7ddff;
|
background-color: #c7ddff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
#polls-panel {
|
.polls-panel {
|
||||||
height: calc(100% - 119px);
|
height: calc(100% - 119px);
|
||||||
}
|
}
|
||||||
|
|
|
@ -167,7 +167,7 @@ body.welcome-page {
|
||||||
margin: 4px;
|
margin: 4px;
|
||||||
display: $welcomePageTabButtonsDisplay;
|
display: $welcomePageTabButtonsDisplay;
|
||||||
|
|
||||||
.tab {
|
[role="tab"] {
|
||||||
background-color: #c7ddff;
|
background-color: #c7ddff;
|
||||||
border-radius: 7px;
|
border-radius: 7px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
@ -176,8 +176,10 @@ body.welcome-page {
|
||||||
margin: 2px;
|
margin: 2px;
|
||||||
padding: 7px 0;
|
padding: 7px 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
color: inherit;
|
||||||
|
border: 0;
|
||||||
|
|
||||||
&.selected {
|
&[aria-selected="true"] {
|
||||||
background-color: #FFF;
|
background-color: #FFF;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -240,7 +240,9 @@
|
||||||
"WaitingForHostTitle": "Waiting for the host ...",
|
"WaitingForHostTitle": "Waiting for the host ...",
|
||||||
"Yes": "Yes",
|
"Yes": "Yes",
|
||||||
"accessibilityLabel": {
|
"accessibilityLabel": {
|
||||||
"liveStreaming": "Live Stream"
|
"close": "Close dialog",
|
||||||
|
"liveStreaming": "Live Stream",
|
||||||
|
"sharingTabs": "Sharing options"
|
||||||
},
|
},
|
||||||
"add": "Add",
|
"add": "Add",
|
||||||
"addMeetingNote": "Add a note about this meeting",
|
"addMeetingNote": "Add a note about this meeting",
|
||||||
|
@ -1387,6 +1389,7 @@
|
||||||
"microsoftLogo": "Microsoft logo",
|
"microsoftLogo": "Microsoft logo",
|
||||||
"policyLogo": "Policy logo"
|
"policyLogo": "Policy logo"
|
||||||
},
|
},
|
||||||
|
"meetingsAccessibilityLabel": "Meetings",
|
||||||
"mobileDownLoadLinkAndroid": "Download mobile app for Android",
|
"mobileDownLoadLinkAndroid": "Download mobile app for Android",
|
||||||
"mobileDownLoadLinkFDroid": "Download mobile app for F-Droid",
|
"mobileDownLoadLinkFDroid": "Download mobile app for F-Droid",
|
||||||
"mobileDownLoadLinkIos": "Download mobile app for iOS",
|
"mobileDownLoadLinkIos": "Download mobile app for iOS",
|
||||||
|
|
|
@ -105,17 +105,14 @@ class MeetingsList extends Component<Props> {
|
||||||
* @returns {React.ReactNode}
|
* @returns {React.ReactNode}
|
||||||
*/
|
*/
|
||||||
render() {
|
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 there are no recent meetings we don't want to display anything.
|
||||||
*/
|
*/
|
||||||
if (meetings) {
|
if (meetings) {
|
||||||
return (
|
return (
|
||||||
<Container
|
<Container className = 'meetings-list'>
|
||||||
aria-label = { t('welcomepage.recentList') }
|
|
||||||
className = 'meetings-list'
|
|
||||||
tabIndex = '-1'>
|
|
||||||
{
|
{
|
||||||
meetings.length === 0
|
meetings.length === 0
|
||||||
? listEmptyComponent
|
? listEmptyComponent
|
||||||
|
@ -237,23 +234,16 @@ class MeetingsList extends Component<Props> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container
|
<Container
|
||||||
aria-label = { title }
|
|
||||||
className = { rootClassName }
|
className = { rootClassName }
|
||||||
key = { index }
|
key = { index }
|
||||||
onClick = { onPress }
|
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>
|
|
||||||
<Container className = 'right-column'>
|
<Container className = 'right-column'>
|
||||||
<Text className = 'title'>
|
<Text
|
||||||
|
className = 'title'
|
||||||
|
onClick = { onPress }
|
||||||
|
onKeyPress = { onKeyPress }
|
||||||
|
role = 'button'
|
||||||
|
tabIndex = { 0 }>
|
||||||
{ title }
|
{ title }
|
||||||
</Text>
|
</Text>
|
||||||
{
|
{
|
||||||
|
@ -269,6 +259,14 @@ class MeetingsList extends Component<Props> {
|
||||||
</Text>) : null
|
</Text>) : null
|
||||||
}
|
}
|
||||||
</Container>
|
</Container>
|
||||||
|
<Container className = 'left-column'>
|
||||||
|
<Text className = 'title'>
|
||||||
|
{ _toDateString(date) }
|
||||||
|
</Text>
|
||||||
|
<Text className = 'subtitle'>
|
||||||
|
{ _toTimeString(time) }
|
||||||
|
</Text>
|
||||||
|
</Container>
|
||||||
<Container className = 'actions'>
|
<Container className = 'actions'>
|
||||||
{ elementAfter || null }
|
{ elementAfter || null }
|
||||||
|
|
||||||
|
|
|
@ -26,6 +26,13 @@ export interface IProps {
|
||||||
*/
|
*/
|
||||||
className?: string;
|
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.
|
* Custom icon. If used, the icon prop is ignored.
|
||||||
* Used to allow custom children instead of just the default icons.
|
* Used to allow custom children instead of just the default icons.
|
||||||
|
@ -55,7 +62,7 @@ export interface IProps {
|
||||||
/**
|
/**
|
||||||
* Keydown handler.
|
* Keydown handler.
|
||||||
*/
|
*/
|
||||||
onKeyDown?: (e?: React.KeyboardEvent) => void;
|
onKeyDown?: (e: React.KeyboardEvent<HTMLDivElement>) => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Keypress handler.
|
* Keypress handler.
|
||||||
|
@ -67,6 +74,11 @@ export interface IProps {
|
||||||
*/
|
*/
|
||||||
overflowType?: TEXT_OVERFLOW_TYPES;
|
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.
|
* Whether the item is marked as selected.
|
||||||
*/
|
*/
|
||||||
|
@ -150,6 +162,7 @@ const ContextMenuItem = ({
|
||||||
accessibilityLabel,
|
accessibilityLabel,
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
|
controls,
|
||||||
customIcon,
|
customIcon,
|
||||||
disabled,
|
disabled,
|
||||||
id,
|
id,
|
||||||
|
@ -158,6 +171,7 @@ const ContextMenuItem = ({
|
||||||
onKeyDown,
|
onKeyDown,
|
||||||
onKeyPress,
|
onKeyPress,
|
||||||
overflowType,
|
overflowType,
|
||||||
|
role = 'button',
|
||||||
selected,
|
selected,
|
||||||
testId,
|
testId,
|
||||||
text,
|
text,
|
||||||
|
@ -167,8 +181,10 @@ const ContextMenuItem = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
aria-controls = { controls }
|
||||||
aria-disabled = { disabled }
|
aria-disabled = { disabled }
|
||||||
aria-label = { accessibilityLabel }
|
aria-label = { accessibilityLabel }
|
||||||
|
aria-selected = { role === 'tab' ? selected : undefined }
|
||||||
className = { cx(styles.contextMenuItem,
|
className = { cx(styles.contextMenuItem,
|
||||||
_overflowDrawer && styles.contextMenuItemDrawer,
|
_overflowDrawer && styles.contextMenuItemDrawer,
|
||||||
disabled && styles.contextMenuItemDisabled,
|
disabled && styles.contextMenuItemDisabled,
|
||||||
|
@ -181,8 +197,11 @@ const ContextMenuItem = ({
|
||||||
onClick = { disabled ? undefined : onClick }
|
onClick = { disabled ? undefined : onClick }
|
||||||
onKeyDown = { disabled ? undefined : onKeyDown }
|
onKeyDown = { disabled ? undefined : onKeyDown }
|
||||||
onKeyPress = { disabled ? undefined : onKeyPress }
|
onKeyPress = { disabled ? undefined : onKeyPress }
|
||||||
role = 'button'
|
role = { role }
|
||||||
tabIndex = { disabled ? undefined : 0 }>
|
tabIndex = { role === 'tab'
|
||||||
|
? selected ? 0 : -1
|
||||||
|
: disabled ? undefined : 0
|
||||||
|
}>
|
||||||
{customIcon ? customIcon
|
{customIcon ? customIcon
|
||||||
: icon && <Icon
|
: icon && <Icon
|
||||||
className = { styles.contextMenuItemIcon }
|
className = { styles.contextMenuItemIcon }
|
||||||
|
|
|
@ -131,7 +131,7 @@ const Dialog = ({
|
||||||
</p>
|
</p>
|
||||||
{!hideCloseButton && (
|
{!hideCloseButton && (
|
||||||
<ClickableIcon
|
<ClickableIcon
|
||||||
accessibilityLabel = { t('dialog.close') }
|
accessibilityLabel = { t('dialog.accessibilityLabel.close') }
|
||||||
icon = { IconCloseLarge }
|
icon = { IconCloseLarge }
|
||||||
id = 'modal-header-close-button'
|
id = 'modal-header-close-button'
|
||||||
onClick = { onClose } />
|
onClick = { onClose } />
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import React, { ComponentType, useCallback, useEffect, useMemo, useState } from 'react';
|
import React, { ComponentType, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { MoveFocusInside } from 'react-focus-lock';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { makeStyles } from 'tss-react/mui';
|
import { makeStyles } from 'tss-react/mui';
|
||||||
|
@ -89,10 +90,6 @@ const useStyles = makeStyles()(theme => {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
closeButtonContainer: {
|
|
||||||
paddingBottom: theme.spacing(4)
|
|
||||||
},
|
|
||||||
|
|
||||||
buttonContainer: {
|
buttonContainer: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
boxSizing: 'border-box',
|
boxSizing: 'border-box',
|
||||||
|
@ -109,6 +106,7 @@ const useStyles = makeStyles()(theme => {
|
||||||
|
|
||||||
backContainer: {
|
backContainer: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
flexDirection: 'row-reverse',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
|
||||||
'& > button': {
|
'& > button': {
|
||||||
|
@ -127,6 +125,11 @@ const useStyles = makeStyles()(theme => {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
header: {
|
||||||
|
order: -1,
|
||||||
|
paddingBottom: theme.spacing(4)
|
||||||
|
},
|
||||||
|
|
||||||
footer: {
|
footer: {
|
||||||
justifyContent: 'flex-end',
|
justifyContent: 'flex-end',
|
||||||
paddingTop: theme.spacing(4),
|
paddingTop: theme.spacing(4),
|
||||||
|
@ -168,6 +171,7 @@ const DialogWithTabs = ({
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [ selectedTab, setSelectedTab ] = useState<string | undefined>(defaultTab ?? tabs[0].name);
|
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 [ tabStates, setTabStates ] = useState(tabs.map(tab => tab.props));
|
||||||
const clientWidth = useSelector((state: IReduxState) => state['features/base/responsive-ui'].clientWidth);
|
const clientWidth = useSelector((state: IReduxState) => state['features/base/responsive-ui'].clientWidth);
|
||||||
const [ isMobile, setIsMobile ] = useState(false);
|
const [ isMobile, setIsMobile ] = useState(false);
|
||||||
|
@ -188,18 +192,58 @@ const DialogWithTabs = ({
|
||||||
}
|
}
|
||||||
}, [ isMobile ]);
|
}, [ isMobile ]);
|
||||||
|
|
||||||
const back = useCallback(() => {
|
const onUserSelection = useCallback((tabName?: string) => {
|
||||||
setSelectedTab(undefined);
|
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(() => {
|
const onClose = useCallback(() => {
|
||||||
dispatch(hideDialog());
|
dispatch(hideDialog());
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onClick = useCallback((tabName: string) => () => {
|
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 getTabProps = (tabId: number) => {
|
||||||
const tabConfiguration = tabs[tabId];
|
const tabConfiguration = tabs[tabId];
|
||||||
const currentTabState = tabStates[tabId];
|
const currentTabState = tabStates[tabId];
|
||||||
|
@ -256,7 +300,7 @@ const DialogWithTabs = ({
|
||||||
|
|
||||||
const closeIcon = useMemo(() => (
|
const closeIcon = useMemo(() => (
|
||||||
<ClickableIcon
|
<ClickableIcon
|
||||||
accessibilityLabel = { t('dialog.close') }
|
accessibilityLabel = { t('dialog.accessibilityLabel.close') }
|
||||||
icon = { IconCloseLarge }
|
icon = { IconCloseLarge }
|
||||||
id = 'modal-header-close-button'
|
id = 'modal-header-close-button'
|
||||||
onClick = { onClose } />
|
onClick = { onClose } />
|
||||||
|
@ -268,21 +312,39 @@ const DialogWithTabs = ({
|
||||||
onClose = { onClose }
|
onClose = { onClose }
|
||||||
size = 'large'>
|
size = 'large'>
|
||||||
{(!isMobile || !selectedTab) && (
|
{(!isMobile || !selectedTab) && (
|
||||||
<div className = { classes.sidebar }>
|
<div
|
||||||
|
aria-orientation = 'vertical'
|
||||||
|
className = { classes.sidebar }
|
||||||
|
role = { isMobile ? undefined : 'tablist' }>
|
||||||
<div className = { classes.titleContainer }>
|
<div className = { classes.titleContainer }>
|
||||||
<h2 className = { classes.title }>{t(titleKey ?? '')}</h2>
|
<MoveFocusInside>
|
||||||
|
<h2
|
||||||
|
className = { classes.title }
|
||||||
|
tabIndex = { -1 }>
|
||||||
|
{t(titleKey ?? '')}
|
||||||
|
</h2>
|
||||||
|
</MoveFocusInside>
|
||||||
{isMobile && closeIcon}
|
{isMobile && closeIcon}
|
||||||
</div>
|
</div>
|
||||||
{tabs.map(tab => {
|
{tabs.map((tab, index) => {
|
||||||
const label = t(tab.labelKey);
|
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 (
|
return (
|
||||||
<ContextMenuItem
|
<ContextMenuItem
|
||||||
accessibilityLabel = { label }
|
accessibilityLabel = { label }
|
||||||
className = { cx(isMobile && classes.menuItemMobile) }
|
className = { cx(isMobile && classes.menuItemMobile) }
|
||||||
|
controls = { isMobile ? undefined : `dialogtab-content-${tab.name}` }
|
||||||
icon = { tab.icon }
|
icon = { tab.icon }
|
||||||
|
id = { `dialogtab-button-${tab.name}` }
|
||||||
key = { tab.name }
|
key = { tab.name }
|
||||||
onClick = { onClick(tab.name) }
|
onClick = { onClick(tab.name) }
|
||||||
|
onKeyDown = { isMobile ? onMobileKeyDown(tab.name) : onTabKeyDown(index) }
|
||||||
|
role = { isMobile ? undefined : 'tab' }
|
||||||
selected = { tab.name === selectedTab }
|
selected = { tab.name === selectedTab }
|
||||||
text = { label } />
|
text = { label } />
|
||||||
);
|
);
|
||||||
|
@ -290,25 +352,45 @@ const DialogWithTabs = ({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{(!isMobile || selectedTab) && (
|
{(!isMobile || selectedTab) && (
|
||||||
<div className = { classes.contentContainer }>
|
<div
|
||||||
<div className = { cx(classes.buttonContainer, classes.closeButtonContainer) }>
|
className = { classes.contentContainer }
|
||||||
{isMobile && (
|
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 }>
|
<span className = { classes.backContainer }>
|
||||||
|
<h2
|
||||||
|
className = { classes.title }
|
||||||
|
tabIndex = { -1 }>
|
||||||
|
{(selectedTabIndex !== null) && t(tabs[selectedTabIndex].labelKey)}
|
||||||
|
</h2>
|
||||||
<ClickableIcon
|
<ClickableIcon
|
||||||
accessibilityLabel = { t('dialog.Back') }
|
accessibilityLabel = { t('dialog.Back') }
|
||||||
icon = { IconArrowBack }
|
icon = { IconArrowBack }
|
||||||
id = 'modal-header-back-button'
|
id = 'modal-header-back-button'
|
||||||
onClick = { back } />
|
onClick = { back } />
|
||||||
<h2 className = { classes.title }>
|
|
||||||
{(selectedTabIndex !== null) && t(tabs[selectedTabIndex].labelKey)}
|
|
||||||
</h2>
|
|
||||||
</span>
|
</span>
|
||||||
)}
|
{closeIcon}
|
||||||
{closeIcon}
|
</div>
|
||||||
</div>
|
)}
|
||||||
<div className = { classes.content }>
|
{tabs.map(tab => (
|
||||||
{selectedTabComponent}
|
<div
|
||||||
</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
|
<div
|
||||||
className = { cx(classes.buttonContainer, classes.footer) }>
|
className = { cx(classes.buttonContainer, classes.footer) }>
|
||||||
<Button
|
<Button
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback, useEffect } from 'react';
|
||||||
import { makeStyles } from 'tss-react/mui';
|
import { makeStyles } from 'tss-react/mui';
|
||||||
|
|
||||||
import { isMobileBrowser } from '../../../environment/utils';
|
import { isMobileBrowser } from '../../../environment/utils';
|
||||||
|
@ -11,6 +11,7 @@ interface ITabProps {
|
||||||
selected: string;
|
selected: string;
|
||||||
tabs: Array<{
|
tabs: Array<{
|
||||||
accessibilityLabel: string;
|
accessibilityLabel: string;
|
||||||
|
controlsId: string;
|
||||||
countBadge?: number;
|
countBadge?: number;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -87,26 +88,52 @@ const Tabs = ({
|
||||||
}: ITabProps) => {
|
}: ITabProps) => {
|
||||||
const { classes, cx } = useStyles();
|
const { classes, cx } = useStyles();
|
||||||
const isMobile = isMobileBrowser();
|
const isMobile = isMobileBrowser();
|
||||||
|
const onClick = useCallback(id => () => {
|
||||||
const handleChange = useCallback((e: React.MouseEvent<HTMLButtonElement>) => {
|
onChange(id);
|
||||||
onChange(e.currentTarget.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 (
|
return (
|
||||||
<div
|
<div
|
||||||
aria-label = { accessibilityLabel }
|
aria-label = { accessibilityLabel }
|
||||||
className = { cx(classes.container, className) }
|
className = { cx(classes.container, className) }
|
||||||
role = 'tablist'>
|
role = 'tablist'>
|
||||||
{tabs.map(tab => (
|
{tabs.map((tab, index) => (
|
||||||
<button
|
<button
|
||||||
|
aria-controls = { tab.controlsId }
|
||||||
aria-label = { tab.accessibilityLabel }
|
aria-label = { tab.accessibilityLabel }
|
||||||
aria-selected = { selected === tab.id }
|
aria-selected = { selected === tab.id }
|
||||||
className = { cx(classes.tab, selected === tab.id && 'selected', isMobile && 'is-mobile') }
|
className = { cx(classes.tab, selected === tab.id && 'selected', isMobile && 'is-mobile') }
|
||||||
disabled = { tab.disabled }
|
disabled = { tab.disabled }
|
||||||
id = { tab.id }
|
id = { tab.id }
|
||||||
key = { tab.id }
|
key = { tab.id }
|
||||||
onClick = { handleChange }
|
onClick = { onClick(tab.id) }
|
||||||
role = 'tab'>
|
onKeyDown = { onKeyDown(index) }
|
||||||
|
role = 'tab'
|
||||||
|
tabIndex = { selected === tab.id ? undefined : -1 }>
|
||||||
{tab.label}
|
{tab.label}
|
||||||
{tab.countBadge && <span className = { classes.badge }>{tab.countBadge}</span>}
|
{tab.countBadge && <span className = { classes.badge }>{tab.countBadge}</span>}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -202,7 +202,8 @@ class CalendarList extends AbstractPage<Props> {
|
||||||
className = 'meetings-list-empty-button'
|
className = 'meetings-list-empty-button'
|
||||||
onClick = { this._onOpenSettings }
|
onClick = { this._onOpenSettings }
|
||||||
onKeyPress = { this._onKeyPressOpenSettings }
|
onKeyPress = { this._onKeyPressOpenSettings }
|
||||||
role = 'button'>
|
role = 'button'
|
||||||
|
tabIndex = { 0 }>
|
||||||
<Icon
|
<Icon
|
||||||
className = 'meetings-list-empty-icon'
|
className = 'meetings-list-empty-icon'
|
||||||
src = { IconCalendar } />
|
src = { IconCalendar } />
|
||||||
|
|
|
@ -134,35 +134,38 @@ class Chat extends AbstractChat<Props> {
|
||||||
_renderChat() {
|
_renderChat() {
|
||||||
const { _isPollsEnabled, _isPollsTabFocused } = this.props;
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{ _isPollsEnabled && this._renderTabs() }
|
{ _isPollsEnabled && this._renderTabs() }
|
||||||
<div
|
<div
|
||||||
aria-labelledby = { CHAT_TABS.CHAT }
|
aria-labelledby = { CHAT_TABS.CHAT }
|
||||||
className = { clsx('chat-panel', !_isPollsEnabled && 'chat-panel-no-tabs') }
|
className = { clsx(
|
||||||
id = 'chat-panel'
|
'chat-panel',
|
||||||
role = 'tabpanel'>
|
!_isPollsEnabled && 'chat-panel-no-tabs',
|
||||||
|
_isPollsTabFocused && 'hide'
|
||||||
|
) }
|
||||||
|
id = { `${CHAT_TABS.CHAT}-panel` }
|
||||||
|
role = 'tabpanel'
|
||||||
|
tabIndex = { 0 }>
|
||||||
<MessageContainer
|
<MessageContainer
|
||||||
messages = { this.props._messages } />
|
messages = { this.props._messages } />
|
||||||
<MessageRecipient />
|
<MessageRecipient />
|
||||||
<ChatInput
|
<ChatInput
|
||||||
onSend = { this._onSendMessage } />
|
onSend = { this._onSendMessage } />
|
||||||
</div>
|
</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'),
|
accessibilityLabel: t('chat.tabs.chat'),
|
||||||
countBadge: _isPollsTabFocused && _nbUnreadMessages > 0 ? _nbUnreadMessages : undefined,
|
countBadge: _isPollsTabFocused && _nbUnreadMessages > 0 ? _nbUnreadMessages : undefined,
|
||||||
id: CHAT_TABS.CHAT,
|
id: CHAT_TABS.CHAT,
|
||||||
|
controlsId: `${CHAT_TABS.CHAT}-panel`,
|
||||||
label: t('chat.tabs.chat')
|
label: t('chat.tabs.chat')
|
||||||
}, {
|
}, {
|
||||||
accessibilityLabel: t('chat.tabs.polls'),
|
accessibilityLabel: t('chat.tabs.polls'),
|
||||||
countBadge: !_isPollsTabFocused && _nbUnreadPolls > 0 ? _nbUnreadPolls : undefined,
|
countBadge: !_isPollsTabFocused && _nbUnreadPolls > 0 ? _nbUnreadPolls : undefined,
|
||||||
id: CHAT_TABS.POLLS,
|
id: CHAT_TABS.POLLS,
|
||||||
|
controlsId: `${CHAT_TABS.POLLS}-panel`,
|
||||||
label: t('chat.tabs.polls')
|
label: t('chat.tabs.polls')
|
||||||
}
|
}
|
||||||
] } />
|
] } />
|
||||||
|
|
|
@ -115,7 +115,6 @@ class ChatInput extends Component<IProps, IState> {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Input
|
<Input
|
||||||
autoFocus = { true }
|
|
||||||
className = 'chat-input'
|
className = 'chat-input'
|
||||||
icon = { this.props._areSmileysDisabled ? undefined : IconFaceSmile }
|
icon = { this.props._areSmileysDisabled ? undefined : IconFaceSmile }
|
||||||
iconClick = { this._toggleSmileysPanel }
|
iconClick = { this._toggleSmileysPanel }
|
||||||
|
|
|
@ -191,7 +191,7 @@ class DesktopPicker extends PureComponent<IProps, IState> {
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
render() {
|
render() {
|
||||||
const { selectedTab, selectedSource, sources } = this.state;
|
const { selectedTab, selectedSource, sources, types } = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
|
@ -204,14 +204,27 @@ class DesktopPicker extends PureComponent<IProps, IState> {
|
||||||
size = 'large'
|
size = 'large'
|
||||||
titleKey = 'dialog.shareYourScreen'>
|
titleKey = 'dialog.shareYourScreen'>
|
||||||
{ this._renderTabs() }
|
{ this._renderTabs() }
|
||||||
<DesktopPickerPane
|
{types.map(type => (
|
||||||
key = { selectedTab }
|
<div
|
||||||
onClick = { this._onPreviewClick }
|
aria-labelledby = { `${type}-button` }
|
||||||
onDoubleClick = { this._onSubmit }
|
className = { selectedTab === type ? undefined : 'hide' }
|
||||||
onShareAudioChecked = { this._onShareAudioChecked }
|
id = { `${type}-panel` }
|
||||||
selectedSourceId = { selectedSource.id }
|
key = { type }
|
||||||
sources = { sources[selectedTab as keyof typeof sources] }
|
role = 'tabpanel'
|
||||||
type = { selectedTab } />
|
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>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -348,17 +361,18 @@ class DesktopPicker extends PureComponent<IProps, IState> {
|
||||||
type => {
|
type => {
|
||||||
return {
|
return {
|
||||||
accessibilityLabel: t(TAB_LABELS[type as keyof typeof TAB_LABELS]),
|
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])
|
label: t(TAB_LABELS[type as keyof typeof TAB_LABELS])
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs
|
<Tabs
|
||||||
accessibilityLabel = ''
|
accessibilityLabel = { t('dialog.sharingTabs') }
|
||||||
className = 'desktop-picker-tabs-container'
|
className = 'desktop-picker-tabs-container'
|
||||||
onChange = { this._onTabSelected }
|
onChange = { this._onTabSelected }
|
||||||
selected = { this.state.selectedTab }
|
selected = { `${this.state.selectedTab}-tab` }
|
||||||
tabs = { tabs } />);
|
tabs = { tabs } />);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -40,7 +40,6 @@ const PollsPane = ({ createMode, onCreate, setCreateMode, t }: AbstractProps) =>
|
||||||
<div className = { classes.footer }>
|
<div className = { classes.footer }>
|
||||||
<Button
|
<Button
|
||||||
accessibilityLabel = { t('polls.create.create') }
|
accessibilityLabel = { t('polls.create.create') }
|
||||||
autoFocus = { true }
|
|
||||||
fullWidth = { true }
|
fullWidth = { true }
|
||||||
labelKey = { 'polls.create.create' }
|
labelKey = { 'polls.create.create' }
|
||||||
onClick = { onCreate } />
|
onClick = { onCreate } />
|
||||||
|
|
|
@ -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>);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +1,5 @@
|
||||||
// @flow
|
// @flow
|
||||||
import React, { Component } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import Tab from './Tab';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The type of the React {@code Component} props of {@link Tabs}.
|
* The type of the React {@code Component} props of {@link Tabs}.
|
||||||
|
@ -9,14 +7,10 @@ import Tab from './Tab';
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handler for selecting the tab.
|
* Accessibility label for the tabs container.
|
||||||
|
*
|
||||||
*/
|
*/
|
||||||
onSelect: Function,
|
accessibilityLabel: string,
|
||||||
|
|
||||||
/**
|
|
||||||
* The index of the selected tab.
|
|
||||||
*/
|
|
||||||
selected: number,
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tabs information.
|
* Tabs information.
|
||||||
|
@ -27,44 +21,87 @@ type Props = {
|
||||||
/**
|
/**
|
||||||
* A React component that implements tabs.
|
* A React component that implements tabs.
|
||||||
*
|
*
|
||||||
|
* @returns {ReactElement} The component.
|
||||||
*/
|
*/
|
||||||
export default class Tabs extends Component<Props> {
|
const Tabs = ({ accessibilityLabel, tabs }: Props) => {
|
||||||
static defaultProps = {
|
const [ current, setCurrent ] = useState(0);
|
||||||
tabs: [],
|
|
||||||
selected: 0
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
const onClick = useCallback(index => event => {
|
||||||
* Implements the React Components's render method.
|
event.preventDefault();
|
||||||
*
|
setCurrent(index);
|
||||||
* @inheritdoc
|
}, []);
|
||||||
*/
|
|
||||||
render() {
|
|
||||||
const { onSelect, selected, tabs } = this.props;
|
|
||||||
const { content = null } = tabs.length
|
|
||||||
? tabs[Math.min(selected, tabs.length - 1)]
|
|
||||||
: {};
|
|
||||||
|
|
||||||
return (
|
const onKeyDown = useCallback(index => event => {
|
||||||
<div className = 'tab-container'>
|
let newIndex = null;
|
||||||
{ tabs.length > 1 ? (
|
|
||||||
<div className = 'tab-buttons'>
|
if (event.key === 'ArrowLeft') {
|
||||||
{
|
event.preventDefault();
|
||||||
tabs.map((tab, index) => (
|
newIndex = index === 0 ? tabs.length - 1 : index - 1;
|
||||||
<Tab
|
}
|
||||||
index = { index }
|
|
||||||
isSelected = { index === selected }
|
if (event.key === 'ArrowRight') {
|
||||||
key = { index }
|
event.preventDefault();
|
||||||
label = { tab.label }
|
newIndex = index === tabs.length - 1 ? 0 : index + 1;
|
||||||
onSelect = { onSelect } />
|
}
|
||||||
))
|
|
||||||
}
|
if (newIndex !== null) {
|
||||||
</div>) : null
|
setCurrent(newIndex);
|
||||||
}
|
}
|
||||||
<div className = 'tab-content'>
|
}, [ tabs ]);
|
||||||
{ content }
|
|
||||||
</div>
|
useEffect(() => {
|
||||||
</div>
|
// 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;
|
||||||
|
|
|
@ -49,8 +49,7 @@ class WelcomePage extends AbstractWelcomePage {
|
||||||
...this.state,
|
...this.state,
|
||||||
|
|
||||||
generateRoomnames:
|
generateRoomnames:
|
||||||
interfaceConfig.GENERATE_ROOMNAMES_ON_WELCOME_PAGE,
|
interfaceConfig.GENERATE_ROOMNAMES_ON_WELCOME_PAGE
|
||||||
selectedTab: 0
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -114,7 +113,6 @@ class WelcomePage extends AbstractWelcomePage {
|
||||||
this._setRoomInputRef = this._setRoomInputRef.bind(this);
|
this._setRoomInputRef = this._setRoomInputRef.bind(this);
|
||||||
this._setAdditionalToolbarContentRef
|
this._setAdditionalToolbarContentRef
|
||||||
= this._setAdditionalToolbarContentRef.bind(this);
|
= this._setAdditionalToolbarContentRef.bind(this);
|
||||||
this._onTabSelected = this._onTabSelected.bind(this);
|
|
||||||
this._renderFooter = this._renderFooter.bind(this);
|
this._renderFooter = this._renderFooter.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -326,18 +324,6 @@ class WelcomePage extends AbstractWelcomePage {
|
||||||
super._onRoomChange(event.target.value);
|
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.
|
* Renders the footer.
|
||||||
*
|
*
|
||||||
|
@ -405,6 +391,7 @@ class WelcomePage extends AbstractWelcomePage {
|
||||||
|
|
||||||
if (_calendarEnabled) {
|
if (_calendarEnabled) {
|
||||||
tabs.push({
|
tabs.push({
|
||||||
|
id: 'calendar',
|
||||||
label: t('welcomepage.upcomingMeetings'),
|
label: t('welcomepage.upcomingMeetings'),
|
||||||
content: <CalendarList />
|
content: <CalendarList />
|
||||||
});
|
});
|
||||||
|
@ -412,6 +399,7 @@ class WelcomePage extends AbstractWelcomePage {
|
||||||
|
|
||||||
if (_recentListEnabled) {
|
if (_recentListEnabled) {
|
||||||
tabs.push({
|
tabs.push({
|
||||||
|
id: 'recent',
|
||||||
label: t('welcomepage.recentMeetings'),
|
label: t('welcomepage.recentMeetings'),
|
||||||
content: <RecentList />
|
content: <RecentList />
|
||||||
});
|
});
|
||||||
|
@ -423,9 +411,9 @@ class WelcomePage extends AbstractWelcomePage {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs
|
<Tabs
|
||||||
onSelect = { this._onTabSelected }
|
accessibilityLabel = { t('welcomepage.meetingsAccessibilityLabel') }
|
||||||
selected = { this.state.selectedTab }
|
tabs = { tabs } />
|
||||||
tabs = { tabs } />);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in New Issue