Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Layout component - coldiv. #572

Merged
merged 6 commits into from
Oct 16, 2024
76 changes: 76 additions & 0 deletions src/layout/col-div.style.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import styled, { css } from "styled-components";
import { MediaQuery } from "../theme/breakpoint/media-query-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;
}

export const StyledDiv = styled.div<StyledDivStyleProps>`
position: relative;

${(props) => {
const {
$xxlStart,
$xxlSpan,

$xlStart,
$xlSpan,

$lgStart,
$lgSpan,

$mdStart,
$mdSpan,

$smStart,
$smSpan,

$xsStart,
$xsSpan,

$xxsStart,
$xxsSpan,
} = props;

return css`
grid-column: ${$xxlStart || "auto"} / span ${$xxlSpan || 1};

${MediaQuery.MaxWidth.xl} {
grid-column: ${$xlStart || "auto"} / span ${$xlSpan || 1};
}

${MediaQuery.MaxWidth.lg} {
grid-column: ${$lgStart || "auto"} / span ${$lgSpan || 1};
}

${MediaQuery.MaxWidth.md} {
grid-column: ${$mdStart || "auto"} / span ${$mdSpan || 1};
}

${MediaQuery.MaxWidth.sm} {
grid-column: ${$smStart || "auto"} / span ${$smSpan || 1};
}

${MediaQuery.MaxWidth.xs} {
grid-column: ${$xsStart || "auto"} / span ${$xsSpan || 1};
}

${MediaQuery.MaxWidth.xxs} {
grid-column: ${$xxsStart || "auto"} / span ${$xxsSpan || 1};
}
`;
}}
`;
126 changes: 126 additions & 0 deletions src/layout/col-div.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import React from "react";
import { ColDivProps } 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: React.Ref<HTMLDivElement>
): 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 };

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 ||
xlCols ||
lgCols ||
mdCols ||
smCols ||
xsCols ||
xxsCols,
xxlColumnCount,
"xxl"
);

const xlStartSpan = getColSpan(
xlCols || lgCols || mdCols || smCols || xsCols || xxsCols,
xlColumnCount,
"xl"
);

const lgStartSpan = getColSpan(
lgCols || mdCols || smCols || xsCols || xxsCols,
lgColumnCount,
"lg"
);

const mdStartSpan = getColSpan(
mdCols || smCols || xsCols || xxsCols,
mdColumnCount,
"md"
);

const smStartSpan = getColSpan(
smCols || xsCols || xxsCols,
smColumnCount,
"sm"
);

const xsStartSpan = getColSpan(xsCols || xxsCols, 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,
};
};

return <StyledDiv ref={ref} {...getStyleProps()} {...otherProps} />;
};

export const ColDiv = React.forwardRef(Component);
2 changes: 2 additions & 0 deletions src/layout/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ColDiv } from "./col-div";
import { Container } from "./container";
import { Content } from "./content";
import { Section } from "./section";
Expand All @@ -6,6 +7,7 @@ export const Layout = {
Section: Section,
Container: Container,
Content: Content,
ColDiv: ColDiv,
};

export * from "./types";
34 changes: 34 additions & 0 deletions src/layout/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { DefaultTheme } from "styled-components";
import { AddOne, Range } from "../util/utility-types";

interface CommonLayoutProps extends React.HTMLAttributes<HTMLDivElement> {
children: React.ReactNode;
"data-testid"?: string | undefined;
Expand All @@ -14,3 +17,34 @@ export interface ContainerProps extends CommonLayoutProps {
export interface SectionProps extends CommonLayoutProps {}

export interface ContentProps extends ContainerProps {}

export type ColSpan<Max extends number | undefined> = Max extends number
? Range<Max> | [Range<AddOne<Max>>, Range<AddOne<Max>>] | undefined
: number | [number, number] | undefined;

export type BreakpointSpan<
Breakpoint extends keyof DefaultTheme["maxColumns"]
> = DefaultTheme["maxColumns"] extends Record<
Breakpoint,
infer Max extends number
>
? ColSpan<Max>
: number | [number, number] | undefined;
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<HTMLDivElement>,
ColProps {
"data-testid"?: string;
}
16 changes: 16 additions & 0 deletions src/util/utility-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,19 @@ export type RequiredKeys<T> = {
// eslint-disable-next-line @typescript-eslint/ban-types
[K in keyof T]-?: {} extends { [P in K]: T[K] } ? never : K;
}[keyof T];

// Gets a union of numbers from 1 to N e.g. Range<3> evaluates to 1 | 2 |3
export type Range<
N extends number,
Result extends number[] = []
> = Result["length"] extends N
? Exclude<Result[number] | N, 0>
: Range<N, [...Result, Result["length"]]>;

//Increments a numeric literal e.g. AddOne<1> evaluates to 2
export type AddOne<
N extends number,
Result extends number[] = []
> = Result["length"] extends N
? [...Result, Result["length"]]["length"]
: AddOne<N, [...Result, Result["length"]]>;
94 changes: 94 additions & 0 deletions stories/layout-test/Coldiv.stories.tsx
Original file line number Diff line number Diff line change
@@ -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 = () => (
<ThemeProvider theme={mockTheme}>
<Content type="grid">
<ColDiv style={{ backgroundColor: "#f0f0f0", padding: "1rem" }}>
Default ColDiv
</ColDiv>
</Content>
</ThemeProvider>
);

// Mobile config: spans across 3 columns in mobile view
export const MobileColsOnly = () => (
<ThemeProvider theme={mockTheme}>
<Content type="grid">
<ColDiv smCols={8}>Mobile: Span 3 columns</ColDiv>
</Content>
</ThemeProvider>
);

// Tablet config: spans across 6 columns in tablet view
export const TabletColsOnly = () => (
<ThemeProvider theme={mockTheme}>
<Content type="grid">
<ColDiv
xxlCols={6}
style={{ backgroundColor: "#c8e6c9", padding: "1rem" }}
>
Tablet: Span 6 columns
</ColDiv>
</Content>
</ThemeProvider>
);

// Desktop config: spans across 8 columns in desktop view
export const DesktopColsOnly = () => (
<ThemeProvider theme={mockTheme}>
<Content type="grid">
<ColDiv
xlCols={8}
style={{ backgroundColor: "#ffe0b2", padding: "1rem" }}
>
Desktop: Span 8 columns
</ColDiv>
</Content>
</ThemeProvider>
);

// Mixed config: spans across different columns in mobile, tablet, and desktop view
export const MixedCols = () => (
<ThemeProvider theme={mockTheme}>
<Content type="grid">
<ColDiv
smCols={8}
lgCols={4}
xlCols={6}
style={{ backgroundColor: "#d1c4e9", padding: "1rem" }}
>
Mobile: Span 2 columns, Tablet: Span 4 columns, Desktop: Span 6
columns
</ColDiv>
</Content>
</ThemeProvider>
);

// Using start and span values: spans across a specific range of columns
export const StartAndSpan = () => (
<ThemeProvider theme={mockTheme}>
<Content type="grid">
<Layout.ColDiv
smCols={[1, 5]}
lgCols={[3, 6]}
xxlCols={[1, 12]}
xlCols={[4, 10]}
style={{ backgroundColor: "#ffccbc", padding: "1rem" }}
>
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
</Layout.ColDiv>
</Content>
</ThemeProvider>
);
Loading