@shopify/polaris
Version:
Shopify’s admin product component library
133 lines (124 loc) • 4.5 kB
JavaScript
import { useState } from 'react';
import { themeDefault, getMediaConditions } from '@shopify/polaris-tokens';
import { isServer } from './target.js';
import { useIsomorphicLayoutEffect } from './use-isomorphic-layout-effect.js';
const Breakpoints = {
// TODO: Update to smDown
navigationBarCollapsed: '767.95px',
// TODO: Update to lgDown
stackedContent: '1039.95px'
};
const noWindowMatches = {
media: '',
addListener: noop,
removeListener: noop,
matches: false,
onchange: noop,
addEventListener: noop,
removeEventListener: noop,
dispatchEvent: _ => true
};
function noop() {}
function navigationBarCollapsed() {
return isServer ? noWindowMatches : window.matchMedia(`(max-width: ${Breakpoints.navigationBarCollapsed})`);
}
function stackedContent() {
return isServer ? noWindowMatches : window.matchMedia(`(max-width: ${Breakpoints.stackedContent})`);
}
/**
* Directional alias for each Polaris `breakpoints` token.
*
* @example 'smUp' | 'smDown' | 'smOnly' | 'mdUp' | etc.
*/
/**
* Match results for each directional Polaris `breakpoints` alias.
*/
const hookCallbacks = new Set();
const breakpointsQueryEntries = getBreakpointsQueryEntries(themeDefault.breakpoints);
if (!isServer) {
breakpointsQueryEntries.forEach(([breakpointAlias, query]) => {
const eventListener = event => {
for (const hookCallback of hookCallbacks) {
hookCallback(breakpointAlias, event.matches);
}
};
const mql = window.matchMedia(query);
if (mql.addListener) {
mql.addListener(eventListener);
} else {
mql.addEventListener('change', eventListener);
}
});
}
function getDefaultMatches(defaults) {
return Object.fromEntries(breakpointsQueryEntries.map(([directionAlias]) => [directionAlias, typeof defaults === 'boolean' ? defaults : defaults?.[directionAlias] ?? false]));
}
function getLiveMatches() {
return Object.fromEntries(breakpointsQueryEntries.map(([directionAlias, query]) => [directionAlias, window.matchMedia(query).matches]));
}
/**
* Retrieves media query matches for each directional Polaris `breakpoints` alias.
*
* @example
* const {smUp} = useBreakpoints();
* return smUp && 'Hello world';
*
* @example
* const {mdUp} = useBreakpoints({defaults: {mdUp: true}});
* mdUp //=> `true` during SSR
*
* @example
* const breakpoints = useBreakpoints({defaults: true});
* breakpoints //=> All values will be `true` during SSR
*/
function useBreakpoints(options) {
// 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(getDefaultMatches(options?.defaults));
useIsomorphicLayoutEffect(() => {
// Now that we're client side, get the real values
setBreakpoints(getLiveMatches());
// Register a callback to set the breakpoints object whenever there's a
// change in the future
const callback = (breakpointAlias, matches) => {
setBreakpoints(prevBreakpoints => ({
...prevBreakpoints,
[breakpointAlias]: matches
}));
};
hookCallbacks.add(callback);
return () => {
hookCallbacks.delete(callback);
};
}, []);
return breakpoints;
}
/**
* Converts `breakpoints` tokens into directional media query entries.
*
* @example
* const breakpointsQueryEntries = getBreakpointsQueryEntries(breakpoints);
* breakpointsQueryEntries === [
* ['xsUp', '(min-width: ...)'],
* ['xsDown', '(max-width: ...)'],
* ['xsOnly', '(min-width: ...) and (max-width: ...)'],
* ['smUp', '(min-width: ...) and (max-width: ...)'],
* ['mdUp', '(min-width: ...) and (max-width: ...)'],
* // etc.
* ]
*/
function getBreakpointsQueryEntries(breakpoints) {
const mediaConditionEntries = Object.entries(getMediaConditions(breakpoints));
return mediaConditionEntries.map(([breakpointsToken, mediaConditions]) => Object.entries(mediaConditions).map(([direction, mediaCondition]) => {
const breakpointsAlias = breakpointsToken.split('-')[1];
// e.g. smUp, smDown, smOnly, etc.
const directionAlias = `${breakpointsAlias}${capitalize(direction)}`;
return [directionAlias, mediaCondition];
})).flat();
}
function capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
export { getBreakpointsQueryEntries, navigationBarCollapsed, stackedContent, useBreakpoints };