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

Add BreakpointsProvider to AppProvider #12268

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading