Skip to content

Commit

Permalink
Add BreakpointsProvider to AppProvider
Browse files Browse the repository at this point in the history
  • Loading branch information
aaronccasanova committed Jun 15, 2024
1 parent 902db58 commit 360a41a
Show file tree
Hide file tree
Showing 3 changed files with 90 additions and 62 deletions.
5 changes: 5 additions & 0 deletions .changeset/silent-houses-attack.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/polaris': minor
---

Added `BreakpointsProvider` component
21 changes: 12 additions & 9 deletions polaris-react/src/components/AppProvider/AppProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
} from '@shopify/polaris-tokens';

import {EphemeralPresenceManager} from '../EphemeralPresenceManager';
import {BreakpointsProvider} from '../../utilities/breakpoints';
import {MediaQueryProvider} from '../MediaQueryProvider';
import {FocusManager} from '../FocusManager';
import {PortalsManager} from '../PortalsManager';
Expand Down Expand Up @@ -190,15 +191,17 @@ export class AppProvider extends Component<AppProviderProps, State> {
<ScrollLockManagerContext.Provider value={this.scrollLockManager}>
<StickyManagerContext.Provider value={this.stickyManager}>
<LinkContext.Provider value={link}>
<MediaQueryProvider>
<PortalsManager>
<FocusManager>
<EphemeralPresenceManager>
{children}
</EphemeralPresenceManager>
</FocusManager>
</PortalsManager>
</MediaQueryProvider>
<BreakpointsProvider>
<MediaQueryProvider>
<PortalsManager>
<FocusManager>
<EphemeralPresenceManager>
{children}
</EphemeralPresenceManager>
</FocusManager>
</PortalsManager>
</MediaQueryProvider>
</BreakpointsProvider>
</LinkContext.Provider>
</StickyManagerContext.Provider>
</ScrollLockManagerContext.Provider>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import {useState} from 'react';
import React, {createContext, useContext, useState} from 'react';
import {getMediaConditions, themeDefault} from '@shopify/polaris-tokens';
import type {
BreakpointsAlias,
BreakpointsAliasDirection,
BreakpointsTokenGroup,
} from '@shopify/polaris-tokens';

import {isServer} from './target';
import {useIsomorphicLayoutEffect} from './use-isomorphic-layout-effect';

const Breakpoints = {
Expand Down Expand Up @@ -60,23 +59,69 @@ const breakpointsQueryEntries = getBreakpointsQueryEntries(
themeDefault.breakpoints,
);

function getMatches(
defaults?: UseBreakpointsOptions['defaults'],
/**
* Used to force defaults on initial client side render so they match SSR
* values and hence avoid a Hydration error.
*/
forceDefaults?: boolean,
) {
if (!isServer && !forceDefaults) {
return Object.fromEntries(
breakpointsQueryEntries.map(([directionAlias, query]) => [
directionAlias,
window.matchMedia(query).matches,
]),
) as BreakpointsMatches;
}
const defaultBreakpoints = getMatches();

const BreakpointsContext =
createContext<BreakpointsMatches>(defaultBreakpoints);

interface BreakpointsProviderProps {
children?: React.ReactNode;
}

export function BreakpointsProvider(props: BreakpointsProviderProps) {
const [breakpoints, setBreakpoints] = useState(defaultBreakpoints);

useIsomorphicLayoutEffect(() => {
const mediaQueryLists = breakpointsQueryEntries.map(([_, query]) =>
window.matchMedia(query),
);

const handlers = mediaQueryLists.map((_, index) => {
const directionAlias = breakpointsQueryEntries[index][0];

function handler(event: {matches: boolean}) {
setBreakpoints((prevBreakpoints) => ({
...prevBreakpoints,
[directionAlias]: event.matches,
}));
}

return handler;
});

mediaQueryLists.forEach((mql, index) => {
if (mql.addListener) {
mql.addListener(handlers[index]);
} else {
mql.addEventListener('change', handlers[index]);
}
});

// Trigger the breakpoint recalculation at least once client-side to ensure
// we don't have stale default values from SSR.
mediaQueryLists.forEach((mql, index) => {
handlers[index]({matches: mql.matches});
});

return () => {
mediaQueryLists.forEach((mql, index) => {
if (mql.removeListener) {
mql.removeListener(handlers[index]);
} else {
mql.removeEventListener('change', handlers[index]);
}
});
};
}, []);

return (
<BreakpointsContext.Provider value={breakpoints}>
{props.children}
</BreakpointsContext.Provider>
);
}

function getMatches(defaults?: UseBreakpointsOptions['defaults']) {
if (typeof defaults === 'object' && defaults !== null) {
return Object.fromEntries(
breakpointsQueryEntries.map(([directionAlias]) => [
Expand Down Expand Up @@ -124,45 +169,20 @@ export interface UseBreakpointsOptions {
* breakpoints //=> All values will be `true` during SSR
*/
export function useBreakpoints(options?: UseBreakpointsOptions) {
// On SSR, and initial CSR, we force usage of the defaults to avoid a
// hydration mismatch error.
// Later, in the effect, we will call this again on the client side without
// any defaults to trigger a more accurate client side evaluation.
const [breakpoints, setBreakpoints] = useState(
getMatches(options?.defaults, true),
);
const breakpoints = useContext(BreakpointsContext);
const [isMounted, setIsMounted] = useState(false);

useIsomorphicLayoutEffect(() => {
const mediaQueryLists = breakpointsQueryEntries.map(([_, query]) =>
window.matchMedia(query),
);

const handler = () => setBreakpoints(getMatches());

mediaQueryLists.forEach((mql) => {
if (mql.addListener) {
mql.addListener(handler);
} else {
mql.addEventListener('change', handler);
}
});

// Trigger the breakpoint recalculation at least once client-side to ensure
// we don't have stale default values from SSR.
handler();

return () => {
mediaQueryLists.forEach((mql) => {
if (mql.removeListener) {
mql.removeListener(handler);
} else {
mql.removeEventListener('change', handler);
}
});
};
setIsMounted(true);
}, []);

return breakpoints;
if (!breakpoints) {
throw new Error(
'No breakpoints were provided. Your application must be wrapped in an <AppProvider> or <ThemeProvider> component. See https://polaris.shopify.com/components/app-provider for implementation instructions.',
);
}

return isMounted ? breakpoints : getMatches(options?.defaults);
}

/**
Expand Down

0 comments on commit 360a41a

Please sign in to comment.