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-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..f64353ec10 --- /dev/null +++ b/src/components/Layout/layout-stories-shared.tsx @@ -0,0 +1,54 @@ +import type { ReactElement } from 'react'; +import { Layout } from '~/src/index'; +import type { LayoutMainProperties } from './layout-main'; + +export const LAYOUT_DOCS_SOURCE = + 'https://cfpb.github.io/design-system/development/main-content-and-sidebars'; + +export const LAYOUT_DOCS = { + component: `Use \`Layout.Main\`, \`Layout.Wrapper\`, \`Layout.Content\`, and \`Layout.Sidebar\` together to structure page content and optional sidebars. + +Main is the container for all content within a layout and configures column structure (\`layout\`) and whether the sidebar bleeds to the window edge. Content is the main body of the page. Wrapper positions content and sidebar columns. Sidebar is a vertical region beside the main content. + +Source: ${LAYOUT_DOCS_SOURCE}`, +} as const; + +export const LAYOUT_EXAMPLE_LOREM = + '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?'; + +export const LayoutExampleContent = (): ReactElement => ( + +

Content

+

{LAYOUT_EXAMPLE_LOREM}

+
+); + +export const LayoutExampleSidebar = (): ReactElement => ( + +
+

Sidebar

+
    +
  • Item 1
  • +
  • Item 2
  • +
  • Item 3
  • +
+
+
+); + +export const renderLayoutTwoColumnExample = ({ + layout = '2-1', +}: { + layout?: LayoutMainProperties['layout']; +}): ReactElement => { + const contentNode = LayoutExampleContent(); + const sidebarNode = LayoutExampleSidebar(); + const columnChildren = + layout === '1-3' ? [sidebarNode, contentNode] : [contentNode, sidebarNode]; + + return ( + + {columnChildren} + + ); +}; 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..6f72aef1d2 --- /dev/null +++ b/src/components/Layout/layout.stories.tsx @@ -0,0 +1,49 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { Layout } from '~/src/index'; +import { + LAYOUT_DOCS, + LayoutExampleContent, + renderLayoutTwoColumnExample, +} from './layout-stories-shared'; + +const meta: Meta = { + title: 'Components (Draft)/Layouts', + tags: ['autodocs'], + component: Layout.Main, + parameters: { + docs: { + description: { + component: LAYOUT_DOCS.component, + }, + }, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const BasicMain: Story = { + name: 'Basic main', + render: () => ( + + {LayoutExampleContent()} + + ), +}; + +export const Layout_2_1: Story = { + name: '2-1 layout', + args: { + layout: '2-1', + }, + render: ({ layout = '2-1' }) => renderLayoutTwoColumnExample({ layout }), +}; + +export const Layout_1_3: Story = { + name: '1-3 layout', + args: { + layout: '1-3', + }, + render: ({ layout = '1-3' }) => renderLayoutTwoColumnExample({ layout }), +}; diff --git a/src/components/Layout/layout.test.tsx b/src/components/Layout/layout.test.tsx new file mode 100644 index 0000000000..71154cb31d --- /dev/null +++ b/src/components/Layout/layout.test.tsx @@ -0,0 +1,169 @@ +import '@testing-library/jest-dom'; +import { render, screen } from '@testing-library/react'; +import Layout from './layout'; + +describe('Layout.Main', () => { + it('renders main landmark with default 2-1 layout classes', () => { + render( + + child + , + ); + + const main = screen.getByRole('main'); + expect(main).toHaveClass('content', 'content--2-1'); + expect(main).toHaveAttribute('id', 'main'); + expect(screen.getByText('child')).toBeInTheDocument(); + }); + + 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); + }); +});