Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/tame-bats-strive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@frontify/fondue-components": minor
"@frontify/fondue": patch
---

fix(Tabs): allow Tabs to be disabled after init
40 changes: 40 additions & 0 deletions packages/components/src/components/Tabs/Tabs.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import { action } from 'storybook/actions';

import { Badge } from '../Badge/Badge';
import { Button } from '../Button/Button';

import { Tabs, TabsContent, TabsRoot, TabsTab, TabsTrigger } from './Tabs';

Expand Down Expand Up @@ -181,6 +182,45 @@
),
};

export const DynamicallyDisabledTabs: Story = {
render: (args) => {
const [isSecondTabDisabled, setIsSecondTabDisabled] = useState(false);
return (
<div className="tw-flex tw-flex-col tw-gap-4">
<Button onPress={() => setIsSecondTabDisabled((isDisabled) => !isDisabled)}>
{isSecondTabDisabled ? 'Enable' : 'Disable'} second tab
</Button>
<Tabs.Root {...args}>
<Tabs.Tab value="first">
<Tabs.Trigger>First Tab</Tabs.Trigger>
<Tabs.Content>
<b>First Content</b>

Check warning on line 197 in packages/components/src/components/Tabs/Tabs.stories.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Ambiguous spacing after previous element b

See more on https://sonarcloud.io/project/issues?id=Frontify_arcade&issues=AZ_VpmQszyrB3mpwAoto&open=AZ_VpmQszyrB3mpwAoto&pullRequest=2818
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut
labore et dolore magna aliqua.
</Tabs.Content>
</Tabs.Tab>
<Tabs.Tab value="second" disabled={isSecondTabDisabled}>
<Tabs.Trigger>Second Tab</Tabs.Trigger>
<Tabs.Content>
<b>Second Content</b>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut
labore et dolore magna aliqua.
</Tabs.Content>
</Tabs.Tab>
<Tabs.Tab value="third">
<Tabs.Trigger>Third Tab</Tabs.Trigger>
<Tabs.Content>
<b>Third Content</b>

Check warning on line 213 in packages/components/src/components/Tabs/Tabs.stories.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Ambiguous spacing after previous element b

See more on https://sonarcloud.io/project/issues?id=Frontify_arcade&issues=AZ_VpmQszyrB3mpwAotp&open=AZ_VpmQszyrB3mpwAotp&pullRequest=2818
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut
labore et dolore magna aliqua.
</Tabs.Content>
</Tabs.Tab>
</Tabs.Root>
</div>
);
},
};

export const WithDecorator: Story = {
render: (args) => (
<Tabs.Root {...args}>
Expand All @@ -202,7 +242,7 @@
<IconIcon size={20} />
</Tabs.Trigger>
<Tabs.Content>
<b>Second Content</b>

Check warning on line 245 in packages/components/src/components/Tabs/Tabs.stories.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Ambiguous spacing after previous element b

See more on https://sonarcloud.io/project/issues?id=Frontify_arcade&issues=AZ_VpmQszyrB3mpwAotq&open=AZ_VpmQszyrB3mpwAotq&pullRequest=2818
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore
et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
aliquip ex ea commodo consequat.
Expand Down
5 changes: 1 addition & 4 deletions packages/components/src/components/Tabs/Tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,6 @@ export const TabsTrigger = ({ children, ...props }: TabsTriggerProps, ref: Forwa
const { addTrigger } = useContext(TabTriggerContext);

const localRef = useRef<HTMLButtonElement>(null);
const previousElement = useRef<ReactNode>(undefined);

useEffect(() => {
addTrigger({
Expand All @@ -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;
};
Expand Down
58 changes: 58 additions & 0 deletions packages/components/src/components/Tabs/__tests__/Tabs.ct.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<Tabs.Root data-test-id={TABS_ROOT_TEST_ID}>
<Tabs.Tab value="first">
<Tabs.Trigger data-test-id={FIRST_TAB_TRIGGER_TEST_ID}>First Tab</Tabs.Trigger>
<Tabs.Content data-test-id={FIRST_TAB_CONTENT_TEST_ID}>First Content</Tabs.Content>
</Tabs.Tab>
<Tabs.Tab value="second">
<Tabs.Trigger data-test-id={SECOND_TAB_TRIGGER_TEST_ID}>Second Tab</Tabs.Trigger>
<Tabs.Content data-test-id={SECOND_TAB_CONTENT_TEST_ID}>Second Content</Tabs.Content>
</Tabs.Tab>
</Tabs.Root>,
);
const component = wrapper.getByTestId(TABS_ROOT_TEST_ID);

await expect(component.getByTestId(SECOND_TAB_TRIGGER_TEST_ID)).not.toBeDisabled();

await wrapper.update(
<Tabs.Root data-test-id={TABS_ROOT_TEST_ID}>
<Tabs.Tab value="first">
<Tabs.Trigger data-test-id={FIRST_TAB_TRIGGER_TEST_ID}>First Tab</Tabs.Trigger>
<Tabs.Content data-test-id={FIRST_TAB_CONTENT_TEST_ID}>First Content</Tabs.Content>
</Tabs.Tab>
<Tabs.Tab value="second" disabled>
<Tabs.Trigger data-test-id={SECOND_TAB_TRIGGER_TEST_ID}>Second Tab</Tabs.Trigger>
<Tabs.Content data-test-id={SECOND_TAB_CONTENT_TEST_ID}>Second Content</Tabs.Content>
</Tabs.Tab>
</Tabs.Root>,
);

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(
<Tabs.Root data-test-id={TABS_ROOT_TEST_ID}>
<Tabs.Tab value="first">
<Tabs.Trigger data-test-id={FIRST_TAB_TRIGGER_TEST_ID}>First Tab</Tabs.Trigger>
<Tabs.Content data-test-id={FIRST_TAB_CONTENT_TEST_ID}>First Content</Tabs.Content>
</Tabs.Tab>
<Tabs.Tab value="second">
<Tabs.Trigger data-test-id={SECOND_TAB_TRIGGER_TEST_ID}>Second Tab</Tabs.Trigger>
<Tabs.Content data-test-id={SECOND_TAB_CONTENT_TEST_ID}>Second Content</Tabs.Content>
</Tabs.Tab>
</Tabs.Root>,
);

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(
<Tabs.Root data-test-id={TABS_ROOT_TEST_ID}>
Expand Down
22 changes: 12 additions & 10 deletions packages/components/src/components/Tabs/hooks/useTabTriggers.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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,
Expand Down
1 change: 0 additions & 1 deletion packages/components/src/components/Tabs/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { type HTMLAttributes, type ReactNode } from 'react';
export type TabTrigger = {
ref?: { current: HTMLButtonElement | null };
element: ReactNode;
previousElement?: ReactNode;
props: HTMLAttributes<HTMLButtonElement>;
disabled?: boolean;
value: string;
Expand Down