Grafana UI: Tabs - add missing style for disabled tabs (#109907)

* Grafana UI: Tabs - add disabled style

* apply PR feedback
This commit is contained in:
Alexa Vargas
2025-08-21 13:50:22 +02:00
committed by GitHub
parent aade015d96
commit 806872bfce
3 changed files with 94 additions and 4 deletions
@@ -27,11 +27,26 @@ export interface TabProps extends HTMLProps<HTMLElement> {
suffix?: NavModelItem['tabSuffix'];
truncate?: boolean;
tooltip?: string;
/** When true, the tab will be disabled and not clickable */
disabled?: boolean;
}
export const Tab = React.forwardRef<HTMLElement, TabProps>(
(
{ label, active, icon, onChangeTab, counter, suffix: Suffix, className, href, truncate, tooltip, ...otherProps },
{
label,
active,
icon,
onChangeTab,
counter,
suffix: Suffix,
className,
href,
truncate,
tooltip,
disabled,
...otherProps
},
ref
) => {
const tabsStyles = useStyles2(getStyles);
@@ -50,16 +65,19 @@ export const Tab = React.forwardRef<HTMLElement, TabProps>(
clearStyles,
tabsStyles.link,
active ? tabsStyles.activeStyle : tabsStyles.notActive,
truncate && tabsStyles.linkTruncate
truncate && tabsStyles.linkTruncate,
disabled && tabsStyles.disabled
);
const commonProps = {
className: linkClass,
'data-testid': selectors.components.Tab.title(label),
...otherProps,
onClick: onChangeTab,
onClick: disabled ? undefined : onChangeTab,
role: 'tab',
'aria-selected': active,
'aria-disabled': disabled,
tabIndex: disabled ? -1 : undefined,
title: !!tooltip ? undefined : otherProps.title, // If tooltip is provided, don't set the title on the link or button, it looks weird
};
@@ -70,7 +88,7 @@ export const Tab = React.forwardRef<HTMLElement, TabProps>(
<div className={cx(tabsStyles.item, truncate && tabsStyles.itemTruncate, className)}>
<a
{...commonProps}
href={href}
href={disabled ? undefined : href}
// don't think we can avoid the type assertion here :(
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
ref={ref as React.ForwardedRef<HTMLAnchorElement>}
@@ -169,5 +187,17 @@ const getStyles = (theme: GrafanaTheme2) => {
suffix: css({
marginLeft: theme.spacing(1),
}),
disabled: css({
color: theme.colors.text.disabled,
cursor: 'not-allowed',
'&:hover, &:focus': {
color: theme.colors.text.disabled,
'&::before': {
backgroundColor: 'transparent',
},
},
}),
};
};
@@ -58,4 +58,37 @@ Counter.args = {
value: 10,
};
export const WithDisabled: StoryFn = () => {
const [state, updateState] = useState([
{ label: 'Enabled Tab', key: 'first', active: true },
{ label: 'Disabled Tab', key: 'second', active: false, disabled: true },
{ label: 'Another Tab', key: 'third', active: false },
]);
return (
<DashboardStoryCanvas>
<TabsBar>
{state.map((tab, index) => {
return (
<Tab
key={index}
label={tab.label}
active={tab.active}
disabled={tab.disabled}
onChangeTab={() =>
!tab.disabled && updateState(state.map((tab, idx) => ({ ...tab, active: idx === index })))
}
/>
);
})}
</TabsBar>
<TabContent>
{state[0].active && <div>First tab content</div>}
{state[1].active && <div>Second tab content (disabled)</div>}
{state[2].active && <div>Third tab content</div>}
</TabContent>
</DashboardStoryCanvas>
);
};
export default meta;
@@ -14,6 +14,9 @@ const setup = (jsx: JSX.Element) => {
const onChangeTab = jest.fn();
describe('Tabs', () => {
beforeEach(() => {
onChangeTab.mockClear();
});
it('should call onChangeTab when clicking a tab', async () => {
const { user } = setup(
<TabsBar>
@@ -96,4 +99,28 @@ describe('Tabs', () => {
expect(screen.getByTestId('tab-suffix')).toBeInTheDocument();
});
it('should render disabled tab correctly', () => {
render(
<TabsBar>
<Tab label="Disabled Tab" active={false} onChangeTab={onChangeTab} disabled={true} />
</TabsBar>
);
const disabledTab = screen.getByRole('tab', { name: 'Disabled Tab' });
expect(disabledTab).toHaveAttribute('aria-disabled', 'true');
});
it('should not call onChangeTab when disabled tab is clicked', async () => {
const { user } = setup(
<TabsBar>
<Tab label="Disabled Tab" active={false} onChangeTab={onChangeTab} disabled={true} />
</TabsBar>
);
const disabledTab = screen.getByRole('tab', { name: 'Disabled Tab' });
await user.click(disabledTab);
expect(onChangeTab).not.toHaveBeenCalled();
});
});