Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/dirty-cooks-clap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@primer/react': minor
---

PageLayout: Add `PageLayout.Sidebar` sub-component
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 24 additions & 0 deletions e2e/components/PageLayout.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,30 @@ const stories = [
id: 'components-pagelayout-features--scroll-container-within-page-layout-pane',
title: 'Scroll Container Within Page Layout Pane',
},
{
id: 'components-pagelayout-features--sidebar-start',
title: 'Sidebar Start',
},
{
id: 'components-pagelayout-features--sidebar-end',
title: 'Sidebar End',
},
{
id: 'components-pagelayout-features--resizable-sidebar',
title: 'Resizable Sidebar',
},
{
id: 'components-pagelayout-features--sidebar-with-pane-resizable',
title: 'Sidebar With Pane Resizable',
},
{
id: 'components-pagelayout-features--sticky-sidebar',
title: 'Sticky Sidebar',
},
{
id: 'components-pagelayout-features--sidebar-fullscreen-responsive-variant',
title: 'Sidebar Fullscreen Responsive Variant',
},
] as const

test.describe('PageLayout', () => {
Expand Down
62 changes: 62 additions & 0 deletions e2e/components/SplitPageLayout.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import {test, expect} from '@playwright/test'
import {visit} from '../test-helpers/storybook'
import {themes} from '../test-helpers/themes'

const stories = [
{
id: 'components-splitpagelayout--default',
title: 'Default',
},
{
id: 'components-splitpagelayout-features--settings-page',
title: 'Settings Page',
},
{
id: 'components-splitpagelayout-features--with-sidebar-start',
title: 'With Sidebar Start',
},
{
id: 'components-splitpagelayout-features--with-sidebar-end',
title: 'With Sidebar End',
},
{
id: 'components-splitpagelayout-features--with-resizable-sidebar',
title: 'With Resizable Sidebar',
},
{
id: 'components-splitpagelayout-features--with-sidebar-and-resizable-pane',
title: 'With Sidebar And Resizable Pane',
},
{
id: 'components-splitpagelayout-features--with-sticky-sidebar',
title: 'With Sticky Sidebar',
},
{
id: 'components-splitpagelayout-features--sidebar-fullscreen-responsive-variant',
title: 'Sidebar Fullscreen Responsive Variant',
},
] as const

test.describe('SplitPageLayout', () => {
for (const story of stories) {
test.describe(story.title, () => {
for (const theme of themes) {
test.describe(theme, () => {
test('default @vrt', async ({page}) => {
await visit(page, {
id: story.id,
globals: {
colorScheme: theme,
},
})

// Default state
expect(await page.screenshot({animations: 'disabled'})).toMatchSnapshot(
`SplitPageLayout.${story.title}.${theme}.png`,
)
})
})
}
})
}
})
87 changes: 87 additions & 0 deletions packages/react/src/PageLayout/PageLayout.docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,24 @@
},
{
"id": "components-pagelayout-features--resizable-pane-with-controlled-width"
},
{
"id": "components-pagelayout-features--sidebar-start"
},
{
"id": "components-pagelayout-features--sidebar-end"
},
{
"id": "components-pagelayout-features--resizable-sidebar"
},
{
"id": "components-pagelayout-features--sidebar-with-pane-resizable"
},
{
"id": "components-pagelayout-features--sticky-sidebar"
},
{
"id": "components-pagelayout-features--sidebar-fullscreen-responsive-variant"
}
],
"importPath": "@primer/react",
Expand Down Expand Up @@ -251,6 +269,75 @@
}
]
},
{
"name": "PageLayout.Sidebar",
"props": [
{
"name": "aria-label",
"type": "string | undefined",
"description": "A unique label for the sidebar region."
},
{
"name": "aria-labelledby",
"type": "string | undefined",
"description": "An id to an element which uniquely labels the sidebar region."
},
{
"name": "position",
"type": "'start' | 'end'",
"defaultValue": "'start'",
"description": "Position of the sidebar relative to the page layout. The sidebar spans the full height of the layout, adjacent to the header, content, and footer."
},
{
"name": "width",
"type": "| 'small' | 'medium' | 'large' | { min: string max: string default: string }",
"defaultValue": "'medium'",
"description": "The width of the sidebar. If using custom widths, provide an object with keys 'min', 'max' and 'default'."
},
{
"name": "minWidth",
"type": "number",
"defaultValue": "256",
"description": "Minimum width of the sidebar when resizable."
},
{
"name": "resizable",
"type": "boolean",
"defaultValue": "false",
"description": "When true, the sidebar may be resized by the user. Width is persisted to localStorage by default."
},
{
"name": "padding",
"type": "| 'none' | 'condensed' | 'normal'",
"defaultValue": "'none'",
"description": "The amount of padding inside the sidebar."
},
{
"name": "divider",
"type": "| 'none' | 'line'",
"defaultValue": "'none'",
"description": "Divider style between the sidebar and the rest of the layout."
},
{
"name": "sticky",
"type": "boolean",
"defaultValue": "false",
"description": "Whether the sidebar sticks to the viewport when scrolling. When enabled, the sidebar uses position: sticky with top: 0 and height: 100vh."
},
{
"name": "responsiveVariant",
"type": "| 'default' | 'fullscreen'",
"defaultValue": "'default'",
"description": "Controls sidebar behavior at narrow viewport widths (below 768px). 'default' retains its normal inline layout. 'fullscreen' expands to cover the full viewport like a dialog overlay."
},
{
"name": "hidden",
"type": "| boolean | { narrow?: boolean regular?: boolean wide?: boolean }",
"defaultValue": "false",
"description": "Whether the sidebar is hidden."
}
]
},
{
"name": "PageLayout.Footer",
"props": [
Expand Down
123 changes: 123 additions & 0 deletions packages/react/src/PageLayout/PageLayout.features.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,129 @@ export const WithCustomPaneHeading: StoryFn = () => (
</PageLayout>
)

export const SidebarStart: StoryFn = () => (
<PageLayout containerWidth="full">
<PageLayout.Sidebar position="start" aria-label="Navigation sidebar">
<Placeholder height={800} label="Sidebar (Start)" />
</PageLayout.Sidebar>
<PageLayout.Header>
<Placeholder height={64} label="Header" />
</PageLayout.Header>
<PageLayout.Content>
<Placeholder height={640} label="Content" />
</PageLayout.Content>
<PageLayout.Footer>
<Placeholder height={64} label="Footer" />
</PageLayout.Footer>
</PageLayout>
)

export const SidebarEnd: StoryFn = () => (
<PageLayout containerWidth="full">
<PageLayout.Sidebar position="end" aria-label="Inspector sidebar">
<Placeholder height={800} label="Sidebar (End)" />
</PageLayout.Sidebar>
<PageLayout.Header>
<Placeholder height={64} label="Header" />
</PageLayout.Header>
<PageLayout.Content>
<Placeholder height={640} label="Content" />
</PageLayout.Content>
<PageLayout.Footer>
<Placeholder height={64} label="Footer" />
</PageLayout.Footer>
</PageLayout>
)

export const ResizableSidebar: StoryFn = () => (
<PageLayout containerWidth="full">
<PageLayout.Sidebar
resizable
position="end"
aria-label="Resizable sidebar"
style={{height: 'auto'}}
width={{
min: '200px',
default: '300px',
max: '2000px',
}}
>
<Placeholder height="100%" label="Resizable Sidebar" />
</PageLayout.Sidebar>
<PageLayout.Header>
<Placeholder height={64} label="Header" />
</PageLayout.Header>
<PageLayout.Pane position="start" aria-label="Side pane">
<Placeholder height={320} label="Pane" />
</PageLayout.Pane>
<PageLayout.Content>
<Placeholder height={640} label="Content" />
</PageLayout.Content>
<PageLayout.Footer>
<Placeholder height={64} label="Footer" />
</PageLayout.Footer>
</PageLayout>
)

export const SidebarWithPaneResizable: StoryFn = () => (
<PageLayout containerWidth="full">
<PageLayout.Sidebar
style={{height: 'auto'}}
resizable
position="end"
aria-label="Navigation sidebar"
width={{min: '200px', default: '300px', max: '2000px'}}
>
<Placeholder height="100%" label="Resizable Sidebar" />
</PageLayout.Sidebar>
<PageLayout.Header>
<Placeholder height={64} label="Header" />
</PageLayout.Header>
<PageLayout.Pane resizable position="start" aria-label="Side pane">
<Placeholder height={320} label="Resizable Pane" />
</PageLayout.Pane>
<PageLayout.Content>
<Placeholder height={640} label="Content" />
</PageLayout.Content>
<PageLayout.Footer>
<Placeholder height={64} label="Footer" />
</PageLayout.Footer>
</PageLayout>
)

export const StickySidebar: StoryFn = () => (
<PageLayout containerWidth="full">
<PageLayout.Sidebar sticky position="start" aria-label="Sticky sidebar">
<Placeholder height={200} label="Sticky Sidebar" />
</PageLayout.Sidebar>
<PageLayout.Header>
<Placeholder height={64} label="Header" />
</PageLayout.Header>
<PageLayout.Content>
<Placeholder height={2000} label="Tall Content (scroll to test sticky)" />
</PageLayout.Content>
<PageLayout.Footer>
<Placeholder height={64} label="Footer" />
</PageLayout.Footer>
</PageLayout>
)

export const SidebarFullscreenResponsiveVariant: StoryFn = () => (
<PageLayout containerWidth="full">
<PageLayout.Sidebar position="start" responsiveVariant="fullscreen" aria-label="Fullscreen sidebar">
<Placeholder height={800} label="Sidebar (fullscreen at narrow)" />
</PageLayout.Sidebar>
<PageLayout.Header>
<Placeholder height={64} label="Header" />
</PageLayout.Header>
<PageLayout.Content>
<Placeholder height={640} label="Content" />
</PageLayout.Content>
<PageLayout.Footer>
<Placeholder height={64} label="Footer" />
</PageLayout.Footer>
</PageLayout>
)
export const ResizablePaneWithoutPersistence: StoryFn = () => {
const [currentWidth, setCurrentWidth] = React.useState<number>(defaultPaneWidth.medium)

Expand Down
Loading
Loading