Skip to content

Commit

Permalink
PageLayout: Implement responsive hidden prop (#2174)
Browse files Browse the repository at this point in the history
* Implement responsive hidden prop

* Write tests for useResponsiveValue

* Document hidden prop

* Update viewport range variables

* Add hidden prop to header/footer/content

* Update PageLayout tests

* Create empty-garlics-clean.md

* Fix lint errors
  • Loading branch information
colebemis committed Jul 26, 2022
1 parent e28aadb commit 62dbc98
Show file tree
Hide file tree
Showing 9 changed files with 390 additions and 6 deletions.
10 changes: 10 additions & 0 deletions .changeset/empty-garlics-clean.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@primer/react": minor
---

Add a responsive `hidden` prop to `PageLayout.Header`, `PageLayout.Pane`, `PageLayout.Content`, and `PageLayout.Footer` that allows you to hide layout regions based on the viewport width. Example usage:

```jsx
// Hide pane on narrow viewports
<PageLayout.Pane hidden={{narrow: true}}>...</PageLayout.Pane>
```
65 changes: 63 additions & 2 deletions docs/content/PageLayout.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,25 @@ See [storybook](https://primer.style/react/storybook?path=/story/layout-pagelayo
</PageLayout>
```

### With pane hidden on narrow viewports

```jsx live
<PageLayout>
<PageLayout.Header>
<Placeholder label="Header" height={64} />
</PageLayout.Header>
<PageLayout.Content>
<Placeholder label="Content" height={240} />
</PageLayout.Content>
<PageLayout.Pane position="start" hidden={{narrow: true}}>
<Placeholder label="Pane" height={120} />
</PageLayout.Pane>
<PageLayout.Footer>
<Placeholder label="Footer" height={64} />
</PageLayout.Footer>
</PageLayout>
```

### With condensed spacing

```jsx live
Expand Down Expand Up @@ -112,8 +131,6 @@ See [storybook](https://primer.style/react/storybook?path=/story/layout-pagelayo

### PageLayout

<!-- TODO: Responsive variants -->

<PropsTable>
<PropsTableRow
name="containerWidth"
Expand Down Expand Up @@ -166,6 +183,17 @@ See [storybook](https://primer.style/react/storybook?path=/story/layout-pagelayo
| 'filled'`}
defaultValue="'inherit'"
/>
<PropsTableRow
name="hidden"
type={`| boolean
| {
narrow?: boolean
regular?: boolean
wide?: boolean
}`}
defaultValue="false"
description="Whether the header is hidden."
/>
<PropsTableSxRow />
</PropsTable>

Expand All @@ -181,6 +209,17 @@ See [storybook](https://primer.style/react/storybook?path=/story/layout-pagelayo
defaultValue="'full'"
description="The maximum width of the content region."
/>
<PropsTableRow
name="hidden"
type={`| boolean
| {
narrow?: boolean
regular?: boolean
wide?: boolean
}`}
defaultValue="false"
description="Whether the content is hidden."
/>
<PropsTableSxRow />
</PropsTable>

Expand Down Expand Up @@ -222,6 +261,17 @@ See [storybook](https://primer.style/react/storybook?path=/story/layout-pagelayo
| 'filled'`}
defaultValue="'inherit'"
/>
<PropsTableRow
name="hidden"
type={`| boolean
| {
narrow?: boolean
regular?: boolean
wide?: boolean
}`}
defaultValue="false"
description="Whether the pane is hidden."
/>
<PropsTableSxRow />
</PropsTable>

Expand All @@ -242,6 +292,17 @@ See [storybook](https://primer.style/react/storybook?path=/story/layout-pagelayo
| 'filled'`}
defaultValue="'inherit'"
/>
<PropsTableRow
name="hidden"
type={`| boolean
| {
narrow?: boolean
regular?: boolean
wide?: boolean
}`}
defaultValue="false"
description="Whether the footer is hidden."
/>
<PropsTableSxRow />
</PropsTable>

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@
"husky": "7.0.4",
"jest": "27.4.5",
"jest-axe": "5.0.1",
"jest-matchmedia-mock": "1.1.0",
"jest-styled-components": "6.3.4",
"jest-matchmedia-mock": "1.1.0",
"jscodeshift": "0.13.0",
Expand Down
54 changes: 53 additions & 1 deletion src/PageLayout/PageLayout.test.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
import {render} from '@testing-library/react'
import React from 'react'
import {act, render} from '@testing-library/react'
import MatchMediaMock from 'jest-matchmedia-mock'
import {ThemeProvider} from '..'
import {viewportRanges} from '../hooks/useResponsiveValue'
import {PageLayout} from './PageLayout'

let matchMedia: MatchMediaMock

describe('PageLayout', () => {
beforeAll(() => {
matchMedia = new MatchMediaMock()
})

afterEach(() => {
matchMedia.clear()
})

it('renders default layout', () => {
const {container} = render(
<ThemeProvider>
Expand Down Expand Up @@ -63,4 +75,44 @@ describe('PageLayout', () => {
)
expect(container).toMatchSnapshot()
})

it('can hide pane when narrow', () => {
// Set narrow viewport
act(() => {
matchMedia.useMediaQuery(viewportRanges.narrow)
})

const {getByText} = render(
<ThemeProvider>
<PageLayout>
<PageLayout.Header>Header</PageLayout.Header>
<PageLayout.Content>Content</PageLayout.Content>
<PageLayout.Pane hidden={{narrow: true}}>Pane</PageLayout.Pane>
<PageLayout.Footer>Footer</PageLayout.Footer>
</PageLayout>
</ThemeProvider>
)

expect(getByText('Pane')).not.toBeVisible()
})

it('shows all subcomponents by default', () => {
// Set regular viewport
act(() => {
matchMedia.useMediaQuery(viewportRanges.regular)
})

const {getByText} = render(
<ThemeProvider>
<PageLayout>
<PageLayout.Header>Header</PageLayout.Header>
<PageLayout.Content>Content</PageLayout.Content>
<PageLayout.Pane hidden={{narrow: true}}>Pane</PageLayout.Pane>
<PageLayout.Footer>Footer</PageLayout.Footer>
</PageLayout>
</ThemeProvider>
)

expect(getByText('Pane')).toBeVisible()
})
})
21 changes: 18 additions & 3 deletions src/PageLayout/PageLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react'
import {BetterSystemStyleObject, merge, SxProp} from '../sx'
import {Box} from '..'
import {ResponsiveValue, useResponsiveValue} from '../hooks/useResponsiveValue'
import {BetterSystemStyleObject, merge, SxProp} from '../sx'

const REGION_ORDER = {
header: 0,
Expand Down Expand Up @@ -178,18 +179,22 @@ const VerticalDivider: React.FC<DividerProps> = ({variant = 'none', variantWhenN
export type PageLayoutHeaderProps = {
divider?: 'none' | 'line'
dividerWhenNarrow?: 'inherit' | 'none' | 'line' | 'filled'
hidden?: boolean | ResponsiveValue<boolean>
} & SxProp

const Header: React.FC<PageLayoutHeaderProps> = ({
divider = 'none',
dividerWhenNarrow = 'inherit',
hidden = false,
children,
sx = {}
}) => {
const isHidden = useResponsiveValue(hidden, false)
const {rowGap} = React.useContext(PageLayoutContext)
return (
<Box
as="header"
hidden={isHidden}
sx={merge<BetterSystemStyleObject>(
{
order: REGION_ORDER.header,
Expand All @@ -216,6 +221,7 @@ Header.displayName = 'PageLayout.Header'

export type PageLayoutContentProps = {
width?: keyof typeof contentWidths
hidden?: boolean | ResponsiveValue<boolean>
} & SxProp

// TODO: Account for pane width when centering content
Expand All @@ -226,10 +232,12 @@ const contentWidths = {
xlarge: '1280px'
}

const Content: React.FC<PageLayoutContentProps> = ({width = 'full', children, sx = {}}) => {
const Content: React.FC<PageLayoutContentProps> = ({width = 'full', hidden = false, children, sx = {}}) => {
const isHidden = useResponsiveValue(hidden, false)
return (
<Box
as="main"
hidden={isHidden}
sx={merge<BetterSystemStyleObject>(
{
order: REGION_ORDER.content,
Expand Down Expand Up @@ -260,6 +268,7 @@ export type PageLayoutPaneProps = {
width?: keyof typeof paneWidths
divider?: 'none' | 'line'
dividerWhenNarrow?: 'inherit' | 'none' | 'line' | 'filled'
hidden?: boolean | ResponsiveValue<boolean>
} & SxProp

const panePositions = {
Expand All @@ -279,9 +288,11 @@ const Pane: React.FC<PageLayoutPaneProps> = ({
width = 'medium',
divider = 'none',
dividerWhenNarrow = 'inherit',
hidden = false,
children,
sx = {}
}) => {
const isHidden = useResponsiveValue(hidden, false)
const {rowGap, columnGap} = React.useContext(PageLayoutContext)
const computedPositionWhenNarrow = positionWhenNarrow === 'inherit' ? position : positionWhenNarrow
const computedDividerWhenNarrow = dividerWhenNarrow === 'inherit' ? divider : dividerWhenNarrow
Expand All @@ -293,7 +304,7 @@ const Pane: React.FC<PageLayoutPaneProps> = ({
merge<BetterSystemStyleObject>(
{
order: panePositions[computedPositionWhenNarrow],
display: 'flex',
display: isHidden ? 'none' : 'flex',
flexDirection: computedPositionWhenNarrow === 'end' ? 'column' : 'column-reverse',
width: '100%',
marginX: 0,
Expand Down Expand Up @@ -335,18 +346,22 @@ Pane.displayName = 'PageLayout.Pane'
export type PageLayoutFooterProps = {
divider?: 'none' | 'line'
dividerWhenNarrow?: 'inherit' | 'none' | 'line' | 'filled'
hidden?: boolean | ResponsiveValue<boolean>
} & SxProp

const Footer: React.FC<PageLayoutFooterProps> = ({
divider = 'none',
dividerWhenNarrow = 'inherit',
hidden = false,
children,
sx = {}
}) => {
const isHidden = useResponsiveValue(hidden, false)
const {rowGap} = React.useContext(PageLayoutContext)
return (
<Box
as="footer"
hidden={isHidden}
sx={merge<BetterSystemStyleObject>(
{
order: REGION_ORDER.footer,
Expand Down
99 changes: 99 additions & 0 deletions src/__tests__/hooks/useResponsiveValue.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import {act, render} from '@testing-library/react'
import MatchMediaMock from 'jest-matchmedia-mock'
import {ResponsiveValue, useResponsiveValue, viewportRanges} from '../../hooks/useResponsiveValue'
import React from 'react'

let matchMedia: MatchMediaMock

beforeAll(() => {
matchMedia = new MatchMediaMock()
})

afterEach(() => {
matchMedia.clear()
})

it('accepts non-responsive values', () => {
const Component = () => {
const value = useResponsiveValue('test', 'fallback')
return <div>{value}</div>
}

const {getByText} = render(<Component />)

expect(getByText('test')).toBeInTheDocument()
})

it('returns narrow value when viewport is narrow', () => {
const Component = () => {
const value = useResponsiveValue({narrow: false, regular: true} as ResponsiveValue<boolean>, true)
return <div>{JSON.stringify(value)}</div>
}

// Set narrow viewport
act(() => {
matchMedia.useMediaQuery(viewportRanges.narrow)
})

const {getByText} = render(<Component />)

expect(getByText('false')).toBeInTheDocument()
})

it('returns wide value when viewport is wide', () => {
const Component = () => {
const value = useResponsiveValue(
{narrow: 'narrowValue', regular: 'regularValue', wide: 'wideValue'} as ResponsiveValue<string>,
'fallbackValue'
)
return <div>{value}</div>
}

// Set wide viewport
act(() => {
matchMedia.useMediaQuery(viewportRanges.wide)
})

const {getByText} = render(<Component />)

expect(getByText('wideValue')).toBeInTheDocument()
})

it('returns regular value when viewport is regular', () => {
const Component = () => {
const value = useResponsiveValue(
{narrow: 'narrowValue', regular: 'regularValue', wide: 'wideValue'} as ResponsiveValue<string>,
'fallbackValue'
)
return <div>{value}</div>
}

// Set regular viewport
act(() => {
matchMedia.useMediaQuery(viewportRanges.regular)
})

const {getByText} = render(<Component />)

expect(getByText('regularValue')).toBeInTheDocument()
})

it('returns fallback when no value is defined for current viewport', () => {
const Component = () => {
const value = useResponsiveValue(
// Missing value for `regular` viewports
{narrow: 'narrowValue', wide: 'wideValue'} as ResponsiveValue<string>,
'fallbackValue'
)
return <div>{value}</div>
}

// Set regular viewport
act(() => {
matchMedia.useMediaQuery(viewportRanges.regular)
})

const {getByText} = render(<Component />)

expect(getByText('fallbackValue')).toBeInTheDocument()
})
Loading

0 comments on commit 62dbc98

Please sign in to comment.