diff --git a/components/capsule-tab.stories.tsx b/components/capsule-tab.stories.tsx index d92b0f10..3f447002 100644 --- a/components/capsule-tab.stories.tsx +++ b/components/capsule-tab.stories.tsx @@ -19,6 +19,11 @@ const meta: Meta = { "ID of the initially active tab. Defaults to the first tab if not provided.", control: "text", }, + onTabChange: { + description: + "Callback triggered when the active tab changes. If provided, the parent component can control the active tab externally. If not provided, CapsuleTab manages the active tab internally.", + control: false, + }, activeBackgroundColor: { description: "Background color of the active capsule indicator.", control: "color", @@ -28,11 +33,16 @@ const meta: Meta = { 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. + +- **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 control layout, borders, colors, 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. +`, }, }, }; @@ -113,8 +123,16 @@ export const Default: Story = { }; const TABS_ITEMS: CapsuleTabContentProps[] = [ - { id: "1", title: "Write", content: }, - { id: "2", title: "Review", content: }, + { + id: "1", + title: "Write", + content: , + }, + { + id: "2", + title: "Review", + content: , + }, ]; return ( diff --git a/components/capsule-tab.tsx b/components/capsule-tab.tsx index d3b267b0..ec835eff 100644 --- a/components/capsule-tab.tsx +++ b/components/capsule-tab.tsx @@ -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"; @@ -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; } @@ -26,20 +28,38 @@ function CapsuleTab({ styles, activeTab = "1", activeBackgroundColor = "black", + onTabChange, }: CapsuleTabProps) { - const [selected, setSelected] = useState(activeTab); + const [selectedLocal, setSelectedLocal] = useState(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 ( - + -
- {activeContent.map((data, index) => ( -
- {data.content} -
- ))} -
+ {activeContent.map((props) => props.content)} +
); } 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 }; diff --git a/test/component/capsule-tab.cy.tsx b/test/component/capsule-tab.cy.tsx index 376373da..4d47e706 100644 --- a/test/component/capsule-tab.cy.tsx +++ b/test/component/capsule-tab.cy.tsx @@ -1,17 +1,85 @@ +import { css } from "styled-components"; import { CapsuleTab, CapsuleTabContentProps, + CapsuleTabStylesProps, } from "./../../components/capsule-tab"; +import { useState } from "react"; describe("Capsule Tab", () => { const TABS_ITEMS: CapsuleTabContentProps[] = [ - { id: "1", title: "Write", content: "Write" }, - { id: "2", title: "Review", content: "Review" }, + { id: "1", title: "Write", content: "Write Tab" }, + { id: "2", title: "Review", content: "Review Tab" }, ]; - context("style", () => { + function ProductCapsuleTab({ + withCallback, + styles, + }: { + styles?: CapsuleTabStylesProps; + withCallback?: boolean; + }) { + const [activeTab, setActiveTab] = useState("2"); + + return ( + { + setActiveTab(id); + console.log(`the activeTab now in the id: ${id}`); + } + : undefined + } + styles={styles} + /> + ); + } + + context("styles", () => { + context("capsuleWrapperStyle", () => { + it("renders with padding left and right with 5px", () => { + cy.mount(); + + const capsule = cy.findAllByLabelText("capsule").eq(0); + + capsule.should("have.css", "padding-left", "5px"); + capsule.should("have.css", "padding-right", "5px"); + }); + + context("when given padding left and right by 20px", () => { + it("renders with those styles", () => { + cy.mount( + + ); + + const capsule = cy.findAllByLabelText("capsule").eq(0); + + capsule.should("have.css", "padding-left", "20px"); + capsule.should("have.css", "padding-right", "20px"); + }); + }); + }); + it("renders capsule with 12px for active", () => { - cy.mount(); + cy.mount( + + ); cy.findAllByLabelText("active-capsule-box") .eq(0) @@ -20,28 +88,119 @@ 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( + + ); + + 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( + + ); + + cy.findByLabelText("capsule-tab-content").should( + "have.css", + "padding", + "20px" + ); + }); + }); + }); }); - context("when given", () => { - it("renders on the left side", () => { - cy.mount(); + context("tabs", () => { + context("when given", () => { + it("renders on the left side", () => { + cy.mount(); - cy.findByLabelText("capsule") - .should("have.css", "justify-content", "normal") - .and("have.css", "width", "458px"); + cy.findByLabelText("capsule") + .should("have.css", "justify-content", "normal") + .and("have.css", "width", "458px"); + }); }); }); context("activeTab", () => { context("when given", () => { - const TABS_ITEMS: CapsuleTabContentProps[] = [ - { id: "1", title: "Write", content: "Write" }, - { id: "2", title: "Review", content: "Review" }, - ]; - it("renders with active equal the id argument", () => { - cy.mount(); + it("renders with initialize and equal with id state", () => { + cy.mount(); cy.contains("Write").should("have.css", "color", "rgb(17, 24, 39)"); cy.contains("Review").should("have.css", "color", "rgb(255, 255, 255)"); + + cy.findByText("Review Tab").should("be.visible"); + }); + + context("when clicking", () => { + it("renders the content and can move to other tab", () => { + cy.mount(); + cy.findByText("Write Tab").should("not.exist"); + cy.findByText("Review Tab").should("exist"); + + cy.contains("Write").click(); + + cy.findByText("Write Tab").should("exist"); + cy.findByText("Review Tab").should("not.exist"); + }); + }); + }); + + context("when given onTabChange", () => { + it("renders the content fully using activeTab from outer", () => { + cy.window().then((win) => { + cy.spy(win.console, "log").as("consoleLog"); + }); + + cy.mount(); + cy.findByText("Write Tab").should("not.exist"); + cy.findByText("Review Tab").should("exist"); + + cy.contains("Write").click(); + + cy.get("@consoleLog").should( + "have.been.calledWith", + "the activeTab now in the id: 1" + ); + + cy.findByText("Write Tab").should("exist"); + cy.findByText("Review Tab").should("not.exist"); + + cy.contains("Review").click(); + + cy.get("@consoleLog").should( + "have.been.calledWith", + "the activeTab now in the id: 2" + ); + + cy.findByText("Write Tab").should("not.exist"); + cy.findByText("Review Tab").should("exist"); }); }); });