@solid-primitives/media
Version:
Primitives for media query and device features
134 lines (133 loc) • 5.18 kB
JavaScript
import { isServer } from "solid-js/web";
import { makeEventListener } from "@solid-primitives/event-listener";
import { entries, noop, createHydratableSignal } from "@solid-primitives/utils";
import { createHydratableStaticStore } from "@solid-primitives/static-store";
import { createHydratableSingletonRoot } from "@solid-primitives/rootless";
/**
* attaches a MediaQuery listener to window, listeneing to changes to provided query
* @param query Media query to listen for
* @param callback function called every time the media match changes
* @returns function removing the listener
* @example
* const clear = makeMediaQueryListener("(max-width: 767px)", e => {
* console.log(e.matches)
* });
* // remove listeners (will happen also on cleanup)
* clear()
*/
export function makeMediaQueryListener(query, callback) {
if (isServer) {
return noop;
}
const mql = typeof query === "string" ? window.matchMedia(query) : query;
return makeEventListener(mql, "change", callback);
}
/**
* Creates a very simple and straightforward media query monitor.
*
* @param query Media query to listen for
* @param fallbackState Server fallback state *(Defaults to `false`)*
* @returns Boolean value if media query is met or not
*
* @example
* ```ts
* const isSmall = createMediaQuery("(max-width: 767px)");
* console.log(isSmall());
* ```
*/
export function createMediaQuery(query, serverFallback = false) {
if (isServer) {
return () => serverFallback;
}
const mql = window.matchMedia(query);
const [state, setState] = createHydratableSignal(serverFallback, () => mql.matches);
const update = () => setState(mql.matches);
makeEventListener(mql, "change", update);
return state;
}
/**
* Provides a signal indicating if the user has requested dark color theme. The setting is being watched with a [Media Query](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme).
*
* @param serverFallback value that should be returned on the server — defaults to `false`
*
* @returns a boolean signal
* @example
* const prefersDark = usePrefersDark();
* createEffect(() => {
* prefersDark() // => boolean
* });
*/
export function createPrefersDark(serverFallback) {
return createMediaQuery("(prefers-color-scheme: dark)", serverFallback);
}
/**
* Provides a signal indicating if the user has requested dark color theme. The setting is being watched with a [Media Query](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme).
*
* This is a [singleton root primitive](https://github.com/solidjs-community/solid-primitives/tree/main/packages/rootless#createSingletonRoot) except if during hydration.
*
* @returns a boolean signal
* @example
* const prefersDark = usePrefersDark();
* createEffect(() => {
* prefersDark() // => boolean
* });
*/
export const usePrefersDark = /*#__PURE__*/ createHydratableSingletonRoot(createPrefersDark.bind(void 0, false));
const getEmptyMatchesFromBreakpoints = (breakpoints) => entries(breakpoints).reduce((matches, [key]) => {
matches[key] = false;
return matches;
}, {});
/**
* Creates a multi-breakpoint monitor to make responsive components easily.
*
* @param breakpoints Map of breakpoint names and their widths
* @param options Options to customize watch, fallback, responsive mode.
* @returns map of currently matching breakpoints.
*
* @example
* ```ts
* const breakpoints = {
sm: "640px",
lg: "1024px",
xl: "1280px",
};
* const matches = createBreakpoints(breakpoints);
* console.log(matches.lg);
* ```
*/
export function createBreakpoints(breakpoints, options = {}) {
const fallback = Object.defineProperty(options.fallbackState ?? getEmptyMatchesFromBreakpoints(breakpoints), "key", { enumerable: false, get: () => Object.keys(breakpoints).pop() });
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (isServer || !window.matchMedia)
return fallback;
const { mediaFeature = "min-width", watchChange = true } = options;
const [matches, setMatches] = createHydratableStaticStore(fallback, () => {
const matches = {};
entries(breakpoints).forEach(([token, width]) => {
const mql = window.matchMedia(`(${mediaFeature}: ${width})`);
matches[token] = mql.matches;
if (watchChange)
makeEventListener(mql, "change", (e) => setMatches(token, e.matches));
});
return matches;
});
return Object.defineProperty(matches, "key", {
enumerable: false,
get: () => Object.keys(matches).findLast(token => matches[token]),
});
}
/**
* Creates a sorted copy of the Breakpoints Object
* If you want to use the result of `createBreakpoints()` with string coercion:
* ```ts
* createBreakpoints(sortBreakpoints({ tablet: "980px", mobile: "640px" }))
* ```
*/
export function sortBreakpoints(breakpoints) {
const sorted = entries(breakpoints);
sorted.sort((x, y) => parseInt(x[1], 10) - parseInt(y[1], 10));
return sorted.reduce((obj, [key, value]) => {
obj[key] = value;
return obj;
}, {});
}