Skip to content
Open
79 changes: 27 additions & 52 deletions src/components/scrollable_tabs/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,32 @@ import { Box, Tab, Tabs, tabsClasses } from '@mui/material';
import { useBreakpoints } from '@hooks/index';
import { CustomTabPanel } from '@components/tabs';
import { CustomTabProps } from '@components/tabs/index.types';
import { tabsSharedStyles } from '@components/tabs/index.styles';
import TabLabel from '@components/tabs/tab_label';
import ArrowBackIosIcon from '@mui/icons-material/ArrowBackIos';
import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';
import Typography from '@components/typography';

const TAB_HEIGHT = '48px';

/**
* Component that renders scrollable tabs.
*
*/
function ScrollableTabs({
tabs,
value,
indicatorMode,
onChange,
className,
variant = 'scrollable',
minHeight = '48px',
tabsCountOnScreen = 0,
layout = 'auto',
appearance = 'chip',
sx,
}: CustomTabProps) {
const { tabletDown } = useBreakpoints();
const { tabletDown, tabletUp } = useBreakpoints();

const variant = layout === 'stretch' && tabletUp ? 'fullWidth' : 'scrollable';

const hasIndicator = appearance === 'plain';

const [valueOfActivePanel, setValueOfActivePanel] = useState(value ?? false);

Expand All @@ -39,7 +45,7 @@ function ScrollableTabs({
* @param newValue The new value of the active tab.
*/
const handleChange = (event: SyntheticEvent, newValue: number) => {
if (!indicatorMode) {
if (!hasIndicator) {
event.preventDefault();
}

Expand Down Expand Up @@ -69,12 +75,12 @@ function ScrollableTabs({
className={className}
slotProps={{
indicator: {
hidden: !indicatorMode,
hidden: !hasIndicator,
sx: {
backgroundColor: indicatorMode
backgroundColor: hasIndicator
? 'var(--accent-main)'
: 'transparent',
borderRadius: indicatorMode && '16px 16px 0px 0px',
borderRadius: hasIndicator && '16px 16px 0px 0px',
height: '4px',
},
},
Expand All @@ -89,25 +95,7 @@ function ScrollableTabs({
color: 'var(--accent-main)',
'&.Mui-disabled': { opacity: 0.3 },
},
'& button.Mui-selected': {
color: 'var(--accent-main)',
background: 'var(--accent-150)',
borderRadius: 'var(--radius-max)',
},
'& .MuiTab-root:not(.Mui-selected)': {
color: 'var(--grey-350)',
'&:hover': {
background: 'var(--accent-100)',
borderRadius: 'var(--radius-max)',
},
},
// Programatically changing color of ripple (wave) when click happens:
'& span.MuiTouchRipple-rippleVisible': {
color: 'var(--accent-main)',
},
'& span.MuiTouchRipple-root': {
borderRadius: 'var(--radius-max)',
},
...tabsSharedStyles(appearance),
'& .MuiSvgIcon-root g path': {
fill: 'var(--accent-400)',
},
Expand All @@ -117,46 +105,33 @@ function ScrollableTabs({
'& .MuiTabScrollButton-root': {
width: 'auto !important',
height: '36px',
borderRadius: 'var(--radius-max)',
transition: 'background-color 0.15s ease-in-out',
'&:hover': {
backgroundColor: 'var(--accent-200)',
},
'&:active': {
backgroundColor: 'var(--accent-150)',
},
},
alignItems: 'center',
minHeight,
minHeight: TAB_HEIGHT,
[`& .${tabsClasses.flexContainer}`]: {
gap: '4px',
},
...sx,
}}
>
{tabs.map(
({ label, icon, className }, index): ReactNode => (
({ label, badge, icon, className }, index): ReactNode => (
<Tab
label={label}
label={
<TabLabel
label={label}
badge={badge}
selected={valueOfActivePanel === index}
/>
}
key={index}
className={className}
icon={icon}
iconPosition="end"
sx={{
minHeight,
height: minHeight,
width:
tabsCountOnScreen !== 0
? `calc(100% / ${tabsCountOnScreen})`
: 'auto',
minHeight: TAB_HEIGHT,
height: TAB_HEIGHT,
minWidth: '20px',
fontSize: 16,
textTransform: 'none',
':not(&.Mui-selected)': { fontWeight: 400 },
'&.Mui-Selected': {
fontWeight: 600,
fontSize: 18,
},
}}
/>
)
Expand Down
4 changes: 0 additions & 4 deletions src/components/tab_label_with_badge/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ const LabelBadge = ({
alignItems: 'center',
height: '24px',
fontSize: '14px',
opacity: 1,
transition: 'opacity 0.2s',
}}
>
<Typography className="body-small-semibold" sx={{ color: badgeColor }}>
Expand Down Expand Up @@ -48,8 +46,6 @@ const TabLabelWithBadge = ({
display: 'flex',
alignItems: 'center',
gap: '8px',
transform: count === 0 && 'translateX(12px)',
transition: 'transform 0.2s',
userSelect: 'none',
}}
>
Expand Down
4 changes: 1 addition & 3 deletions src/components/tab_switcher/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ const TabSwitcher = <T extends string = string>({
<ButtonBase
key={option.value}
role="tab"
className={isActive ? 'h4' : 'body-regular'}
aria-selected={isActive}
disabled={option.disabled}
disableRipple
Expand All @@ -69,9 +70,6 @@ const TabSwitcher = <T extends string = string>({
minHeight: '28px',
borderRadius: 'var(--radius-m)',
fontFamily: 'inherit',
fontSize: '15px',
lineHeight: '20px',
fontWeight: isActive ? 500 : 400,
color: isActive ? 'var(--accent-dark)' : 'var(--accent-400)',
transition: 'color 0.16s ease-out',
'&.Mui-disabled': { opacity: 0.5 },
Expand Down
35 changes: 35 additions & 0 deletions src/components/tabs/index.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { SxProps, Theme } from '@mui/material';
import { TabsAppearance } from './index.types';

// no default here on purpose: each tabs component states its own, so a missing
// prop cannot quietly pick an appearance for the caller
export const tabsSharedStyles = (
appearance: TabsAppearance
): SxProps<Theme> => {
const radius =
appearance === 'chip' ? 'var(--radius-max)' : 'var(--radius-s)';

return {
'& .MuiTab-root': {
borderRadius: radius,
textTransform: 'none',
transition: 'background-color 0.2s',
},
'& .MuiTab-root:hover': {
backgroundColor: 'color-mix(in srgb, var(--accent-150) 38%, transparent)',
},
'& button.Mui-selected': {
color: 'var(--accent-main)',
background: appearance === 'chip' ? 'var(--accent-150)' : 'unset',
},
'& button:not(.Mui-selected)': {
color: 'var(--grey-350)',
},
'& .MuiTouchRipple-root': {
borderRadius: radius,
},
'& span.MuiTouchRipple-rippleVisible': {
color: 'var(--accent-main)',
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
},
};
};
25 changes: 14 additions & 11 deletions src/components/tabs/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { ReactNode, SyntheticEvent, useEffect, useState } from 'react';
import { Tabs as MUITabs, Tab, Box } from '@mui/material';
import { TabsPanelProps, CustomTabProps } from './index.types';
import { tabsSharedStyles } from './index.styles';
import TabLabel from './tab_label';
import useBreakpoints from '@hooks/useBreakpoints';

/**
Expand Down Expand Up @@ -49,6 +51,8 @@ const Tabs = ({
onChange,
actionComponent,
showTabs = true,
// the plain look is what every caller renders today; a chip is opt-in
appearance = 'plain',
}: CustomTabProps) => {
const [valueOfActivePanel, setValueOfActivePanel] = useState(value || 0);
const { tabletDown } = useBreakpoints();
Expand Down Expand Up @@ -94,21 +98,20 @@ const Tabs = ({
},
},
}}
sx={{
'& button.Mui-selected': { color: 'var(--accent-main)' },
'& button:not(.Mui-selected)': { color: 'var(--grey-350)' },
// Programatically changing color of ripple (wave) when click happens:
'& span.MuiTouchRipple-rippleVisible': {
color: 'var(--accent-main)',
},
}}
sx={tabsSharedStyles(appearance)}
>
{tabs.map(
({ label, className }, index): ReactNode => (
({ label, badge, className }, index): ReactNode => (
<Tab
label={label}
label={
<TabLabel
label={label}
badge={badge}
selected={valueOfActivePanel === index}
/>
}
key={index}
className={`${valueOfActivePanel === index ? 'h4' : 'body-regular'} ${className}`}
className={className}
{...a11yProps(index)}
/>
)
Expand Down
49 changes: 28 additions & 21 deletions src/components/tabs/index.types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ReactElement, ReactNode } from 'react';
import { SxProps, TabOwnProps, TabsOwnProps, Theme } from '@mui/material';
import { SxProps, TabOwnProps, Theme } from '@mui/material';

/**
* Props for the TabsPanel component.
Expand All @@ -21,10 +21,30 @@ export type TabsPanelProps = {
value: number | boolean;
};

/**
* The look of the tabs.
* - `chip` - fully rounded tabs, used for the date and period selectors.
* - `plain` - lightly rounded tabs, used wherever tabs switch a page section.
*/
export type TabsAppearance = 'chip' | 'plain';

/**
* The width of the tabs.
* - `auto` - every tab takes the width of its own label.
* - `stretch` - the tabs share the full width from the tablet breakpoint up,
* and fall back to `auto` on smaller screens.
*/
export type TabsLayout = 'auto' | 'stretch';

/**
* Custom props for the Tab component.
*/
export interface CustomTabProps extends TabOwnProps {
/**
* The look of the tabs. (Default: chip)
*/
appearance?: TabsAppearance;

/**
* An array of tabs containing label and corresponding component.
*/
Expand All @@ -34,6 +54,11 @@ export interface CustomTabProps extends TabOwnProps {
*/
label: string | ReactNode;

/**
* The number displayed in a badge next to the label.
*/
badge?: number;

/**
* The component to be rendered in the tab.
*/
Expand All @@ -52,11 +77,6 @@ export interface CustomTabProps extends TabOwnProps {
*/
value?: number | boolean;

/**
* A boolean indicating whether to display the indicator.
*/
indicatorMode?: boolean;

/**
* Callback function triggered when the active tab changes.
* Provides the new active tab index as a parameter.
Expand All @@ -71,22 +91,9 @@ export interface CustomTabProps extends TabOwnProps {
className?: string;

/**
* The variant of the tabs, aligning with the MUI `TabsOwnProps` variant.
* Examples include `"scrollable"` or `"standard"`.
*/
variant?: TabsOwnProps['variant'];

/**
* Minimum height for the tab component.
* Useful for ensuring consistent tab sizes.
*/
minHeight?: string;

/**
* The number of tabs that should be displayed on the screen at once.
* Useful for responsive layouts or custom tab implementations.
* The width of the tabs. (Default: auto)
*/
tabsCountOnScreen?: number;
layout?: TabsLayout;

/**
* The action component to be displayed with the tab.
Expand Down
Loading
Loading