Skip to content

Commit

Permalink
Hidden: Refactor the component to use the new `getBreakpointsDeclarat…
Browse files Browse the repository at this point in the history
…ions` util function and add missing stories and tests (#2729)

* Refactor Hidden component to use getBreakpointDeclarations for responsive rendering

* Add missing stories and test & update docs

* add changeset

* check stories for axe violation

* storybook fixes

* wrap the hidden content in a box to display flex

* breakpoints & ranges -> into consts and update global storybook parameters

* use string when there is a single value for `on`

Co-authored-by: Josh Black <joshblack@github.com>

* code review feedbacks

* utils for breakpoints

* rename and update hard-coded media queries

* PageHeader: Add visual ordering and `PageLayout` story example (#2763)

* Add visual ordering and pageLayout example

* Mock IntersectionObserver for <ThemeProvider> in story render

* remove unused components

* add TitleArea order

* add changeset

* update the snapshot

Co-authored-by: Josh Black <joshblack@github.com>
  • Loading branch information
broccolinisoup and joshblack committed Jan 19, 2023
1 parent d2d4846 commit 4dcf658
Show file tree
Hide file tree
Showing 22 changed files with 555 additions and 186 deletions.
5 changes: 5 additions & 0 deletions .changeset/happy-snails-learn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@primer/react': patch
---

PageHeader: Add visual ordering for layout enforcement
5 changes: 5 additions & 0 deletions .changeset/sharp-oranges-jog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@primer/react': patch
---

Hidden: Refactor Hidden component to use `getBreakpointsDeclarations` util function to reduce layout shifts
36 changes: 27 additions & 9 deletions .storybook/preview.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,33 @@
import {addons} from '@storybook/addons'
import {withThemeProvider, withSurroundingElements, toolbarTypes} from '../src/utils/story-helpers'
import theme from './theme'
import {PrimerBreakpoints} from '../src/utils/layout'

export const globalTypes = toolbarTypes
export const decorators = [withThemeProvider, withSurroundingElements]

let storybookViewports = {}
Object.entries(PrimerBreakpoints).forEach(([viewport, value]) => {
const {width} = value
storybookViewports[viewport] = {
name: viewport,
styles: {
width,
height: '100%',
},
}
})

addons.setConfig({
// Some stories may set up keyboard event handlers, which can be interfered
// with by these keyboard shortcuts.
enableShortcuts: false
enableShortcuts: false,
})

export const parameters = {
actions: {argTypesRegex: '^on[A-Z].*'},
html: {
root: '#html-addon-root',
removeEmptyComments: true
removeEmptyComments: true,
},
options: {
storySort: (a, b) => {
Expand All @@ -29,15 +41,15 @@ export const parameters = {
[
'*',
// Within a set of stories, set the order to the following
['*', 'Playground', /Playground$/, 'Features', 'Examples']
]
]
['*', 'Playground', /Playground$/, 'Features', 'Examples'],
],
],
],
'Behaviors',
'Hooks',
'Private components',
'Deprecated components',
'*'
'*',
]

/**
Expand Down Expand Up @@ -135,6 +147,12 @@ export const parameters = {
}

return compare(getHierarchy(a), getHierarchy(b))
}
}
},
},

viewport: {
viewports: {
...storybookViewports,
},
},
}
30 changes: 17 additions & 13 deletions docs/content/drafts/Hidden.mdx
Original file line number Diff line number Diff line change
@@ -1,26 +1,30 @@
---
title: Hidden
componentId: hidden
status: Draft
description: Use Hidden to responsively hide or show content in narrow, regular and wide viewports.
source: https://github.com/primer/react/blob/main/src/Hidden/index.tsx
status: Alpha
componentId: hidden
storybook: '/react/storybook/?path=/story/drafts-components-hidden--playground'
---

The `Hidden` component is a utility component that will help you hide or show content/components in different viewports.
**Attention:** Use Hidden only to control behaviour in a responsive manner. It does not take any `sx` props.

## Example
<Note>Use Hidden only to control behaviour in a responsive manner. It does not take any `sx` props.</Note>

## Examples

### Single viewport value provided

```jsx live
<Hidden when="narrow">
```jsx live drafts
<Hidden on="narrow">
<Placeholder height="80px" label="This is not visible in narrow viewport" />
</Hidden>
```

## Array as `when` prop
### Multiple viewport values provided

```jsx live
<Hidden when={['narrow', 'wide']}>
```jsx live drafts
<Hidden on={['narrow', 'wide']}>
<Placeholder height="80px" label="This is not visible in narrow and wide viewport" />
</Hidden>
```
Expand All @@ -29,9 +33,9 @@ The `Hidden` component is a utility component that will help you hide or show co

<PropsTable>
<PropsTableRow
name="when"
type="string or Array"
description="Can be one or more values of 'narrow', 'wide', 'regular'"
name="on"
type={`'narrow' | 'wide' | 'regular' | Array<'narrow' | 'regular' | 'wide'>`}
description="In which viewport(s) the children are going to be hidden"
/>
</PropsTable>

Expand All @@ -51,6 +55,6 @@ The `Hidden` component is a utility component that will help you hide or show co
stableApi: false,
addressedApiFeedback: false,
hasDesignGuidelines: false,
hasFigmaComponent: false
hasFigmaComponent: false,
}}
/>
2 changes: 2 additions & 0 deletions docs/src/@primer/gatsby-theme-doctocat/nav.yml
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,8 @@
url: /drafts/UnderlineNav2
- title: PageHeader
url: /drafts/PageHeader
- title: Hidden
url: /drafts/Hidden
- title: Deprecated
children:
- title: ActionList (legacy)
Expand Down
1 change: 1 addition & 0 deletions e2e/test-helpers/viewports.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// TODO: Import PrimerBreakpoints from src/utils/layout/breakpoints.ts and refactor the usage of this object
export const viewports: {[key: string]: number} = {
'primer.breakpoint.xs': 544,
'primer.breakpoint.sm': 768,
Expand Down
50 changes: 13 additions & 37 deletions src/Hidden/Hidden.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,54 +1,30 @@
import {Meta} from '@storybook/react'
import React from 'react'
import {ThemeProvider} from '..'
import {Hidden} from './Hidden'
import BaseStyles from '../BaseStyles'
import Box from '../Box'
import {ComponentMeta, ComponentStory} from '@storybook/react'

const meta: Meta = {
title: 'Layout components/Hidden',
component: Hidden,
decorators: [
(Story: React.ComponentType<React.PropsWithChildren<unknown>>): JSX.Element => (
<ThemeProvider>
<BaseStyles>
<Story />
</BaseStyles>
</ThemeProvider>
),
],
import Hidden from '.'

export default {
title: 'Drafts/Components/Hidden',
parameters: {
controls: {
expanded: true,
},
},
args: {
on: ['regular'],
},
argTypes: {
on: {
type: {
name: 'enum',
value: ['narrow', 'regular', 'wide'],
},
defaultValue: 'regular',
control: {type: 'radio'},
control: {type: 'multi-select'},
description: 'The viewport type to hide the content on.',
},
},
}
export default meta

export const isVisibleInRegularOnly = () => (
<Box>
<Hidden on={['narrow', 'wide']}> This value is only shown in regular viewport</Hidden>
</Box>
)

export const isVisibleInNarrowOnly = () => (
<Box>
<Hidden on={['regular', 'wide']}> This value is only shown in narrow viewport</Hidden>
</Box>
)
} as ComponentMeta<typeof Hidden>

export const isHiddenInNarrowOnly = () => (
<Box>
<Hidden on="narrow">This is hidden in narrow only</Hidden>
</Box>
export const Playground: ComponentStory<typeof Hidden> = args => (
<Hidden {...args}>The content is hidden on {Array(args.on).join(',')}</Hidden>
)
79 changes: 79 additions & 0 deletions src/Hidden/Hidden.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import React from 'react'
import '@testing-library/jest-dom/extend-expect'
import {render} from '@testing-library/react'
import {Hidden} from '.'
import MatchMediaMock from 'jest-matchmedia-mock'
import {behavesAsComponent, checkExports, renderStyles, checkStoriesForAxeViolations} from '../utils/testing'
import {mediaQueries} from '../utils/layout'

let matchMedia: MatchMediaMock
describe('Hidden', () => {
beforeAll(() => {
matchMedia = new MatchMediaMock()
})

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

behavesAsComponent({
Component: Hidden,
options: {skipAs: true, skipSx: true},
toRender: () => (
<Hidden on={'narrow'}>
<div>Hidden on narrow</div>
</Hidden>
),
})

checkExports('Hidden', {
default: Hidden,
Hidden,
})

it('renders `on` prop as expected', () => {
const {container} = render(
<Hidden on={'narrow'}>
<div>Hidden on narrow</div>
</Hidden>,
)
expect(container).toMatchSnapshot()
})

it('renders the styles as expected when a single viewport value is provided as a string via `on` prop', () => {
const expectedStyles = {
// `.replace` is used because renderStyles return the JSON object without a space after the column
[`${mediaQueries.regular.replace(': ', ':')}`]: {
display: 'none',
},
}
expect(
renderStyles(
<Hidden on="regular">
<div>This is hidden on regular viewports</div>
</Hidden>,
),
).toEqual(expect.objectContaining(expectedStyles))
})

it('renders the styles as expected when multiple viewport values are provided as an array via `on` prop', () => {
const expectedStyles = {
[`${mediaQueries.narrow.replace(': ', ':')}`]: {
display: 'none',
},
[`${mediaQueries.wide.replace(': ', ':')}`]: {
display: 'none',
},
}
expect(
renderStyles(
<Hidden on={['narrow', 'wide']}>
<div>This is hidden on regular and wide viewports</div>
</Hidden>,
),
).toEqual(expect.objectContaining(expectedStyles))
})
})

checkStoriesForAxeViolations('features', '../Hidden/')
checkStoriesForAxeViolations('examples', '../Hidden/')
48 changes: 31 additions & 17 deletions src/Hidden/Hidden.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,40 @@
import React from 'react'
import {useMedia} from '../hooks/useMedia'
import {viewportRanges} from '../hooks/useResponsiveValue'
import {ResponsiveValue} from '../hooks/useResponsiveValue'
import {getBreakpointDeclarations} from '../utils/getBreakpointDeclarations'
import Box from '../Box'

type Viewports = 'narrow' | 'regular' | 'wide'
type Viewport = 'narrow' | 'regular' | 'wide'

export type HiddenProps = {
on: Array<Viewports> | Viewports
on: Array<Viewport> | Viewport
children: React.ReactNode
}

export const Hidden = ({on: hiddenViewports, children}: HiddenProps) => {
const isNarrowViewport = useMedia(viewportRanges.narrow)
const isRegularViewport = useMedia(viewportRanges.regular)
const isWideViewport = useMedia(viewportRanges.wide)
let show = true
if (isNarrowViewport && (hiddenViewports === 'narrow' || hiddenViewports.indexOf('narrow') > -1)) {
show = false
} else if (isRegularViewport && (hiddenViewports === 'regular' || hiddenViewports.indexOf('regular') > -1)) {
show = false
} else if (isWideViewport && (hiddenViewports === 'wide' || hiddenViewports.indexOf('wide') > -1)) {
show = false
/* Normalize the value that is received from the prop `on`.
* For array types : ['narrow', 'wide'] -> {narrow: true, wide: true}
* For string types: 'narrow' -> {narrow: true}
*/
function normalize(hiddenViewports: Array<Viewport> | Viewport): ResponsiveValue<boolean> | null {
// For array types
if (Array.isArray(hiddenViewports)) {
const breakpoints: ResponsiveValue<boolean> = {}
// ['narrow', 'wide'] -> {narrow: true, wide: true}
for (const breakpoint of hiddenViewports) {
breakpoints[breakpoint] = true
}
return breakpoints
}
// For string types
// 'narrow' -> {narrow: true}
return {
[hiddenViewports]: true,
}
}

return show ? <>{children}</> : null
export const Hidden = ({on, children}: HiddenProps) => {
// Get breakpoint declarations for the normalized ResponsiveValue object
const styles = getBreakpointDeclarations(normalize(on), 'display', () => 'none')
// Render the children with the styles
return styles ? <Box sx={styles}>{children}</Box> : null
}

Hidden.displayName = 'Hidden'
Loading

0 comments on commit 4dcf658

Please sign in to comment.