diff --git a/docs/local-cfpb-ds.md b/docs/local-cfpb-ds.md new file mode 100644 index 0000000000..5f62aff597 --- /dev/null +++ b/docs/local-cfpb-ds.md @@ -0,0 +1,47 @@ +# Pointing design-system-react at a local `@cfpb/cfpb-design-system` + +Use this when you have the [cfpb/design-system](https://github.com/cfpb/design-system) repo cloned next to this repo and want Storybook/tests to use your branch (e.g. layout fixes) before a release. + +## Layout + +Assume sibling directories: + +```text +projects/ + design-system/ # monorepo root; package lives in packages/cfpb-design-system/ + design-system-react/ # this repo +``` + +If your paths differ, adjust the `portal:` URL below. + +## Yarn (Berry) + +In **design-system-react** `package.json`, temporarily set the devDependency to the **portal** protocol (live symlink to source): + +```json +"@cfpb/cfpb-design-system": "portal:../design-system/packages/cfpb-design-system" +``` + +Then from **design-system-react**: + +```bash +yarn install +yarn storybook +# optional +yarn test +``` + +`portal:` keeps the dependency wired to your clone so SCSS/JS changes in `design-system` show up after save (no publish step). + +## After you’re done + +1. Remove the `portal:` line and restore the published version (e.g. `"5.3.2"`). +2. Run `yarn install` again. + +## Optional: trim duplicate Layout CSS here + +`src/components/Layout/layout.scss` in this repo duplicates some rules that belong in the DS once your PR ships. After you adopt a released `@cfpb/cfpb-design-system` that includes the layout fix, consider removing the overlapping blocks from `layout.scss` so overrides stay minimal. + +## Alternative: `yarn link` + +From `design-system/packages/cfpb-design-system` you can `yarn link`, then in design-system-react `yarn link @cfpb/cfpb-design-system`. Portal is usually simpler in a Yarn workspaces/monorepo workflow. diff --git a/src/components/Layout/layout-content.stories.tsx b/src/components/Layout/layout-content.stories.tsx deleted file mode 100644 index 48f42abed0..0000000000 --- a/src/components/Layout/layout-content.stories.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react-vite'; -import { Layout } from '~/src/index'; - -const meta: Meta = { - title: 'Components (Draft)/Layout/Content', - tags: ['autodocs'], - component: Layout.Content, - parameters: { - docs: { - description: { - component: ` -### CFPB DS Layout.Content component - -Container for the primary page content within a layout. - - -Source: https://cfpb.github.io/design-system/development/main-content-and-sidebars - -### Usage - -import Layout from './Layout
- -< Layout.Main >
-  < Hero / >
-  < Layout.Wrapper >
-    < Layout.Content >
-      Main Content
-    < /Layout.Content >
-    < Layout.Sidebar >
-      Sidebar Content
-    < /Layout.Sidebar >
-  < /Layout.Wrapper >
-< /Layout.Main >
-`, - }, - }, - }, -}; - -export default meta; - -type Story = StoryObj; - -export const Content: Story = { - args: { - flushBottom: false, - flushTopOnSmall: false, - flushAllOnSmall: false, - }, - render: (properties) => ( - - - -
-

Layout.Sidebar

-
    -
  • Item 1
  • -
  • Item 2
  • -
  • Item 3
  • -
-
-
- -

Layout.Content

-

- Lorem ipsum, dolor sit amet consectetur adipisicing elit. Quaerat - alias eum ut officiis optio similique explicabo cupiditate - architecto voluptatem nostrum recusandae, eaque consectetur iure, - veritatis eos, mollitia possimus error earum? -

-
-
-
- ), -}; diff --git a/src/components/Layout/layout-main.stories.tsx b/src/components/Layout/layout-main.stories.tsx deleted file mode 100644 index b570597409..0000000000 --- a/src/components/Layout/layout-main.stories.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react-vite'; -import { Layout } from '~/src/index'; - -const meta: Meta = { - title: 'Components (Draft)/Layout/Main', - tags: ['autodocs'], - component: Layout.Main, - parameters: { - docs: { - description: { - component: ` -### CFPB DS Layout.Main component - -Container for all of the content within a Layout. Used to configure the column structure ('layout') and whether the sidebar bleeds to the window edge ('bleedbar'). - - -
    -
  • layout
      -
    • [1-3](https://cfpb.github.io/design-system/development/main-content-and-sidebars#left-hand-sidebar-layout)
    • -
    • [2-1](https://cfpb.github.io/design-system/development/main-content-and-sidebars#right-hand-sidebar-layout)
    • -
  • - -
- - -Source: https://cfpb.github.io/design-system/development/main-content-and-sidebars - -### Usage - -import Layout from './Layout
- -< Layout.Main >
-  < Hero / >
-  < Layout.Wrapper >
-    < Layout.Content >
-      Main Content
-    < /Layout.Content >
-    < Layout.Sidebar >
-      Sidebar Content
-    < /Layout.Sidebar >
-  < /Layout.Wrapper >
-< /Layout.Main >
-`, - }, - }, - }, -}; - -export default meta; - -type Story = StoryObj; - -export const Layout_2_1: Story = { - args: { - layout: '2-1', - }, - render: (properties) => ( - - - -

Content

-

- Lorem ipsum, dolor sit amet consectetur adipisicing elit. Quaerat - alias eum ut officiis optio similique explicabo cupiditate - architecto voluptatem nostrum recusandae, eaque consectetur iure, - veritatis eos, mollitia possimus error earum? -

-
- -
-

Sidebar

-
    -
  • Item 1
  • -
  • Item 2
  • -
  • Item 3
  • -
-
-
-
-
- ), -}; - -export const Layout_1_3: Story = { - args: { - layout: '1-3', - }, - render: (properties) => ( - - - -
-

Sidebar

-
    -
  • Item 1
  • -
  • Item 2
  • -
  • Item 3
  • -
-
-
- -

Content

-

- Lorem ipsum, dolor sit amet consectetur adipisicing elit. Quaerat - alias eum ut officiis optio similique explicabo cupiditate - architecto voluptatem nostrum recusandae, eaque consectetur iure, - veritatis eos, mollitia possimus error earum? -

-
-
-
- ), -}; diff --git a/src/components/Layout/layout-main.tsx b/src/components/Layout/layout-main.tsx index 8545b2ca55..d6b148f26c 100644 --- a/src/components/Layout/layout-main.tsx +++ b/src/components/Layout/layout-main.tsx @@ -11,13 +11,13 @@ export const LayoutMain = ({ children, classes = '', id = 'main', - layout = '2-1', -}: LayoutMainProperties): JSX.Element => { - const cnames = ['content', `content--${layout}`, classes]; - - return ( -
- {children} -
- ); -}; + layout, +}: LayoutMainProperties): JSX.Element => ( +
+ {children} +
+); diff --git a/src/components/Layout/layout-sidebar.stories.tsx b/src/components/Layout/layout-sidebar.stories.tsx deleted file mode 100644 index c17026ab43..0000000000 --- a/src/components/Layout/layout-sidebar.stories.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react-vite'; -import { Layout } from '~/src/index'; - -const meta: Meta = { - title: 'Components (Draft)/Layout/Sidebar', - tags: ['autodocs'], - component: Layout.Sidebar, - parameters: { - docs: { - description: { - component: ` -### CFPB DS Layout.Sidebar component - -Container for the sidebar content within a layout. - -
    -
  • [flushBottom](https://cfpb.github.io/design-system/development/main-content-and-sidebars#flush-bottom-modifier)
  • -
  • [flushTopOnSmall](https://cfpb.github.io/design-system/development/main-content-and-sidebars#flush-top-modifier-only-on-small-screens)
  • -
  • [flushAllOnSmall](https://cfpb.github.io/design-system/development/main-content-and-sidebars#flush-all-modifier-only-on-small-screens)
  • -
- -Source: https://cfpb.github.io/design-system/development/main-content-and-sidebars - -### Usage - -import Layout from './Layout
- -< Layout.Main >
-  < Hero / >
-  < Layout.Wrapper >
-    < Layout.Content >
-      Main Content
-    < /Layout.Content >
-    < Layout.Sidebar >
-      Sidebar Content
-    < /Layout.Sidebar >
-  < /Layout.Wrapper >
-< /Layout.Main >
-`, - }, - }, - }, -}; - -export default meta; - -type Story = StoryObj; - -export const Sidebar: Story = { - args: { - flushBottom: false, - flushTopOnSmall: false, - flushAllOnSmall: false, - }, - render: (properties) => ( - - - -
-

Layout.Sidebar

-
    -
  • Item 1
  • -
  • Item 2
  • -
  • Item 3
  • -
-
-
- -

Layout.Content

-

- Lorem ipsum, dolor sit amet consectetur adipisicing elit. Quaerat - alias eum ut officiis optio similique explicabo cupiditate - architecto voluptatem nostrum recusandae, eaque consectetur iure, - veritatis eos, mollitia possimus error earum? -

-
-
-
- ), -}; diff --git a/src/components/Layout/layout-stories-shared.tsx b/src/components/Layout/layout-stories-shared.tsx new file mode 100644 index 0000000000..e6535a710e --- /dev/null +++ b/src/components/Layout/layout-stories-shared.tsx @@ -0,0 +1,30 @@ +import type { ReactElement } from 'react'; + +export const LAYOUT_DOCS_SOURCE = + 'https://cfpb.github.io/design-system/development/main-content-and-sidebars'; + +export const LAYOUT_DOCS = { + component: `Layout is a **composition API** that mirrors the [CFPB main content and sidebars](${LAYOUT_DOCS_SOURCE}) pattern. Assemble \`Layout.Main\`, \`Layout.Wrapper\`, \`Layout.Content\`, and optionally \`Layout.Sidebar\`. + +| Piece | Role | +| ----- | ---- | +| **Main** | Page \`
\` landmark (\`.content\`). Set \`layout="2-1"\` or \`layout="1-3"\` for two-column layouts; omit \`layout\` when main and sidebar stack vertically. | +| **Wrapper** | \`.wrapper\` around columns (hero or \`Divider\` may sit above it inside Main). | +| **Content** | Primary page body (\`.content__main\`). | +| **Sidebar** | Optional aside (\`.content__sidebar\`). Order in the wrapper must match the layout (sidebar then main for \`1-3\`; main then sidebar for \`2-1\`). | +| **Content / Sidebar modifiers** | \`flushBottom\`, \`flushTopOnSmall\`, and \`flushAllOnSmall\` map to \`.content--flush-bottom\`, \`.content--flush-top-on-small\`, and \`.content--flush-all-on-small\`. | + +The stories below follow the live examples on the design system documentation page.`, +} as const; + +/** Lorem copy from the CFPB left/right sidebar layout examples. */ +export const LAYOUT_EXAMPLE_LOREM = `Lorem ipsum dolor sit amet, consectetur adipisicing elit. +Cum corrupti tempora nam nihil qui mollitia consectetur +corporis nemo culpa dolorum! Laborum at eos deleniti +consequatur itaque officiis debitis quisquam! Provident!`; + +export const LayoutStoryFooter = (): ReactElement => ( +
+
Footer
+
+); diff --git a/src/components/Layout/layout-wrapper.stories.tsx b/src/components/Layout/layout-wrapper.stories.tsx deleted file mode 100644 index bfbd58278d..0000000000 --- a/src/components/Layout/layout-wrapper.stories.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react-vite'; -import { Layout } from '~/src/index'; - -const meta: Meta = { - title: 'Components (Draft)/Layout/Wrapper', - tags: ['autodocs'], - component: Layout.Wrapper, - parameters: { - docs: { - description: { - component: ` -### CFPB DS Layout.Wrapper component - -Container to help position Content and Sidebar elements. - -Source: https://cfpb.github.io/design-system/development/main-content-and-sidebars - -### Usage - -import Layout from './Layout
- -< Layout.Main >
-  < Hero / >
-  < Layout.Wrapper >
-    < Layout.Content >
-      Main Content
-    < /Layout.Content >
-    < Layout.Sidebar >
-      Sidebar Content
-    < /Layout.Sidebar >
-  < /Layout.Wrapper >
-< /Layout.Main >
-`, - }, - }, - }, -}; - -export default meta; - -type Story = StoryObj; - -export const Wrapper: Story = { - args: { - children: [ - -
-

Layout.Sidebar

-
    -
  • Item 1
  • -
  • Item 2
  • -
  • Item 3
  • -
-
-
, - -

Layout.Content

-

- Lorem ipsum, dolor sit amet consectetur adipisicing elit. Quaerat - alias eum ut officiis optio similique explicabo cupiditate architecto - voluptatem nostrum recusandae, eaque consectetur iure, veritatis eos, - mollitia possimus error earum? -

-
, - ], - }, - render: ({ children }) => ( - - {children} - - ), -}; diff --git a/src/components/Layout/layout.scss b/src/components/Layout/layout.scss index 2599629e9c..dd3c45ba3b 100644 --- a/src/components/Layout/layout.scss +++ b/src/components/Layout/layout.scss @@ -1,3 +1,6 @@ +// Layout overrides for React Layout stories (`layout.stories.tsx`) and consumers. +// At wide viewports, flex + the 2-1 divider below match the CFPB DS main/sidebar examples. + // Override Design System styles that cause content to overflow sidebar boundaries @media only all and (width >= 37.5625em) { .content--2-1 .content__main, @@ -5,3 +8,60 @@ margin-right: 0 !important; } } + +// At the two-column breakpoint, use flex so main and sidebar share the row height of the +// taller column. The vertical divider is drawn from `.content__main::after` with `bottom: 0`, +// so it only reaches the bottom of `.content__main`; stretching that box matches the divider +// to the full sidebar/main height (CFPB DS uses inline-block, which does not equalize height). +@media only screen and (width >= 56.3125em) { + .content--1-3 .wrapper, + .content--2-1 .wrapper { + align-items: stretch; + display: flex; + } + + .content--1-3 .wrapper > .content__main, + .content--1-3 .wrapper > .content__sidebar, + .content--2-1 .wrapper > .content__main, + .content--2-1 .wrapper > .content__sidebar { + display: block; + margin-right: 0 !important; + } + + .content--1-3 .wrapper > .content__sidebar { + flex: 0 0 25%; + max-width: 25%; + } + + .content--1-3 .wrapper > .content__main { + flex: 0 0 75%; + max-width: 75%; + } + + .content--2-1 .wrapper > .content__main { + flex: 0 0 66.6667%; + max-width: 66.6667%; + } + + .content--2-1 .wrapper > .content__sidebar { + flex: 0 0 33.3333%; + max-width: 33.3333%; + } + + // CFPB DS 5.3.2 defines the column divider via `.content__main::after` for `content--1-3` + // but only emits `right: -1.875em` for `content--2-1` (no `content`, border, or positioning). + // Mirror the 1-3 rule so the vertical rule appears between main and a right-hand sidebar. + .content--2-1 .content__main { + position: relative; + } + + .content--2-1 .content__main::after { + border-right: 1px solid var(--content-main-border); + bottom: 0; + content: ''; + position: absolute; + right: -1.875em; + top: 2.8125em; + width: 0; + } +} diff --git a/src/components/Layout/layout.stories.tsx b/src/components/Layout/layout.stories.tsx new file mode 100644 index 0000000000..57e526a2b4 --- /dev/null +++ b/src/components/Layout/layout.stories.tsx @@ -0,0 +1,186 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { Divider, Layout } from '~/src/index'; +import { + LAYOUT_DOCS, + LAYOUT_EXAMPLE_LOREM, + LayoutStoryFooter, +} from './layout-stories-shared'; + +const meta: Meta = { + title: 'Components (Draft)/Layouts', + tags: ['autodocs'], + component: Layout.Main, + parameters: { + layout: 'fullscreen', + docs: { + description: { + component: LAYOUT_DOCS.component, + }, + }, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const MainContentAndSidebar: Story = { + name: 'Main content and sidebar', + render: () => ( + +
+ Content hero +
+ + Main content area + Sidebar + +
+ ), + parameters: { + docs: { + description: { + story: + 'Standard layout for the main content area and sidebar. By default `.content__main` and `.content__sidebar` stack vertically. Column modifiers (`2-1`, `1-3`) convert to side-by-side columns at 801px. Inline styling is for demonstration only.', + }, + }, + }, +}; + +export const LeftHandSidebarLayout: Story = { + name: 'Left-hand sidebar layout', + render: () => ( + <> + + + + Section navigation + +

Main content area

+

{LAYOUT_EXAMPLE_LOREM}

+
+
+
+ + + ), + parameters: { + docs: { + description: { + story: + 'Add `layout="1-3"` to `Layout.Main` for a 1:3 ratio with the sidebar on the left and main content on the right. Place `Layout.Sidebar` before `Layout.Content` in the wrapper.', + }, + }, + }, +}; + +export const RightHandSidebarLayout: Story = { + name: 'Right-hand sidebar layout', + render: () => ( + <> + + + + +

Main content area

+

{LAYOUT_EXAMPLE_LOREM}

+
+ Sidebar +
+
+ + + ), + parameters: { + docs: { + description: { + story: + 'Add `layout="2-1"` to `Layout.Main` for a 2:1 ratio with main content on the left and the sidebar on the right. Place `Layout.Content` before `Layout.Sidebar` in the wrapper.', + }, + }, + }, +}; + +export const FlushBottomModifier: Story = { + name: 'Flush bottom modifier', + render: () => ( + <> + + + + + Side with no bottom padding... + + + Main content with no bottom padding... +
+ .content--flush-bottom is very useful when you have a content block + inside of .content__main with a background and flush sides. +
+
+
+
+ + + ), + parameters: { + docs: { + description: { + story: + 'Set `flushBottom` on `Layout.Content` or `Layout.Sidebar` to apply `.content--flush-bottom` and remove bottom padding.', + }, + }, + }, +}; + +export const FlushTopOnSmallModifier: Story = { + name: 'Flush top modifier (only on small screens)', + render: () => ( + <> + + + + + Side with no top padding on small screens... + + Main content + + + + + ), + parameters: { + docs: { + description: { + story: + 'Set `flushTopOnSmall` on `Layout.Content` or `Layout.Sidebar` to apply `.content--flush-top-on-small`. Top padding is removed only on small screens, where main and sidebar stack in a single column.', + }, + }, + }, +}; + +export const FlushAllOnSmallModifier: Story = { + name: 'Flush all modifier (only on small screens)', + render: () => ( + <> + + + + + Side with no padding or border-based gutters on small screens... + + Main content + + + + + ), + parameters: { + docs: { + description: { + story: + 'Set `flushAllOnSmall` on `Layout.Content` or `Layout.Sidebar` to apply `.content--flush-all-on-small`. All padding and border-based gutters are removed on small screens only.', + }, + }, + }, +}; diff --git a/src/components/Layout/layout.test.tsx b/src/components/Layout/layout.test.tsx new file mode 100644 index 0000000000..34b4828d9d --- /dev/null +++ b/src/components/Layout/layout.test.tsx @@ -0,0 +1,180 @@ +import '@testing-library/jest-dom'; +import { render, screen } from '@testing-library/react'; +import Layout from './layout'; + +describe('Layout.Main', () => { + it('renders main landmark without a column layout class by default', () => { + render( + + child + , + ); + + const main = screen.getByRole('main'); + expect(main).toHaveClass('content'); + expect(main).not.toHaveClass('content--2-1', 'content--1-3'); + expect(main).toHaveAttribute('id', 'main'); + expect(screen.getByText('child')).toBeInTheDocument(); + }); + + it('applies 2-1 layout class when layout is 2-1', () => { + render( + + child + , + ); + + expect(screen.getByRole('main')).toHaveClass('content--2-1'); + }); + + it('applies 1-3 layout class when layout is 1-3', () => { + render( + + child + , + ); + + expect(screen.getByRole('main')).toHaveClass('content--1-3'); + }); + + it('accepts custom id and extra classes', () => { + render( + + child + , + ); + + const main = screen.getByRole('main'); + expect(main).toHaveAttribute('id', 'page-main'); + expect(main).toHaveClass('extra-class'); + }); +}); + +describe('Layout.Wrapper', () => { + it('renders wrapper class and passes through div attributes', () => { + render( + + inner + , + ); + + const wrap = screen.getByTestId('wrap'); + expect(wrap).toHaveClass('wrapper'); + expect(wrap).toHaveAttribute('aria-label', 'Page'); + expect(wrap).toHaveTextContent('inner'); + }); +}); + +describe('Layout.Content', () => { + it('renders content__main and optional flush modifiers', () => { + const { rerender } = render( + + body + , + ); + + let node = screen.getByTestId('content'); + expect(node).toHaveClass('content__main'); + expect(node).not.toHaveClass('content--flush-bottom'); + + rerender( + + body + , + ); + + node = screen.getByTestId('content'); + expect(node).toHaveClass( + 'content__main', + 'content--flush-bottom', + 'content--flush-top-on-small', + 'content--flush-all-on-small', + ); + }); +}); + +describe('Layout.Sidebar', () => { + it('renders aside with sidebar classes and optional flush modifiers', () => { + const { rerender } = render( + nav, + ); + + let aside = screen.getByTestId('side'); + expect(aside.tagName).toBe('ASIDE'); + expect(aside).toHaveClass('sidebar', 'content__sidebar', 'o-sidebar-content'); + expect(aside).not.toHaveClass('content--flush-bottom'); + + rerender( + + nav + , + ); + + aside = screen.getByTestId('side'); + expect(aside).toHaveClass( + 'content--flush-bottom', + 'content--flush-top-on-small', + 'content--flush-all-on-small', + ); + }); +}); + +describe('Layout composition (CFPB DOM order)', () => { + it('2-1: main column precedes sidebar in document order', () => { + render( + + + + Main + + + Side + + + , + ); + + const mainCol = screen.getByTestId('layout-main-col'); + const sidebar = screen.getByTestId('layout-sidebar-col'); + expect( + Boolean( + mainCol.compareDocumentPosition(sidebar) & + Node.DOCUMENT_POSITION_FOLLOWING, + ), + ).toBe(true); + }); + + it('1-3: sidebar precedes main column in document order', () => { + render( + + + + Side + + + Main + + + , + ); + + const mainCol = screen.getByTestId('layout-main-col'); + const sidebar = screen.getByTestId('layout-sidebar-col'); + expect( + Boolean( + sidebar.compareDocumentPosition(mainCol) & + Node.DOCUMENT_POSITION_FOLLOWING, + ), + ).toBe(true); + }); +});