From 3e0c078b6e7f6f9c169463e2701a26236b8381cc Mon Sep 17 00:00:00 2001 From: Richard Dinh <1038306+flacoman91@users.noreply.github.com> Date: Tue, 28 Apr 2026 13:14:15 -0700 Subject: [PATCH 1/2] update layout, make divider line match whatever height --- .../Layout/layout-content.stories.tsx | 67 +++++++-- src/components/Layout/layout-main.stories.tsx | 133 +++++++++++------- .../Layout/layout-sidebar.stories.tsx | 20 +-- .../Layout/layout-wrapper.stories.tsx | 19 +-- src/components/Layout/layout.scss | 57 ++++++++ 5 files changed, 217 insertions(+), 79 deletions(-) diff --git a/src/components/Layout/layout-content.stories.tsx b/src/components/Layout/layout-content.stories.tsx index 48f42abed0..16106853a7 100644 --- a/src/components/Layout/layout-content.stories.tsx +++ b/src/components/Layout/layout-content.stories.tsx @@ -52,8 +52,17 @@ export const Content: Story = { flushAllOnSmall: false, }, render: (properties) => ( - + + +

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? +

+

Layout.Sidebar

@@ -62,17 +71,55 @@ export const Content: Story = {
  • Item 2
  • Item 3
  • +

    Layout.Sidebar

    +
      +
    • Item 1
    • +
    • Item 2
    • +
    • Item 3
    • +
    +

    Layout.Sidebar

    +
      +
    • Item 1
    • +
    • Item 2
    • +
    • Item 3
    • +

    Layout.Sidebar

    +
      +
    • Item 1
    • +
    • Item 2
    • +
    • Item 3
    • +
    +

    Layout.Sidebar

    +
      +
    • Item 1
    • +
    • Item 2
    • +
    • Item 3
    • +
    +

    Layout.Sidebar

    +
      +
    • Item 1
    • +
    • Item 2
    • +
    • Item 3
    • +
    +

    Layout.Sidebar

    +
      +
    • Item 1
    • +
    • Item 2
    • +
    • Item 3
    • +
    +

    Layout.Sidebar

    +
      +
    • Item 1
    • +
    • Item 2
    • +
    • Item 3
    • +
    +

    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 index b570597409..3418fa6632 100644 --- a/src/components/Layout/layout-main.stories.tsx +++ b/src/components/Layout/layout-main.stories.tsx @@ -1,5 +1,7 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; +import type { ReactElement } from 'react'; import { Layout } from '~/src/index'; +import type { LayoutMainProperties } from './layout-main'; const meta: Meta = { title: 'Components (Draft)/Layout/Main', @@ -40,6 +42,8 @@ import Layout from './Layout
        < /Layout.Sidebar >
      < /Layout.Wrapper >
    < /Layout.Main >
    + +**Note:** For \`layout="1-3"\` (sidebar on the left), put \`Layout.Sidebar\` **before** \`Layout.Content\` inside \`Layout.Wrapper\`. For \`layout="2-1"\`, put **main first**, then sidebar—matching the [CFPB markup](https://cfpb.github.io/design-system/development/main-content-and-sidebars). `, }, }, @@ -50,64 +54,93 @@ export default meta; type Story = StoryObj; +const exampleContent = ( + +

    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? +

    +
    +); + +const exampleSidebar = ( + +
    +

    Sidebar

    +
      +
    • Item 1
    • +
    • Item 2
    • +
    • Item 3
    • +
    +

    Sidebar

    +
      +
    • Item 1
    • +
    • Item 2
    • +
    • Item 3
    • +
    +

    Sidebar

    +
      +
    • Item 1
    • +
    • Item 2
    • +
    • Item 3
    • +
    +

    Sidebar

    +
      +
    • Item 1
    • +
    • Item 2
    • +
    • Item 3
    • +
    +

    Sidebar

    +
      +
    • Item 1
    • +
    • Item 2
    • +
    • Item 3
    • +
    +

    Sidebar

    +
      +
    • Item 1
    • +
    • Item 2
    • +
    • Item 3
    • +
    +

    Sidebar

    +
      +
    • Item 1
    • +
    • Item 2
    • +
    • Item 3
    • +
    +
    +
    +); + +function renderMainLayout( + properties: LayoutMainProperties, +): ReactElement { + const layout = properties.layout ?? '2-1'; + const columnChildren = + layout === '1-3' + ? [exampleSidebar, exampleContent] + : [exampleContent, exampleSidebar]; + + return ( + + {columnChildren} + + ); +} + 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
    • -
    -
    -
    -
    -
    - ), + render: (properties) => renderMainLayout(properties), }; 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? -

    -
    -
    -
    - ), + render: (properties) => renderMainLayout(properties), }; diff --git a/src/components/Layout/layout-sidebar.stories.tsx b/src/components/Layout/layout-sidebar.stories.tsx index c17026ab43..7271a3e9b1 100644 --- a/src/components/Layout/layout-sidebar.stories.tsx +++ b/src/components/Layout/layout-sidebar.stories.tsx @@ -53,8 +53,17 @@ export const Sidebar: Story = { flushAllOnSmall: false, }, render: (properties) => ( - + + +

    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? +

    +

    Layout.Sidebar

    @@ -65,15 +74,6 @@ export const Sidebar: Story = {
    - -

    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-wrapper.stories.tsx b/src/components/Layout/layout-wrapper.stories.tsx index bfbd58278d..a371aca496 100644 --- a/src/components/Layout/layout-wrapper.stories.tsx +++ b/src/components/Layout/layout-wrapper.stories.tsx @@ -42,7 +42,17 @@ type Story = StoryObj; export const Wrapper: Story = { args: { + // Order matches default Layout.Main layout "2-1" (main first, then sidebar). children: [ + +

    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? +

    +
    ,

    Layout.Sidebar

    @@ -53,15 +63,6 @@ export const Wrapper: Story = {
    , - -

    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 }) => ( diff --git a/src/components/Layout/layout.scss b/src/components/Layout/layout.scss index 2599629e9c..5e268314b0 100644 --- a/src/components/Layout/layout.scss +++ b/src/components/Layout/layout.scss @@ -5,3 +5,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; + } +} From c1191720c923adde6050036b759e69192ea5bc78 Mon Sep 17 00:00:00 2001 From: Richard Dinh <1038306+flacoman91@users.noreply.github.com> Date: Tue, 28 Apr 2026 13:32:56 -0700 Subject: [PATCH 2/2] update unit tests --- src/components/Layout/layout.test.tsx | 169 ++++++++++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 src/components/Layout/layout.test.tsx 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); + }); +});