Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PageLayout: Implement responsive hidden prop #2174

Merged
merged 11 commits into from
Jul 26, 2022
8 changes: 6 additions & 2 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 @@ -260,6 +261,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 +281,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 +297,7 @@ const Pane: React.FC<PageLayoutPaneProps> = ({
merge<BetterSystemStyleObject>(
{
order: panePositions[computedPositionWhenNarrow],
display: 'flex',
display: isHidden ? 'none' : 'flex',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just confirming that we want hidden to prevent screen readers from accessing the content too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe so 👍 cc @hectahertz @alliethu @ichelsea to confirm

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's part of the page layout that shouldn't be able to be accessed by anyone, then we'd want to hide if from screen reader users too.

flexDirection: computedPositionWhenNarrow === 'end' ? 'column' : 'column-reverse',
width: '100%',
marginX: 0,
Expand Down
49 changes: 49 additions & 0 deletions src/hooks/useMedia.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copied from https://github.com/streamich/react-use/blob/master/src/useMedia.ts

import React from 'react'

const getInitialState = (query: string, defaultState?: boolean) => {
// Prevent a React hydration mismatch when a default value is provided by not defaulting to window.matchMedia(query).matches.
if (defaultState !== undefined) {
return defaultState
}

if (typeof window !== 'undefined') {
return window.matchMedia(query).matches
}

// A default value has not been provided, and you are rendering on the server, warn of a possible hydration mismatch when defaulting to false.
if (process.env.NODE_ENV !== 'production') {
// eslint-disable-next-line no-console
console.warn(
'`useMedia` When server side rendering, defaultState should be defined to prevent a hydration mismatches.'
)
}

return false
}

export function useMedia(query: string, defaultState?: boolean) {
const [state, setState] = React.useState(getInitialState(query, defaultState))

React.useEffect(() => {
let mounted = true
const mql = window.matchMedia(query)
const onChange = () => {
if (!mounted) {
return
}
setState(!!mql.matches)
}

mql.addListener(onChange)
setState(mql.matches)

return () => {
mounted = false
mql.removeListener(onChange)
}
}, [query])

return state
}
60 changes: 60 additions & 0 deletions src/hooks/useResponsiveValue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import {useMedia} from './useMedia'

// TODO: Write tests for this hook

export type ResponsiveValue<TRegular, TNarrow = TRegular, TWide = TRegular> = {
narrow?: TNarrow // Applies when viewport is < 768px
regular?: TRegular // Applies when viewports is >= 768px
wide?: TWide // Applies when viewports is >= 1400px
}

/**
* Flattens all possible value types into single union type
* For example, if `T` is `'none' | 'line' | Responsive<'none' | 'line' | 'filled'>`,
* `FlattenResponsiveValue<T>` will be `'none' | 'line' | 'filled'`
*/
export type FlattenResponsiveValue<T> =
| (T extends ResponsiveValue<infer TRegular, infer TNarrow, infer TWide> ? TRegular | TNarrow | TWide : never)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
| Exclude<T, ResponsiveValue<any>>

/**
* Checks if the value is a responsive value.
* In other words, is it an object with viewport range keys?
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function isResponsiveValue(value: any): value is ResponsiveValue<any> {
return typeof value === 'object' && Object.keys(value).some(key => ['narrow', 'regular', 'wide'].includes(key))
}

/**
* Resolves responsive values based on the current viewport width.
* For example, if the current viewport width is less than 768px, the value of `{regular: 'foo', narrow: 'bar'}` this hook will return `'bar'`.
*/
export function useResponsiveValue<T, F>(value: T, fallback: F): FlattenResponsiveValue<T> | F {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️ this hook

// Check viewport size
// TODO: Get these breakpoint values from primer/primitives
const isNarrowViewport = useMedia('(max-width: 767px)') // < 768px
const isRegularViewport = useMedia('(min-width: 768px)') // >= 768px
colebemis marked this conversation as resolved.
Show resolved Hide resolved
const isWideViewport = useMedia('(min-width: 1400px)') // >= 1400px

if (isResponsiveValue(value)) {
// If we've reached this line, we know that value is a responsive value
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const responsiveValue = value as Extract<T, ResponsiveValue<any>>

if (isNarrowViewport && 'narrow' in responsiveValue) {
return responsiveValue.narrow
} else if (isWideViewport && 'wide' in responsiveValue) {
return responsiveValue.wide
} else if (isRegularViewport && 'regular' in responsiveValue) {
return responsiveValue.regular
} else {
return fallback
}
} else {
// If we've reached this line, we know that value is not a responsive value
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return value as Exclude<T, ResponsiveValue<any>>
}
}