Skip to content
Merged
26 changes: 20 additions & 6 deletions components/capsule-tab.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { CapsuleTab, CapsuleTabContentProps } from "./capsule-tab";
import { Button } from "./button";
import { ChangeEvent, useState } from "react";
import { Textbox } from "./textbox";
import { css } from "styled-components";

const meta: Meta<typeof CapsuleTab> = {
title: "Stage/CapsuleTab",
Expand All @@ -28,11 +29,16 @@ const meta: Meta<typeof CapsuleTab> = {
description: `
Custom styles for the CapsuleTab component. This object allows you to override styles for individual parts:

- **containerStyle**: Outer wrapper of the CapsuleTab (border, spacing, layout)
- **tabStyle**: Styles applied to the capsule tabs area (forwarded into the internal Capsule)
- **self**: Styles applied to the outer \`CapsuleTabWrapper\`. Useful for controlling borders, spacing, layout, shadows, or overall appearance.

Each field accepts a \`CSSProp\` (styled-components compatible) and can be used to control layout, borders, colors, and visual appearance.
`,
- **contentStyle**: Styles applied to the \`ContentWrapper\` that contains the active tab content. You can control padding, layout direction, background, etc.

- **capsuleWrapperStyle**: Styles forwarded to the \`Capsule\` component's container wrapper. Useful for adjusting border radius, alignment, or capsule layout behavior.

- **tabStyle**: Styles applied to individual capsule tabs inside the \`Capsule\` component. You can control tab border radius, colors, padding, hover states, and visual appearance.

Each field accepts a \`CSSProp\` (styled-components compatible) and can be used to customize the layout and appearance of specific parts of the component.
`,
},
},
};
Expand Down Expand Up @@ -113,8 +119,16 @@ export const Default: Story = {
};

const TABS_ITEMS: CapsuleTabContentProps[] = [
{ id: "1", title: "Write", content: <WriteTabContent /> },
{ id: "2", title: "Review", content: <ReviewTabContent /> },
{
id: "1",
title: "Write",
content: <WriteTabContent key={"write-tab"} />,
},
{
id: "2",
title: "Review",
content: <ReviewTabContent key={"review-tab"} />,
},
];

return (
Expand Down
65 changes: 38 additions & 27 deletions components/capsule-tab.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ReactNode, useState } from "react";
import { ReactNode, useCallback, useState } from "react";
import { Capsule } from "./capsule";
import styled, { css, CSSProp } from "styled-components";

Expand All @@ -7,10 +7,12 @@ export interface CapsuleTabProps {
activeTab?: string;
activeBackgroundColor?: string;
styles?: CapsuleTabStylesProps;
onTabChange?: (id: string) => void;
}

export interface CapsuleTabStylesProps {
containerStyle?: CSSProp;
self?: CSSProp;
contentStyle?: CSSProp;
capsuleWrapperStyle?: CSSProp;
tabStyle?: CSSProp;
}
Expand All @@ -26,13 +28,27 @@ function CapsuleTab({
styles,
activeTab = "1",
activeBackgroundColor = "black",
onTabChange,
}: CapsuleTabProps) {
const [selected, setSelected] = useState<string>(activeTab);
const [selectedLocal, setSelectedLocal] = useState<string>(activeTab);

const activeContent = tabs.filter((data) => data.id === selected);
const isControlled = onTabChange && activeTab !== undefined;
const selected = isControlled ? activeTab : selectedLocal;

const setSelected = useCallback(
(next: string) => {
if (!isControlled) {
setSelectedLocal(next);
}

onTabChange?.(next);
},
[isControlled, onTabChange]
);
const activeContent = tabs.filter((tab) => tab.id === selected);

return (
<CapsuleTabWrapper $containerStyle={styles?.containerStyle}>
<CapsuleTabWrapper aria-label="capsule-tab-wrapper" $style={styles?.self}>
<Capsule
styles={{
containerStyle: css`
Expand All @@ -53,42 +69,37 @@ function CapsuleTab({
full
/>

<div
style={{
display: "flex",
flexDirection: "column",
width: "100%",
}}
<ContentWrapper
aria-label="capsule-tab-content"
$style={styles?.contentStyle}
>
{activeContent.map((data, index) => (
<div
key={index}
style={{
width: "100%",
height: "100%",
}}
>
{data.content}
</div>
))}
</div>
{activeContent.map((props) => props.content)}
</ContentWrapper>
</CapsuleTabWrapper>
);
}

const CapsuleTabWrapper = styled.div<{
$containerStyle?: CSSProp;
$style?: CSSProp;
}>`
display: flex;
flex-direction: column;
gap: 0.25rem;
width: 100%;
padding-bottom: 5px;
border: 1px solid #ebebeb;
border-radius: 2px;
box-shadow: 0 1px 3px -3px #5b5b5b;

${({ $containerStyle }) => $containerStyle}
${({ $style }) => $style}
`;

const ContentWrapper = styled.div<{
$style?: CSSProp;
}>`
display: flex;
flex-direction: column;
width: 100%;

${({ $style }) => $style}
`;

export { CapsuleTab };
51 changes: 50 additions & 1 deletion test/component/capsule-tab.cy.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { css } from "styled-components";
import {
CapsuleTab,
CapsuleTabContentProps,
Expand All @@ -9,7 +10,7 @@ describe("Capsule Tab", () => {
{ id: "2", title: "Review", content: "Review" },
];

context("style", () => {
context("styles", () => {
it("renders capsule with 12px for active", () => {
cy.mount(<CapsuleTab activeTab="1" tabs={TABS_ITEMS} />);

Expand All @@ -20,6 +21,54 @@ describe("Capsule Tab", () => {
.eq(0)
.should("have.css", "border-radius", "12px");
});

context("self", () => {
context("when given padding 20px", () => {
it("renders the container wrapper with padding 20px", () => {
cy.mount(
<CapsuleTab
activeTab="1"
styles={{
self: css`
padding: 20px;
`,
}}
tabs={TABS_ITEMS}
/>
);

cy.findByLabelText("capsule-tab-wrapper").should(
"have.css",
"padding",
"20px"
);
});
});
});

context("contentStyle", () => {
context("when given padding 20px", () => {
it("renders the content wrapper with padding 20px", () => {
cy.mount(
<CapsuleTab
activeTab="1"
styles={{
contentStyle: css`
padding: 20px;
`,
}}
tabs={TABS_ITEMS}
/>
);

cy.findByLabelText("capsule-tab-content").should(
"have.css",
"padding",
"20px"
);
});
});
});
});

context("when given", () => {
Expand Down