From 5252585318178b5046969c7bb723c7fe68f59b83 Mon Sep 17 00:00:00 2001 From: Gundu Mahidhar Reddy Date: Thu, 19 Sep 2024 11:18:15 +0800 Subject: [PATCH] [CCUBE-1548][MAHI]Set up ColDiv for layout with its tests and storybook and add theme capability for Layout component --- src/layout/col-div.style.tsx | 119 +++++++++++++++++++++++++ src/layout/col-div.tsx | 107 ++++++++++++++++++++++ src/layout/index.ts | 2 + src/layout/types.ts | 68 ++++++++++++++ stories/layout-test/Coldiv.stories.tsx | 94 +++++++++++++++++++ tests/layout/layout-coldiv.spec.tsx | 110 +++++++++++++++++++++++ 6 files changed, 500 insertions(+) create mode 100644 src/layout/col-div.style.tsx create mode 100644 src/layout/col-div.tsx create mode 100644 stories/layout-test/Coldiv.stories.tsx create mode 100644 tests/layout/layout-coldiv.spec.tsx diff --git a/src/layout/col-div.style.tsx b/src/layout/col-div.style.tsx new file mode 100644 index 000000000..c886b57ee --- /dev/null +++ b/src/layout/col-div.style.tsx @@ -0,0 +1,119 @@ +import styled, { css } from "styled-components"; +import { MediaQuery } from "../theme/mediaquery/mediaquery-helper"; + +export interface StyledDivStyleProps { + $xxsStart?: number | undefined; + $xxsSpan?: number | undefined; + $xsStart?: number | undefined; + $xsSpan?: number | undefined; + $smStart?: number | undefined; + $smSpan?: number | undefined; + $mdStart?: number | undefined; + $mdSpan?: number | undefined; + $lgStart?: number | undefined; + $lgSpan?: number | undefined; + $xlStart?: number | undefined; + $xlSpan?: number | undefined; + $xxlStart?: number | undefined; + $xxlSpan?: number | undefined; + + $xxsMargin?: string | number; + $xxsGutter?: string | number; + $xsMargin?: string | number; + $xsGutter?: string | number; + $smMargin?: string | number; + $smGutter?: string | number; + $mdMargin?: string | number; + $mdGutter?: string | number; + $lgMargin?: string | number; + $lgGutter?: string | number; + $xlMargin?: string | number; + $xlGutter?: string | number; + $xxlMargin?: string | number; + $xxlGutter?: string | number; +} + +export const StyledDiv = styled.div` + position: relative; + + ${(props) => { + const { + $xxlStart, + $xxlSpan, + $xxlMargin, + $xxlGutter, + + $xlStart, + $xlSpan, + $xlMargin, + $xlGutter, + + $lgStart, + $lgSpan, + $lgMargin, + $lgGutter, + + $mdStart, + $mdSpan, + $mdMargin, + $mdGutter, + + $smStart, + $smSpan, + $smMargin, + $smGutter, + + $xsStart, + $xsSpan, + $xsMargin, + $xsGutter, + + $xxsStart, + $xxsSpan, + $xxsMargin, + $xxsGutter, + } = props; + + return css` + grid-column: ${$xxlStart || "auto"} / span ${$xxlSpan || 1}; + margin: ${$xxlMargin}px; + padding: ${$xxlGutter}px; + + ${MediaQuery.MaxWidth.xl} { + grid-column: ${$xlStart || "auto"} / span ${$xlSpan || 1}; + margin: ${$xlMargin}px; + padding: ${$xlGutter}px; + } + + ${MediaQuery.MaxWidth.lg} { + grid-column: ${$lgStart || "auto"} / span ${$lgSpan || 1}; + margin: ${$lgMargin}px; + padding: ${$lgGutter}px; + } + + ${MediaQuery.MaxWidth.md} { + grid-column: ${$mdStart || "auto"} / span ${$mdSpan || 1}; + margin: ${$mdMargin}px; + padding: ${$mdGutter}px; + } + + ${MediaQuery.MaxWidth.sm} { + grid-column: ${$smStart || "auto"} / span ${$smSpan || 1}; + margin: ${$smMargin}px; + padding: ${$smGutter}px; + } + + ${MediaQuery.MaxWidth.xs} { + grid-column: ${$xsStart || "auto"} / span ${$xsSpan || 1}; + margin: ${$xsMargin}px; + padding: ${$xsGutter}px; + } + + ${MediaQuery.MaxWidth.xxs} { + grid-column: ${$xxsStart || "auto"} / span ${$xxsSpan || 1}; + margin: ${$xxsMargin}px; + padding: ${$xxsGutter}px; + } + `; + }} +`; diff --git a/src/layout/col-div.tsx b/src/layout/col-div.tsx new file mode 100644 index 000000000..c3d866443 --- /dev/null +++ b/src/layout/col-div.tsx @@ -0,0 +1,107 @@ +import React from "react"; +import { ColDivProps, DivRef } from "./types"; +import { StyledDiv } from "./col-div.style"; +import { BreakpointValues } from "../theme/breakpoint/theme-helper"; +import { ThemeSpec } from "../theme/types"; +import { useTheme } from "styled-components"; + +const Component = (props: ColDivProps, ref: DivRef): JSX.Element => { + const theme = useTheme() as ThemeSpec; + + const { + xxlCols, + xlCols, + lgCols, + mdCols, + smCols, + xsCols, + xxsCols, + ...otherProps + } = props; + + const getColSpan = ( + cols: number | [number, number] | undefined, + maxCols: number, + label: string + ) => { + if (!cols) return { start: undefined, span: undefined }; + + // during development process to give wwarning + if (process.env.NODE_ENV === "development") { + if (typeof cols === "number" && cols > maxCols) { + console.warn( + `Warning: ${label}Cols exceeds maximum (${maxCols}): ${cols}` + ); + } else if ( + Array.isArray(cols) && + (cols[0] > maxCols + 1 || cols[1] > maxCols + 1) + ) { + console.warn( + `Warning: ${label}Cols array exceeds maximum (${maxCols}): [${cols[0]}, ${cols[1]}]` + ); + } + } + + if (Array.isArray(cols)) { + const [start, end] = cols; + const span = Math.min(end - start, maxCols); + return { start, span }; + } + + return { start: undefined, span: Math.min(cols, maxCols) }; + }; + + const getStyleProps = () => { + const xxlColumnCount = BreakpointValues["xxl-column"]({ theme }); + const xlColumnCount = BreakpointValues["xl-column"]({ theme }); + const lgColumnCount = BreakpointValues["lg-column"]({ theme }); + const mdColumnCount = BreakpointValues["md-column"]({ theme }); + const smColumnCount = BreakpointValues["sm-column"]({ theme }); + const xsColumnCount = BreakpointValues["xs-column"]({ theme }); + const xxsColumnCount = BreakpointValues["xxs-column"]({ theme }); + + const xxlStartSpan = getColSpan(xxlCols, xxlColumnCount, "xxl"); + const xlStartSpan = getColSpan(xlCols, xlColumnCount, "xl"); + const lgStartSpan = getColSpan(lgCols, lgColumnCount, "lg"); + const mdStartSpan = getColSpan(mdCols, mdColumnCount, "md"); + const smStartSpan = getColSpan(smCols, smColumnCount, "sm"); + const xsStartSpan = getColSpan(xsCols, xsColumnCount, "xs"); + const xxsStartSpan = getColSpan(xxsCols, xxsColumnCount, "xxs"); + + return { + $xxlStart: xxlStartSpan.start, + $xxlSpan: xxlStartSpan.span, + $xlStart: xlStartSpan.start, + $xlSpan: xlStartSpan.span, + $lgStart: lgStartSpan.start, + $lgSpan: lgStartSpan.span, + $mdStart: mdStartSpan.start, + $mdSpan: mdStartSpan.span, + $smStart: smStartSpan.start, + $smSpan: smStartSpan.span, + $xsStart: xsStartSpan.start, + $xsSpan: xsStartSpan.span, + $xxsStart: xxsStartSpan.start, + $xxsSpan: xxsStartSpan.span, + + $xxlMargin: BreakpointValues["xxl-margin"]({ theme }), + $xxlGutter: BreakpointValues["xxl-gutter"]({ theme }), + $xlMargin: BreakpointValues["xl-margin"]({ theme }), + $xlGutter: BreakpointValues["xl-gutter"]({ theme }), + $lgMargin: BreakpointValues["lg-margin"]({ theme }), + $lgGutter: BreakpointValues["lg-gutter"]({ theme }), + $mdMargin: BreakpointValues["md-margin"]({ theme }), + $mdGutter: BreakpointValues["md-gutter"]({ theme }), + $smMargin: BreakpointValues["sm-margin"]({ theme }), + $smGutter: BreakpointValues["sm-gutter"]({ theme }), + $xsMargin: BreakpointValues["xs-margin"]({ theme }), + $xsGutter: BreakpointValues["xs-gutter"]({ theme }), + $xxsMargin: BreakpointValues["xxs-margin"]({ theme }), + $xxsGutter: BreakpointValues["xxs-gutter"]({ theme }), + }; + }; + + return ; +}; + +export const ColDiv = React.forwardRef(Component); diff --git a/src/layout/index.ts b/src/layout/index.ts index 0767e37d0..3337d54de 100644 --- a/src/layout/index.ts +++ b/src/layout/index.ts @@ -1,3 +1,4 @@ +import { ColDiv } from "./col-div"; import { Container } from "./container"; import { Content } from "./content"; import { Section } from "./section"; @@ -6,6 +7,7 @@ export const Layout = { Section: Section, Container: Container, Content: Content, + ColDiv: ColDiv, }; export * from "./types"; diff --git a/src/layout/types.ts b/src/layout/types.ts index 7bec2f333..0fa502e57 100644 --- a/src/layout/types.ts +++ b/src/layout/types.ts @@ -1,3 +1,5 @@ +import { DefaultTheme } from "styled-components"; + export interface CommonLayoutProps extends React.HTMLAttributes { children: React.ReactNode; @@ -17,3 +19,69 @@ export interface SectionProps extends CommonLayoutProps {} export type DivRef = React.Ref; export interface ContentProps extends ContainerProps {} + +// does recursion till it gets the max col number +type Range< + N extends number, + Result extends number[] = [] +> = Result["length"] extends N + ? Exclude + : Range; + +//add one to the range for array +type AddOne< + N extends number, + Result extends number[] = [] +> = Result["length"] extends N + ? [...Result, Result["length"]]["length"] + : AddOne; + +// uses the range and if there is no max defined it just becomes a number +export type ColSpan = Max extends number + ? Range | [Range>, Range>] | undefined + : number | [number, number] | undefined; + +// using generic breakpointspan to extract column span for a specific breakpoint +export type BreakpointSpan< + Breakpoint extends keyof DefaultTheme["maxColumns"] +> = DefaultTheme["maxColumns"] extends Record< + Breakpoint, + infer Max extends number +> + ? ColSpan + : number | [number, number] | undefined; + +// refactor ColProps to use BreakpointSpan +export interface ColProps { + /** + * Specifies the number of columns to be spanned across for any breakpoint. + * If an array is specified, the format is [startCol, endCol]. + */ + xxlCols?: BreakpointSpan<"xxl">; + xlCols?: BreakpointSpan<"xl">; + lgCols?: BreakpointSpan<"lg">; + mdCols?: BreakpointSpan<"md">; + smCols?: BreakpointSpan<"sm">; + xsCols?: BreakpointSpan<"xs">; + xxsCols?: BreakpointSpan<"xxs">; +} +export interface ColDivProps + extends React.HTMLAttributes, + ColProps { + "data-testid"?: string; + + xxlMargin?: string | number; + xxlGutter?: string | number; + xlMargin?: string | number; + xlGutter?: string | number; + lgMargin?: string | number; + lgGutter?: string | number; + mdMargin?: string | number; + mdGutter?: string | number; + smMargin?: string | number; + smGutter?: string | number; + xsMargin?: string | number; + xsGutter?: string | number; + xxsMargin?: string | number; + xxsGutter?: string | number; +} diff --git a/stories/layout-test/Coldiv.stories.tsx b/stories/layout-test/Coldiv.stories.tsx new file mode 100644 index 000000000..8daa77027 --- /dev/null +++ b/stories/layout-test/Coldiv.stories.tsx @@ -0,0 +1,94 @@ +import { ThemeProvider } from "styled-components"; +import { ColDiv } from "../../src/layout/col-div"; +import { Content } from "../../src/layout/content"; +import { mockTheme } from "./mock-theme"; +import { Layout } from "../../src/layout"; + +export default { + title: "Layout-Test/ColDiv", + component: ColDiv, +}; + +// Default view span 1 column +export const Default = () => ( + + + + Default ColDiv + + + +); + +// Mobile config: spans across 3 columns in mobile view +export const MobileColsOnly = () => ( + + + Mobile: Span 3 columns + + +); + +// Tablet config: spans across 6 columns in tablet view +export const TabletColsOnly = () => ( + + + + Tablet: Span 6 columns + + + +); + +// Desktop config: spans across 8 columns in desktop view +export const DesktopColsOnly = () => ( + + + + Desktop: Span 8 columns + + + +); + +// Mixed config: spans across different columns in mobile, tablet, and desktop view +export const MixedCols = () => ( + + + + Mobile: Span 2 columns, Tablet: Span 4 columns, Desktop: Span 6 + columns + + + +); + +// Using start and span values: spans across a specific range of columns +export const StartAndSpan = () => ( + + + + Mobile: Takes up span 4 columns starting from 1 | Tablet: Takes + up span 3 columns starting from 3 | Desktop: Takes up span 6 + columns starting from 4 + + + +); diff --git a/tests/layout/layout-coldiv.spec.tsx b/tests/layout/layout-coldiv.spec.tsx new file mode 100644 index 000000000..772e6c196 --- /dev/null +++ b/tests/layout/layout-coldiv.spec.tsx @@ -0,0 +1,110 @@ +import { render } from "@testing-library/react"; +import "jest-styled-components"; +import { ThemeProvider } from "styled-components"; +import { ColDiv } from "../../src/layout/col-div"; +import { ThemeSpec } from "../../src/theme/types"; + +const mockTheme: ThemeSpec = { + colourScheme: "lifesg", + fontScheme: "lifesg", + animationScheme: "lifesg", + borderScheme: "lifesg", + spacingScheme: "lifesg", + radiusScheme: "lifesg", + breakpointScheme: "lifesg", +}; + +describe("ColDiv Component", () => { + it("should render with default settings (spanning 1 column)", () => { + const { container } = render( + + Default ColDiv + + ); + + expect(container.firstChild).toHaveStyleRule( + "grid-column", + "auto / span 1" + ); + }); + + it("should correctly apply xxs column span", () => { + const { container } = render( + + XXS ColDiv + + ); + + expect(container.firstChild).toHaveStyleRule( + "grid-column", + "auto / span 2", + { + media: `screen and (max-width: 320px)`, + } + ); + }); + + it("should correctly apply xs column span", () => { + const { container } = render( + + XS ColDiv + + ); + + expect(container.firstChild).toHaveStyleRule( + "grid-column", + "auto / span 3", + { + media: `screen and (max-width: 375px)`, + } + ); + }); + + it("should correctly apply sm column span", () => { + const { container } = render( + + SM ColDiv + + ); + + expect(container.firstChild).toHaveStyleRule( + "grid-column", + "auto / span 4", + { + media: `screen and (max-width: 420px)`, + } + ); + }); + + it("should correctly apply start and span for xxs", () => { + const { container } = render( + + XXS Start and Span ColDiv + + ); + + expect(container.firstChild).toHaveStyleRule( + "grid-column", + "1 / span 2", + { + media: `screen and (max-width: 320px)`, + } + ); + }); + + it("should correctly apply start and span for lg", () => { + const { container } = render( + + LG Start and Span ColDiv + + ); + + expect(container.firstChild).toHaveStyleRule( + "grid-column", + "2 / span 4", + { + media: `screen and (max-width: 1023px)`, + } + ); + }); +});