From 9911710c944105df16e62fee922badc19fd8c6ea Mon Sep 17 00:00:00 2001 From: mattcompiles Date: Fri, 31 Jul 2020 09:41:19 +1000 Subject: [PATCH] Add useBreakpoint (#700) Co-authored-by: Michael Taranto --- .changeset/tender-clocks-clean.md | 9 +++ .../__snapshots__/contract.test.ts.snap | 18 ++++- .../BraidProvider/BraidProvider.tsx | 5 +- .../BraidTestProvider.docs.tsx | 15 ++++ .../BraidTestProvider/BraidTestProvider.tsx | 18 +++-- lib/components/index.ts | 1 + .../useBreakpoint/BreakpointProvider.tsx | 68 +++++++++++++++++++ .../useBreakpoint/useBreakpoint.docs.tsx | 46 +++++++++++++ lib/components/useBreakpoint/useBreakpoint.ts | 5 ++ lib/themes/makeBraidTheme.ts | 1 + 10 files changed, 179 insertions(+), 7 deletions(-) create mode 100644 .changeset/tender-clocks-clean.md create mode 100644 lib/components/useBreakpoint/BreakpointProvider.tsx create mode 100644 lib/components/useBreakpoint/useBreakpoint.docs.tsx create mode 100644 lib/components/useBreakpoint/useBreakpoint.ts diff --git a/.changeset/tender-clocks-clean.md b/.changeset/tender-clocks-clean.md new file mode 100644 index 0000000000..5c6affdf6a --- /dev/null +++ b/.changeset/tender-clocks-clean.md @@ -0,0 +1,9 @@ +--- +'braid-design-system': minor +--- + +Add useBreakpoint + +`useBreakpoint` will return the breakpoint the browser viewport currently falls within (mobile, tablet or desktop). As this can only be calculated in the browser, the value may also be null. Window resizing is supported. + +**Note:** Avoid use of this hook where possible. Responsive properties and media queries are a better option in most cases. diff --git a/generate-component-docs/__snapshots__/contract.test.ts.snap b/generate-component-docs/__snapshots__/contract.test.ts.snap index 7e6b781a28..13ac5d5c02 100644 --- a/generate-component-docs/__snapshots__/contract.test.ts.snap +++ b/generate-component-docs/__snapshots__/contract.test.ts.snap @@ -1561,7 +1561,7 @@ Object { | ComponentClass | FunctionComponent styleBody?: boolean - theme: { name: string; displayName: string; background: string; webFonts: { linkTag: string; }[]; space: { grid: number; space: { gutter: number; xxsmall: number; xsmall: number; small: number; medium: number; large: number; xlarge: number; xxlarge: number; }; }; color: { ...; }; backgroundLightness: { ...; }; treatTheme: ... + theme: { name: string; displayName: string; background: string; webFonts: { linkTag: string; }[]; space: { grid: number; space: { gutter: number; xxsmall: number; xsmall: number; small: number; medium: number; large: number; xlarge: number; xxlarge: number; }; }; breakpoint: Record<...>; color: { ...; }; backgroundLightnes... }, } `; @@ -1570,6 +1570,10 @@ exports[`BraidTestProvider 1`] = ` Object { exportType: component, props: { + breakpoint?: + | "desktop" + | "mobile" + | "tablet" children: ReactNode linkComponent?: | ComponentClass @@ -6205,6 +6209,18 @@ Object { } `; +exports[`useBreakpoint 1`] = ` +Object { + exportType: hook, + params: Array [], + returnType: + | "desktop" + | "mobile" + | "tablet" + | null, +} +`; + exports[`useColor 1`] = ` Object { exportType: hook, diff --git a/lib/components/BraidProvider/BraidProvider.tsx b/lib/components/BraidProvider/BraidProvider.tsx index 3504104f61..a7507a48e5 100644 --- a/lib/components/BraidProvider/BraidProvider.tsx +++ b/lib/components/BraidProvider/BraidProvider.tsx @@ -9,6 +9,7 @@ import { TreatProvider } from 'sku/react-treat'; import { ensureResetImported } from '../../reset/resetTracker'; import { HideFocusRingsRoot } from '../private/hideFocusRings/hideFocusRings'; import { BraidTestProviderContext } from '../BraidTestProvider/BraidTestProviderContext'; +import { BreakpointProvider } from '../useBreakpoint/BreakpointProvider'; import { BraidTheme } from '../../themes/BraidTheme.d'; if (process.env.NODE_ENV === 'development') { @@ -64,7 +65,9 @@ export const BraidProvider = ({ {alreadyInBraidProvider || inTestProvider ? ( children ) : ( - {children} + + {children} + )} diff --git a/lib/components/BraidTestProvider/BraidTestProvider.docs.tsx b/lib/components/BraidTestProvider/BraidTestProvider.docs.tsx index f32711223d..de7ca39e88 100644 --- a/lib/components/BraidTestProvider/BraidTestProvider.docs.tsx +++ b/lib/components/BraidTestProvider/BraidTestProvider.docs.tsx @@ -46,6 +46,21 @@ const docs: ComponentDocs = { }); `, }, + { + label: 'Specifying a breakpoint', + code: ` + import { BraidTestProvider } from 'braid-design-system'; + import { render } from 'react-testing-library'; + + it('should do something', () => { + render( + + ... + + ); + }); + `, + }, ], }; diff --git a/lib/components/BraidTestProvider/BraidTestProvider.tsx b/lib/components/BraidTestProvider/BraidTestProvider.tsx index 2344c7099f..6cbff391c7 100644 --- a/lib/components/BraidTestProvider/BraidTestProvider.tsx +++ b/lib/components/BraidTestProvider/BraidTestProvider.tsx @@ -5,20 +5,28 @@ import { BraidProviderProps, } from '../BraidProvider/BraidProvider'; import { BraidTestProviderContext } from './BraidTestProviderContext'; +import { + breakpointContext, + Breakpoint, +} from '../useBreakpoint/BreakpointProvider'; interface BraidTestProviderProps extends Omit { themeName?: keyof typeof themes; + breakpoint?: Breakpoint | null; } export const BraidTestProvider = ({ themeName = 'wireframe', + breakpoint = null, ...restProps }: BraidTestProviderProps) => ( - + + + ); diff --git a/lib/components/index.ts b/lib/components/index.ts index c22ba0c42a..d9c8ce94da 100644 --- a/lib/components/index.ts +++ b/lib/components/index.ts @@ -8,6 +8,7 @@ export { ThemeNameConsumer } from './ThemeNameConsumer/ThemeNameConsumer'; export { useThemeName } from './useThemeName/useThemeName'; export { useSpace } from './useSpace/useSpace'; export { useColor } from './useColor/useColor'; +export { useBreakpoint } from './useBreakpoint/useBreakpoint'; export { Accordion } from './Accordion/Accordion'; export { AccordionItem } from './Accordion/AccordionItem'; export { Actions } from './Actions/Actions'; diff --git a/lib/components/useBreakpoint/BreakpointProvider.tsx b/lib/components/useBreakpoint/BreakpointProvider.tsx new file mode 100644 index 0000000000..4b53bd670c --- /dev/null +++ b/lib/components/useBreakpoint/BreakpointProvider.tsx @@ -0,0 +1,68 @@ +// Adapted version of https://github.com/streamich/react-use/blob/master/src/useMedia.ts +import React, { useEffect, useState, createContext, ReactNode } from 'react'; + +import { useBraidTheme } from '../BraidProvider/BraidProvider'; + +export type Breakpoint = 'mobile' | 'tablet' | 'desktop'; + +const minWidthQuery = (breakpoint: number) => + window.matchMedia(`(min-width: ${breakpoint}px)`); + +const getCurrentBreakpoint = ( + tabletQuery: MediaQueryList, + desktopQuery: MediaQueryList, +) => { + if (desktopQuery.matches) { + return 'desktop'; + } + if (tabletQuery.matches) { + return 'tablet'; + } + return 'mobile'; +}; + +export const breakpointContext = createContext(null); + +interface BreakpointProviderProps { + children: ReactNode; +} +export function BreakpointProvider({ children }: BreakpointProviderProps) { + const { tablet, desktop } = useBraidTheme().breakpoint; + + const [state, setState] = useState(null); + + useEffect(() => { + let mounted = true; + const tabletQuery = minWidthQuery(tablet); + const desktopQuery = minWidthQuery(desktop); + + const onChange = () => { + if (!mounted) { + return; + } + + const newBreakPoint = getCurrentBreakpoint(tabletQuery, desktopQuery); + + if (newBreakPoint !== state) { + setState(newBreakPoint); + } + }; + + tabletQuery.addListener(onChange); + desktopQuery.addListener(onChange); + + onChange(); + + return () => { + mounted = false; + tabletQuery.removeListener(onChange); + desktopQuery.removeListener(onChange); + }; + }, [tablet, desktop, state]); + + return ( + + {children} + + ); +} diff --git a/lib/components/useBreakpoint/useBreakpoint.docs.tsx b/lib/components/useBreakpoint/useBreakpoint.docs.tsx new file mode 100644 index 0000000000..2b3e0a6802 --- /dev/null +++ b/lib/components/useBreakpoint/useBreakpoint.docs.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { ComponentDocs } from '../../../site/src/types'; +import { Alert } from '../Alert/Alert'; +import { Strong } from '../Strong/Strong'; +import { Stack } from '../Stack/Stack'; +import { Text } from '../Text/Text'; + +import { useBreakpoint } from './useBreakpoint'; + +const docs: ComponentDocs = { + category: 'Logic', + description: ( + + + This hook will return the breakpoint the browser viewport currently + falls within (mobile, tablet or{' '} + desktop). As this can only be calculated in the + browser, the value may also be null. Window resizing is supported. + + + + Avoid use of this hook where possible. Responsive properties and media + queries are a better option in most cases. + + + + ), + screenshotWidths: [], + examples: [ + { + playroom: false, + Example: () => { + const breakpoint = useBreakpoint(); + + return Current breakpoint: {breakpoint}; + }, + code: ` + const breakpoint = useBreakpoint(); + + return Current breakpoint: {breakpoint}; + `, + }, + ], +}; + +export default docs; diff --git a/lib/components/useBreakpoint/useBreakpoint.ts b/lib/components/useBreakpoint/useBreakpoint.ts new file mode 100644 index 0000000000..16c68f435b --- /dev/null +++ b/lib/components/useBreakpoint/useBreakpoint.ts @@ -0,0 +1,5 @@ +import { useContext } from 'react'; + +import { breakpointContext } from './BreakpointProvider'; + +export const useBreakpoint = () => useContext(breakpointContext); diff --git a/lib/themes/makeBraidTheme.ts b/lib/themes/makeBraidTheme.ts index 7bf8813d14..4cb558d127 100644 --- a/lib/themes/makeBraidTheme.ts +++ b/lib/themes/makeBraidTheme.ts @@ -197,6 +197,7 @@ const makeRuntimeTokens = (tokens: TreatTheme) => ({ grid: tokens.grid, space: tokens.space, }, + breakpoint: tokens.breakpoint, color: tokens.color, backgroundLightness: mapValues( tokens.color.background,