snapshot of https://github.com/jitsi/jitsi-meet.git @ fd1749dd28f4138aa80234fc7f311b19aaffa0e7 (2026-09-15)
This commit is contained in:
167
react/features/base/ui/components/web/Tabs.tsx
Normal file
167
react/features/base/ui/components/web/Tabs.tsx
Normal file
@@ -0,0 +1,167 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { isMobileBrowser } from '../../../environment/utils';
|
||||
import Icon from '../../../icons/components/Icon';
|
||||
|
||||
interface ITabProps {
|
||||
accessibilityLabel: string;
|
||||
className?: string;
|
||||
onChange: (id: string) => void;
|
||||
selected: string;
|
||||
tabs: Array<{
|
||||
accessibilityLabel: string;
|
||||
controlsId: string;
|
||||
countBadge?: number;
|
||||
disabled?: boolean;
|
||||
icon?: Function;
|
||||
id: string;
|
||||
label?: string;
|
||||
title?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
container: {
|
||||
display: 'flex'
|
||||
},
|
||||
|
||||
tab: {
|
||||
...theme.typography.bodyShortBold,
|
||||
color: theme.palette.tabText,
|
||||
flex: 1,
|
||||
padding: '14px',
|
||||
background: 'none',
|
||||
border: 0,
|
||||
appearance: 'none',
|
||||
borderBottom: `2px solid ${theme.palette.tabBorder}`,
|
||||
transition: 'color, border-color 0.2s',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: 0,
|
||||
|
||||
'&:hover': {
|
||||
color: theme.palette.tabTextHover,
|
||||
borderColor: theme.palette.tabBorderHover
|
||||
},
|
||||
|
||||
'&.focus-visible': {
|
||||
outline: 0,
|
||||
boxShadow: `0px 0px 0px 2px ${theme.palette.tabFocus}`,
|
||||
border: 0,
|
||||
color: theme.palette.tabTextSelected
|
||||
},
|
||||
|
||||
'&.selected': {
|
||||
color: theme.palette.tabTextSelected,
|
||||
borderColor: theme.palette.tabBorderSelected
|
||||
},
|
||||
|
||||
'&:disabled': {
|
||||
color: theme.palette.tabTextDisabled,
|
||||
borderColor: theme.palette.tabBorderDisabled
|
||||
},
|
||||
|
||||
'&.is-mobile': {
|
||||
...theme.typography.bodyShortBoldLarge
|
||||
}
|
||||
},
|
||||
|
||||
badge: {
|
||||
...theme.typography.labelBold,
|
||||
alignItems: 'center',
|
||||
backgroundColor: theme.palette.tabBadgeBackground,
|
||||
borderRadius: theme.spacing(2),
|
||||
color: theme.palette.tabBadgeText,
|
||||
display: 'inline-flex',
|
||||
height: theme.spacing(3),
|
||||
justifyContent: 'center',
|
||||
marginLeft: theme.spacing(2),
|
||||
minWidth: theme.spacing(2),
|
||||
padding: `0 ${theme.spacing(1)}`
|
||||
},
|
||||
|
||||
icon: {
|
||||
marginRight: theme.spacing(1)
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
const Tabs = ({
|
||||
accessibilityLabel,
|
||||
className,
|
||||
onChange,
|
||||
selected,
|
||||
tabs
|
||||
}: ITabProps) => {
|
||||
const { classes, cx } = useStyles();
|
||||
const isMobile = isMobileBrowser();
|
||||
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, 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 = { onClick(tab.id) }
|
||||
onKeyDown = { onKeyDown(index) }
|
||||
role = 'tab'
|
||||
tabIndex = { selected === tab.id ? undefined : -1 }
|
||||
title = { tab.title }>
|
||||
{
|
||||
tab.icon && <Icon
|
||||
className = { classes.icon }
|
||||
src = { tab.icon } />
|
||||
}
|
||||
{ tab.label }
|
||||
{
|
||||
tab.countBadge && <span className = { classes.badge }>
|
||||
{ tab.countBadge }
|
||||
</span>
|
||||
}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tabs;
|
||||
Reference in New Issue
Block a user