diff --git a/CHANGELOG.md b/CHANGELOG.md index 32bb902ef..fce14582d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ Currently, this repo is in Prerelease. When it is released, this project will ad ### Updates -- Updates `Template` component(s) and styles per [TAD](https://docs.google.com/document/d/1vZJX7Y-DnEM44-iWw5qoXGdxqavHEN6prhr-YDTNr0o/edit?pli=1&tab=t.0). +- [Updates `Template` component(s)](<(https://newyorkpubliclibrary.atlassian.net/browse/DSD-1884)>) and styles per TAD. ## 3.5.0 (December 5, 2024) diff --git a/src/components/Template/Template.mdx b/src/components/Template/Template.mdx index 21ee2cc3d..852a6ee54 100644 --- a/src/components/Template/Template.mdx +++ b/src/components/Template/Template.mdx @@ -34,7 +34,7 @@ import Link from "../Link/Link"; ## Template Overview -TBD +### Using the `Template` with the `Header` and `Footer` - // ... - // ... - // ... - // ... - // ... - +//... + +<> + + + `} language="jsx" /> ## Template Props @@ -81,6 +85,9 @@ breaking a reader's rhythm. The `TemplateMain` component has a max width of 1280 which is too wide for text. If your content includes long chunks of text, you should use the `TemplateMainNarrow` component instead. +This means you may use `TemplateMain` or `TemplateMainNarrow` depending on your needs, +but the two should not be used in conjunction. + The `TemplateMain` and `TemplateMainNarrow` components render the content as a `
` element with a default "id" of `"mainContent"`. This should be used as the anchor element that the skip navigation link points to. If your application @@ -100,7 +107,7 @@ Pass the location of the sidebar to the `sidebar` prop as either "left" or "righ - // ... + //... `} language="jsx" diff --git a/src/components/Template/Template.stories.tsx b/src/components/Template/Template.stories.tsx index c5941352d..54a8dff29 100644 --- a/src/components/Template/Template.stories.tsx +++ b/src/components/Template/Template.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta } from "@storybook/react"; +import type { Meta, StoryObj } from "@storybook/react"; import { getPlaceholderImage } from "../../utils/utils"; import Accordion, { AccordionDataProps } from "../Accordion/Accordion"; import Banner from "../Banner/Banner"; @@ -9,7 +9,7 @@ import FeaturedContent from "../FeaturedContent/FeaturedContent"; import Form, { FormField, FormRow } from "../Form/Form"; import Heading from "../Heading/Heading"; import Hero from "../Hero/Hero"; -import { HorizontalRule } from "../HorizontalRule/HorizontalRule"; +import HorizontalRule from "../HorizontalRule/HorizontalRule"; import Placeholder from "../Placeholder/Placeholder"; import Table from "../Table/Table"; import TextInput from "../TextInput/TextInput"; @@ -21,14 +21,25 @@ import { TemplateMainNarrow, TemplateSidebar, TemplateBottom, + sidebarPlacementArray, + SidebarPlacement, } from "./Template"; -const meta: Meta = { +const meta: Meta = { title: "Components/Page Layout/Template", component: Template, + argTypes: { + id: { control: false }, + sidebar: { + control: { type: "radio" }, + options: sidebarPlacementArray, + table: { defaultValue: { summary: "none" } }, + }, + }, }; export default meta; +type Story = StoryObj; const accordionData: AccordionDataProps[] = [ { @@ -169,7 +180,11 @@ const otherSubHeaderText = * Main Story for the Template component. This must contains the `args` * and `parameters` properties in this object. */ -export const WithControls = { + +export const sidebarLabel = (sidebar: SidebarPlacement) => { + return `${sidebar[0].toUpperCase()}${sidebar.slice(1)} Sidebar`; +}; +export const WithControls: Story = { args: { id: "template", sidebar: "left", @@ -180,25 +195,31 @@ export const WithControls = { table: { defaultValue: { summary: "none" } }, }, }, - render: (args) => ( - - ), + render: (args) => { + const { sidebar } = args; + + return ( + + ); + }, parameters: { design: { type: "figma", @@ -207,7 +228,7 @@ export const WithControls = { }, }; -export const TemplateFullExample = { +export const TemplateFullExample: Story = { args: { sidebar: "left", }, @@ -333,36 +354,6 @@ export const TemplateFullExampleNarrow = { render: () => ( <> ), diff --git a/src/components/Template/Template.test.tsx b/src/components/Template/Template.test.tsx index e6fd85a41..de4d34e5d 100644 --- a/src/components/Template/Template.test.tsx +++ b/src/components/Template/Template.test.tsx @@ -1,3 +1,4 @@ +import React from "react"; import { render, screen } from "@testing-library/react"; import { axe } from "jest-axe"; import renderer from "react-test-renderer"; @@ -9,13 +10,14 @@ import { TemplateMain, TemplateSidebar, TemplateBottom, + SidebarPlacement, + TemplateMainNarrow, } from "./Template"; import Placeholder from "../Placeholder/Placeholder"; +import { sidebarLabel } from "./Template.stories"; const breakout = Breakout; -const sidebar = "left"; const contentTop = Content Top; -const contentSidebar = Left Sidebar; const contentMain = ( <> Main Content @@ -24,26 +26,37 @@ const contentMain = ( ); const contentBottom = Content Bottom; -const templateChildren = ( +const templateComponents = ( + sidebar: SidebarPlacement = "none", + useMainNarrow: boolean = false +) => ( ); describe("Template components accessibility", () => { it("passes axe accessibility test", async () => { - const { container } = render(<>{templateChildren}); + const { container } = render(templateComponents("left")); expect(await axe(container)).toHaveNoViolations(); }); }); describe("Template components", () => { - it("renders each section", () => { - render(<>{templateChildren}); + it("renders each section with left sidebar", () => { + render(templateComponents("left")); expect(screen.getByText("Breakout")).toBeInTheDocument(); expect(screen.getByText("Content Top")).toBeInTheDocument(); @@ -53,26 +66,76 @@ describe("Template components", () => { expect(screen.getByText("Content Bottom")).toBeInTheDocument(); }); - it("renders a #mainContent id in the TemplateContent component", () => { - const { container } = render(<>{templateChildren}); + it("renders each section with left sidebar", () => { + render(templateComponents("right")); + + expect(screen.getByText("Breakout")).toBeInTheDocument(); + expect(screen.getByText("Content Top")).toBeInTheDocument(); + expect(screen.getByText("Right Sidebar")).toBeInTheDocument(); + expect(screen.getByText("Main Content")).toBeInTheDocument(); + expect(screen.getByText("More Content")).toBeInTheDocument(); + expect(screen.getByText("Content Bottom")).toBeInTheDocument(); + }); + + it("renders a #mainContent id when using TemplateMain", () => { + const { container } = render(templateComponents("left")); + + expect(container.querySelector("#mainContent")).toBeInTheDocument(); + expect(screen.getByRole("main")).toHaveAttribute("id", "mainContent"); + }); + + it("renders a #mainContent id when using TemplateMainNarrow", () => { + const { container } = render(templateComponents("none", true)); expect(container.querySelector("#mainContent")).toBeInTheDocument(); expect(screen.getByRole("main")).toHaveAttribute("id", "mainContent"); }); + it("passes a ref to the div wrapper element", () => { + const ref = React.createRef(); + const { container } = render( + + ); + + expect(container.querySelectorAll("div")[0]).toBe(ref.current); + }); + it("renders the UI snapshot correctly", () => { - const templateComponents = renderer - .create( - - ) + const templateComponentsLeftSidebar = renderer + .create(templateComponents("left")) + .toJSON(); + + const templateComponentsRightSidebar = renderer + .create(templateComponents("right")) .toJSON(); - expect(templateComponents).toMatchSnapshot(); + const templateComponentsNoSidebar = renderer + .create(templateComponents("none")) + .toJSON(); + + const templateComponentsMainNarrow = renderer + .create(templateComponents("none", true)) + .toJSON(); + + const templateWithChakraProps = renderer.create( + + ); + + const templateWithOtherProps = renderer.create( + + ); + + expect(templateComponentsLeftSidebar).toMatchSnapshot(); + expect(templateComponentsRightSidebar).toMatchSnapshot(); + expect(templateComponentsNoSidebar).toMatchSnapshot(); + expect(templateComponentsMainNarrow).toMatchSnapshot(); + expect(templateWithChakraProps).toMatchSnapshot(); + expect(templateWithOtherProps).toMatchSnapshot(); }); }); diff --git a/src/components/Template/Template.tsx b/src/components/Template/Template.tsx index 9ec95b073..a6b14adee 100644 --- a/src/components/Template/Template.tsx +++ b/src/components/Template/Template.tsx @@ -1,11 +1,15 @@ -import { Box, ChakraComponent, useStyleConfig } from "@chakra-ui/react"; +import { Box, chakra, ChakraComponent, useStyleConfig } from "@chakra-ui/react"; +import { forwardRef } from "react"; + +export const sidebarPlacementArray = ["none", "left", "right"] as const; +export type SidebarPlacement = typeof sidebarPlacementArray[number]; export interface TemplateProps { /** ID that other components can cross reference for accessibility purposes. */ id?: string; /** Renders the `TemplateSidebar` component either on the left or * right side of the `TemplateMain` component. */ - sidebar?: "none" | "left" | "right"; + sidebar?: SidebarPlacement; } export interface TemplateMainProps { @@ -23,25 +27,31 @@ const Template: ChakraComponent< React.PropsWithChildren & React.RefAttributes >, React.PropsWithChildren -> = ({ children, sidebar = "none" }) => { - const styles = useStyleConfig("TemplateNew", { variant: sidebar }); +> = chakra( + forwardRef>( + ({ children, sidebar = "none" }, ref?) => { + const styles = useStyleConfig("Template", { variant: sidebar }); - return {children}; -}; + return ( + + {children} + + ); + } + ) +); /** * This optional component renders its children above the main content * and spans edge-to-edge. It is most useful for `Breadcrumbs`, `Hero`, * or other banner-like components. */ -const TemplateBreakout: React.FC = ( - props: React.PropsWithChildren -) => { - const styles = useStyleConfig("TemplateNewBreakout", {}); +const TemplateBreakout: React.FC = ({ children }) => { + const styles = useStyleConfig("TemplateBreakout", {}); return ( - {props.children} + {children} ); }; @@ -51,9 +61,9 @@ const TemplateBreakout: React.FC = ( * will render below `TemplateBreakout` (if being used) and above the * main content and sidebar (if one exists). */ -const TemplateTop: React.FC = ( - props: React.PropsWithChildren -) => {props.children}; +const TemplateTop: React.FC = ({ children }) => ( + {children} +); /** * This component renders an HTML `
` element with an id of "mainContent". @@ -61,17 +71,14 @@ const TemplateTop: React.FC = ( * navigation link. The component should not be used in conjunction with * `TemplateMainNarrow`. */ -const TemplateMain: React.FC = ( - props: React.PropsWithChildren -) => { - const { children, id = "mainContent" } = props; - - return ( - - {children} - - ); -}; +const TemplateMain: React.FC> = ({ + children, + id = "mainContent", +}) => ( + + {children} + +); /** * This component renders an HTML `
` element with an id of "mainContent". @@ -81,18 +88,16 @@ const TemplateMain: React.FC = ( * lieu of `TemplateMain`, and should not be used in conjunction with * `TemplateSidebar`. */ -const TemplateMainNarrow: React.FC = ( - props: React.PropsWithChildren -) => { - const { children, id = "mainContent" } = props; - const styles = useStyleConfig("TemplateNewMainNarrow"); +const TemplateMainNarrow: React.FC> = + ({ children, id = "mainContent" }) => { + const styles = useStyleConfig("TemplateMainNarrow"); - return ( - - {children} - - ); -}; + return ( + + {children} + + ); + }; /** * This optional component is used to render content in a sidebar column. @@ -100,17 +105,17 @@ const TemplateMainNarrow: React.FC = ( * prop value of "left" or "right" must be passed to the `Template` wrapper * to render the correct CSS styles. */ -const TemplateSidebar: React.FC = ( - props: React.PropsWithChildren -) => {props.children}; +const TemplateSidebar: React.FC = ({ children }) => ( + {children} +); /** * This optional component renders content at a max width of 1280px and * will always render below the main content and sidebar (if one exists). */ -const TemplateBottom: React.FC = ( - props: React.PropsWithChildren -) => {props.children}; +const TemplateBottom: React.FC = ({ children }) => ( + {children} +); export { Template, diff --git a/src/components/Template/__snapshots__/Template.test.tsx.snap b/src/components/Template/__snapshots__/Template.test.tsx.snap index 44fe72b37..3ed58d778 100644 --- a/src/components/Template/__snapshots__/Template.test.tsx.snap +++ b/src/components/Template/__snapshots__/Template.test.tsx.snap @@ -57,3 +57,203 @@ exports[`Template components renders the UI snapshot correctly 1`] = ` `; + +exports[`Template components renders the UI snapshot correctly 2`] = ` +
+
+
+ Breakout +
+
+
+
+ Content Top +
+
+
+
+ Right Sidebar +
+
+
+
+ Main Content +
+
+ More Content +
+
+
+
+ Content Bottom +
+
+
+`; + +exports[`Template components renders the UI snapshot correctly 3`] = ` +
+
+
+ Breakout +
+
+
+
+ Content Top +
+
+
+
+ Main Content +
+
+ More Content +
+
+
+
+ Content Bottom +
+
+
+`; + +exports[`Template components renders the UI snapshot correctly 4`] = ` +
+
+
+ Breakout +
+
+
+
+ Content Top +
+
+
+
+ Main Content +
+
+ More Content +
+
+
+
+ Content Bottom +
+
+
+`; + +exports[`Template components renders the UI snapshot correctly 5`] = ` +
+
+
+ Main Content +
+
+ More Content +
+
+
+`; + +exports[`Template components renders the UI snapshot correctly 6`] = ` +
+
+
+ Main Content +
+
+ More Content +
+
+
+`; diff --git a/src/components/Template/templateChangelogData.ts b/src/components/Template/templateChangelogData.ts index 5804f35f7..57bd50cdb 100644 --- a/src/components/Template/templateChangelogData.ts +++ b/src/components/Template/templateChangelogData.ts @@ -14,9 +14,7 @@ export const changelogData: ChangelogData[] = [ version: "Prerelease", type: "Update", affects: ["Functionality", "Styles"], - notes: [ - "Major update to component and styles based on the [Template TAD](https://docs.google.com/document/d/1vZJX7Y-DnEM44-iWw5qoXGdxqavHEN6prhr-YDTNr0o/edit?pli=1&tab=t.0).", - ], + notes: ["Major update to component and styles based on the Template TAD."], }, { date: "2024-12-05", diff --git a/src/theme/components/template.ts b/src/theme/components/template.ts index 433ca8417..ec90435d8 100644 --- a/src/theme/components/template.ts +++ b/src/theme/components/template.ts @@ -1,113 +1,68 @@ import { defineStyleConfig } from "@chakra-ui/react"; import { defineStyle } from "@chakra-ui/system"; -/** - * Grid layout based on https://www.joshwcomeau.com/css/full-bleed/ - */ + +export const responsiveGap = { base: "1rem", md: "1.5rem", xl: "1rem" }; +export const responsivePadding = { base: "1rem", md: "1.5rem", xl: "2rem" }; const Template = defineStyleConfig({ baseStyle: defineStyle({ boxSizing: "border-box", color: "ui.typography.body", display: "grid", - gridTemplateColumns: ` - 1fr - min(1280px, 100%) - 1fr`, - rowGap: "grid.l", - _dark: { - color: "dark.ui.typography.body", - }, - }), - sizes: {}, - defaultProps: {}, -}); -// Elements that need to breakout will span outside -// the center 1280px grid column. -const TemplateBreakout = defineStyleConfig({ - baseStyle: defineStyle({ - width: "100%", - // This could be "1 / 4" and it would mean the same. This is - // "future-proof" the grid column assignment to the last column. - gridColumn: "1 / -1", + maxWidth: "1280px", + m: "0 auto", + p: "s", + gridTemplateAreas: `"breakout" "top" "main" "bottom"`, + gridTemplateColumns: "repeat(1, minmax(100px, 1fr))", + gridTemplateRows: "auto", + columnGap: responsiveGap, + "& > *:not(:last-child)": { mb: responsiveGap }, }), -}); -const TemplateContent = defineStyleConfig({ - baseStyle: defineStyle({ - // Set this element to start on the second 1280px grid column. - gridColumn: "2", - // This element also contains its own grid system within, but we use "flex" - // for mobile to deal with overflow issues related to the Table component. - display: { base: "flex", md: "grid" }, - flexDirection: { base: "column", md: null }, - gridTemplateColumns: "1fr", - paddingY: 0, - rowGap: "grid.l", - }), - // With left or right sidebars, we need to set two grid columns and - // the column for the sidebar is max 271px width (255px for the sidebar - // + 16px for padding). variants: { left: { - gridTemplateColumns: { md: "271px 1fr" }, + gridTemplateAreas: { + base: `"breakout" "top" "sidebar" "main" "bottom"`, + md: `"breakout breakout" "top top" "sidebar main" "bottom bottom"`, + }, + gridTemplateColumns: { + base: "repeat(1, minmax(100px, 1fr))", + md: "repeat(2, minmax(100px, 1fr))", + lg: "minmax(100px, 1fr) minmax(200px, 2fr)", + xl: "minmax(100px, 1fr) minmax(300px, 3fr)", + }, }, right: { - gridTemplateColumns: { md: "1fr 271px" }, + gridTemplateAreas: { + base: `"breakout" "top" "main" "sidebar" "bottom"`, + md: `"breakout breakout" "top top" "main sidebar" "bottom bottom"`, + }, + gridTemplateColumns: { + base: "repeat(1, minmax(100px, 1fr))", + md: "repeat(2, minmax(100px, 1fr))", + lg: "minmax(200px, 2fr) minmax(100px, 1fr)", + xl: "minmax(300px, 3fr) minmax(100px, 1fr)", + }, }, }, }); -const TemplateContentTopBottom = defineStyleConfig({ +const TemplateBreakout = defineStyleConfig({ baseStyle: defineStyle({ - gridColumn: { base: "1", md: "1 / span 2" }, - height: "100%", - paddingX: "s", + width: "100vw", + ml: "calc(-50vw + 50%)", + px: "s", }), }); -/** The overflow styles were added to deal with overflow issues related to the - * Table component. */ -const TemplateContentPrimary = defineStyleConfig({ +const TemplateMainNarrow = defineStyleConfig({ baseStyle: defineStyle({ - gridColumn: { base: "1", md: "1 / span 2" }, - paddingX: "s", + maxWidth: { lg: "720px" }, + m: { lg: "0 auto" }, }), - variants: { - left: { - gridColumn: { base: "1", md: "2" }, - marginEnd: { md: 0 }, - minWidth: { md: 0 }, - paddingRight: "s", - paddingLeft: { base: "s", md: "l" }, - overflow: { base: "unset", md: "hidden" }, - }, - right: { - gridColumn: "1", - paddingRight: { base: "s", md: "l" }, - paddingLeft: "s", - overflow: { base: "unset", md: "hidden" }, - }, - }, -}); -const TemplateContentSidebar = defineStyleConfig({ - variants: { - left: { - gridColumn: "1", - paddingLeft: "s", - paddingRight: { base: "s", md: 0 }, - }, - right: { - gridColumn: { base: "1", md: "2" }, - paddingLeft: { base: "s", md: 0 }, - paddingRight: "s", - }, - }, }); export default { Template, TemplateBreakout, - TemplateContent, - TemplateContentTopBottom, - TemplateContentPrimary, - TemplateContentSidebar, + TemplateMainNarrow, }; diff --git a/src/theme/components/templatenew.ts b/src/theme/components/templatenew.ts deleted file mode 100644 index f9195fd77..000000000 --- a/src/theme/components/templatenew.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { defineStyleConfig } from "@chakra-ui/react"; -import { defineStyle } from "@chakra-ui/system"; - -export const responsiveGap = { base: "1rem", md: "1.5rem", xl: "1rem" }; -export const responsivePadding = { base: "1rem", md: "1.5rem", xl: "2rem" }; - -const TemplateNew = defineStyleConfig({ - baseStyle: defineStyle({ - boxSizing: "border-box", - color: "ui.typography.body", - display: "grid", - maxWidth: "1280px", - m: "0 auto", - p: "s", - gridTemplateAreas: `"breakout" "top" "main" "bottom"`, - gridTemplateColumns: "repeat(1, minmax(100px, 1fr))", - gridTemplateRows: "auto", - columnGap: responsiveGap, - "& > *:not(:last-child)": { mb: responsiveGap }, - }), - variants: { - left: { - gridTemplateAreas: { - base: `"breakout" "top" "sidebar" "main" "bottom"`, - md: `"breakout breakout" "top top" "sidebar main" "bottom bottom"`, - }, - gridTemplateColumns: { - base: "repeat(1, minmax(100px, 1fr))", - md: "repeat(2, minmax(100px, 1fr))", - lg: "minmax(100px, 1fr) minmax(200px, 2fr)", - xl: "minmax(100px, 1fr) minmax(300px, 3fr)", - }, - }, - right: { - gridTemplateAreas: { - base: `"breakout" "top" "main" "sidebar" "bottom"`, - md: `"breakout breakout" "top top" "main sidebar" "bottom bottom"`, - }, - gridTemplateColumns: { - base: "repeat(1, minmax(100px, 1fr))", - md: "repeat(2, minmax(100px, 1fr))", - lg: "minmax(200px, 2fr) minmax(100px, 1fr)", - xl: "minmax(300px, 3fr) minmax(100px, 1fr)", - }, - }, - }, -}); - -const TemplateNewBreakout = defineStyleConfig({ - baseStyle: defineStyle({ - width: "100vw", - ml: "calc(-50vw + 50%)", - px: "s", - }), -}); - -const TemplateNewMainNarrow = defineStyleConfig({ - baseStyle: defineStyle({ - maxWidth: { lg: "720px" }, - m: { lg: "0 auto" }, - }), -}); - -export default { - TemplateNew, - TemplateNewBreakout, - TemplateNewMainNarrow, -}; diff --git a/src/theme/index.ts b/src/theme/index.ts index 2255dea01..2044aead1 100644 --- a/src/theme/index.ts +++ b/src/theme/index.ts @@ -71,7 +71,6 @@ import { SubNav, SubNavChildren } from "./components/subnav"; import Tabs from "./components/tabs"; import TagSetStyles from "./components/tagSet"; import TemplateStyles from "./components/template"; -import TemplateNewStyles from "./components/templatenew"; import Text from "./components/text"; import TextInput from "./components/textInput"; import Toggle from "./components/toggle"; @@ -176,7 +175,6 @@ const theme: any = { CustomTable, ...TagSetStyles, ...TemplateStyles, - ...TemplateNewStyles, Text, TextInput, ...Toggle,