diff --git a/.changeset/tame-bats-strive.md b/.changeset/tame-bats-strive.md
new file mode 100644
index 0000000000..1aa369f5d1
--- /dev/null
+++ b/.changeset/tame-bats-strive.md
@@ -0,0 +1,6 @@
+---
+"@frontify/fondue-components": minor
+"@frontify/fondue": patch
+---
+
+fix(Tabs): allow Tabs to be disabled after init
diff --git a/packages/components/src/components/Tabs/Tabs.stories.tsx b/packages/components/src/components/Tabs/Tabs.stories.tsx
index 33f4480ba3..4fe6c1bb75 100644
--- a/packages/components/src/components/Tabs/Tabs.stories.tsx
+++ b/packages/components/src/components/Tabs/Tabs.stories.tsx
@@ -6,6 +6,7 @@ import { useState } from 'react';
import { action } from 'storybook/actions';
import { Badge } from '../Badge/Badge';
+import { Button } from '../Button/Button';
import { Tabs, TabsContent, TabsRoot, TabsTab, TabsTrigger } from './Tabs';
@@ -181,6 +182,45 @@ export const DisabledTabs: Story = {
),
};
+export const DynamicallyDisabledTabs: Story = {
+ render: (args) => {
+ const [isSecondTabDisabled, setIsSecondTabDisabled] = useState(false);
+ return (
+
+ setIsSecondTabDisabled((isDisabled) => !isDisabled)}>
+ {isSecondTabDisabled ? 'Enable' : 'Disable'} second tab
+
+
+
+ First Tab
+
+ First Content
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut
+ labore et dolore magna aliqua.
+
+
+
+ Second Tab
+
+ Second Content
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut
+ labore et dolore magna aliqua.
+
+
+
+ Third Tab
+
+ Third Content
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut
+ labore et dolore magna aliqua.
+
+
+
+
+ );
+ },
+};
+
export const WithDecorator: Story = {
render: (args) => (
diff --git a/packages/components/src/components/Tabs/Tabs.tsx b/packages/components/src/components/Tabs/Tabs.tsx
index ff498548be..f6507d2937 100644
--- a/packages/components/src/components/Tabs/Tabs.tsx
+++ b/packages/components/src/components/Tabs/Tabs.tsx
@@ -208,7 +208,6 @@ export const TabsTrigger = ({ children, ...props }: TabsTriggerProps, ref: Forwa
const { addTrigger } = useContext(TabTriggerContext);
const localRef = useRef(null);
- const previousElement = useRef(undefined);
useEffect(() => {
addTrigger({
@@ -217,11 +216,9 @@ export const TabsTrigger = ({ children, ...props }: TabsTriggerProps, ref: Forwa
disabled,
props,
element: children,
- previousElement: previousElement.current,
});
- previousElement.current = children;
// eslint-disable-next-line @eslint-react/exhaustive-deps
- }, [children]);
+ }, [children, disabled, value, addTrigger]);
return null;
};
diff --git a/packages/components/src/components/Tabs/__tests__/Tabs.ct.tsx b/packages/components/src/components/Tabs/__tests__/Tabs.ct.tsx
index 6e4fceab03..3d762dbf91 100644
--- a/packages/components/src/components/Tabs/__tests__/Tabs.ct.tsx
+++ b/packages/components/src/components/Tabs/__tests__/Tabs.ct.tsx
@@ -239,6 +239,64 @@ test('should not allow disabled tab to be selected', async ({ mount }) => {
await expect(component.getByTestId(THIRD_TAB_CONTENT_TEST_ID)).toBeVisible();
});
+test('should update the disabled state after the initial render', async ({ mount }) => {
+ const wrapper = await mount(
+
+
+ First Tab
+ First Content
+
+
+ Second Tab
+ Second Content
+
+ ,
+ );
+ const component = wrapper.getByTestId(TABS_ROOT_TEST_ID);
+
+ await expect(component.getByTestId(SECOND_TAB_TRIGGER_TEST_ID)).not.toBeDisabled();
+
+ await wrapper.update(
+
+
+ First Tab
+ First Content
+
+
+ Second Tab
+ Second Content
+
+ ,
+ );
+
+ await expect(component.getByTestId(SECOND_TAB_TRIGGER_TEST_ID)).toBeDisabled();
+
+ await component.getByTestId(SECOND_TAB_TRIGGER_TEST_ID).click({ force: true });
+
+ await expect(component.getByTestId(FIRST_TAB_TRIGGER_TEST_ID)).toHaveAttribute('data-state', 'active');
+ await expect(component.getByTestId(SECOND_TAB_CONTENT_TEST_ID)).not.toBeVisible();
+
+ await wrapper.update(
+
+
+ First Tab
+ First Content
+
+
+ Second Tab
+ Second Content
+
+ ,
+ );
+
+ await expect(component.getByTestId(SECOND_TAB_TRIGGER_TEST_ID)).not.toBeDisabled();
+
+ await component.getByTestId(SECOND_TAB_TRIGGER_TEST_ID).click();
+
+ await expect(component.getByTestId(SECOND_TAB_TRIGGER_TEST_ID)).toHaveAttribute('data-state', 'active');
+ await expect(component.getByTestId(SECOND_TAB_CONTENT_TEST_ID)).toBeVisible();
+});
+
test('should allow looping when switching tabs with keyboard', async ({ mount, page }) => {
const wrapper = await mount(
diff --git a/packages/components/src/components/Tabs/hooks/useTabTriggers.ts b/packages/components/src/components/Tabs/hooks/useTabTriggers.ts
index 9338ca5e36..06a1f5b172 100644
--- a/packages/components/src/components/Tabs/hooks/useTabTriggers.ts
+++ b/packages/components/src/components/Tabs/hooks/useTabTriggers.ts
@@ -1,6 +1,6 @@
/* (c) Copyright Frontify Ltd., all rights reserved. */
-import { type RefObject, useEffect, useLayoutEffect, useRef, useState } from 'react';
+import { type RefObject, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
import { type TabTrigger } from '../types';
@@ -140,17 +140,19 @@ export const useTabTriggers = ({
}
}, [triggers, triggerListRef, activeIndicatorRef]);
- const addTrigger = (trigger: TabTrigger) => {
- if (!trigger.previousElement) {
- setTriggers((prev) => [...prev, trigger]);
- }
-
+ const addTrigger = useCallback((trigger: TabTrigger) => {
setTriggers((prev) => {
- const index = prev.findIndex((element) => element.element === trigger.previousElement);
- prev[index] = trigger;
- return [...prev];
+ const index = prev.findIndex((element) => element.value === trigger.value);
+
+ if (index === -1) {
+ return [...prev, trigger];
+ }
+
+ const next = [...prev];
+ next[index] = trigger;
+ return next;
});
- };
+ }, []);
return {
triggerListRef,
diff --git a/packages/components/src/components/Tabs/types.ts b/packages/components/src/components/Tabs/types.ts
index e331b8c06c..6d27f1d0b3 100644
--- a/packages/components/src/components/Tabs/types.ts
+++ b/packages/components/src/components/Tabs/types.ts
@@ -5,7 +5,6 @@ import { type HTMLAttributes, type ReactNode } from 'react';
export type TabTrigger = {
ref?: { current: HTMLButtonElement | null };
element: ReactNode;
- previousElement?: ReactNode;
props: HTMLAttributes;
disabled?: boolean;
value: string;