UNPKG

@hakuna-matata-ui/color-mode

Version:

React component and hooks for handling light and dark mode.

420 lines (351 loc) 11.4 kB
import { useEnvironment } from '@hakuna-matata-ui/react-env'; import { noop, isBrowser, __DEV__ } from '@hakuna-matata-ui/utils'; import * as React from 'react'; function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } var classNames = { light: "chakra-ui-light", dark: "chakra-ui-dark" }; /** * SSR: Graceful fallback for the `body` element */ var mockBody = { classList: { add: noop, remove: noop } }; var getBody = function getBody(document) { return isBrowser ? document.body : mockBody; }; /** * Function to add/remove class from `body` based on color mode */ function syncBodyClassName(isDark, document) { var body = getBody(document); body.classList.add(isDark ? classNames.dark : classNames.light); body.classList.remove(isDark ? classNames.light : classNames.dark); } /** * Check if JS media query matches the query string passed */ function getMediaQuery(query) { var mediaQueryList = window.matchMedia == null ? void 0 : window.matchMedia(query); if (!mediaQueryList) { return undefined; } return !!mediaQueryList.media === mediaQueryList.matches; } var queries = { light: "(prefers-color-scheme: light)", dark: "(prefers-color-scheme: dark)" }; function getColorScheme(fallback) { var _getMediaQuery; var isDark = (_getMediaQuery = getMediaQuery(queries.dark)) != null ? _getMediaQuery : fallback === "dark"; return isDark ? "dark" : "light"; } /** * Adds system os color mode listener, and run the callback * once preference changes */ function addListener(fn) { if (!("matchMedia" in window)) { return noop; } var mediaQueryList = window.matchMedia(queries.dark); var listener = function listener() { fn(mediaQueryList.matches ? "dark" : "light", true); }; mediaQueryList.addEventListener("change", listener); return function () { mediaQueryList.removeEventListener("change", listener); }; } var root = { get: function get() { return document.documentElement.style.getPropertyValue("--chakra-ui-color-mode"); }, set: function set(mode) { if (isBrowser) { document.documentElement.style.setProperty("--chakra-ui-color-mode", mode); } } }; var hasSupport = function hasSupport() { return typeof Storage !== "undefined"; }; var storageKey = "chakra-ui-color-mode"; /** * Simple object to handle read-write to localStorage */ var localStorageManager = { get: function get(init) { if (!hasSupport()) return init; try { var _value = localStorage.getItem(storageKey); return _value != null ? _value : init; } catch (error) { if (__DEV__) { console.log(error); } return init; } }, set: function set(value) { if (!hasSupport()) return; try { localStorage.setItem(storageKey, value); } catch (error) { if (__DEV__) { console.log(error); } } }, type: "localStorage" }; /** * Simple object to handle read-write to cookies */ var cookieStorageManager = function cookieStorageManager(cookies) { if (cookies === void 0) { cookies = ""; } return { get: function get(init) { var match = cookies.match(new RegExp("(^| )" + storageKey + "=([^;]+)")); if (match) { return match[2]; } return init; }, set: function set(value) { document.cookie = storageKey + "=" + value + "; max-age=31536000; path=/"; }, type: "cookie" }; }; var ColorModeContext = /*#__PURE__*/React.createContext({}); if (__DEV__) { ColorModeContext.displayName = "ColorModeContext"; } /** * React hook that reads from `ColorModeProvider` context * Returns the color mode and function to toggle it */ var useColorMode = function useColorMode() { var context = React.useContext(ColorModeContext); if (context === undefined) { throw new Error("useColorMode must be used within a ColorModeProvider"); } return context; }; /** * Provides context for the color mode based on config in `theme` * Returns the color mode and function to toggle the color mode */ function ColorModeProvider(props) { var value = props.value, children = props.children, _props$options = props.options, useSystemColorMode = _props$options.useSystemColorMode, initialColorMode = _props$options.initialColorMode, _props$colorModeManag = props.colorModeManager, colorModeManager = _props$colorModeManag === void 0 ? localStorageManager : _props$colorModeManag; var defaultColorMode = initialColorMode === "dark" ? "dark" : "light"; /** * Only attempt to retrieve if we're on the server. Else this will result * in a hydration mismatch warning and partially invalid visuals. * * Else fallback safely to `theme.config.initialColormode` (default light) */ var _React$useState = React.useState(colorModeManager.type === "cookie" ? colorModeManager.get(defaultColorMode) : defaultColorMode), colorMode = _React$useState[0], rawSetColorMode = _React$useState[1]; var _useEnvironment = useEnvironment(), document = _useEnvironment.document; React.useEffect(function () { /** * Since we cannot initially retrieve localStorage to due above mentioned * reasons, do so after hydration. * * Priority: * - if `useSystemColorMode` is true system-color will be used as default - initial * colormode is the fallback if system color mode isn't resolved * * - if `--chakra-ui-color-mode` is defined through e.g. `ColorModeScript` this * will be used * * - if `colorModeManager` = `localStorage` and a value is defined for * `chakra-ui-color-mode` this will be used * * - if `initialColorMode` = `system` system-color will be used as default - * initial colormode is the fallback if system color mode isn't resolved * * - if `initialColorMode` = `'light'|'dark'` the corresponding value will be used */ if (isBrowser && colorModeManager.type === "localStorage") { var systemColorWithFallback = getColorScheme(defaultColorMode); if (useSystemColorMode) { return rawSetColorMode(systemColorWithFallback); } var rootGet = root.get(); var colorManagerGet = colorModeManager.get(); if (rootGet) { return rawSetColorMode(rootGet); } if (colorManagerGet) { return rawSetColorMode(colorManagerGet); } if (initialColorMode === "system") { return rawSetColorMode(systemColorWithFallback); } return rawSetColorMode(defaultColorMode); } }, [colorModeManager, useSystemColorMode, defaultColorMode, initialColorMode]); React.useEffect(function () { var isDark = colorMode === "dark"; syncBodyClassName(isDark, document); root.set(isDark ? "dark" : "light"); }, [colorMode, document]); var setColorMode = React.useCallback(function (value, isListenerEvent) { if (isListenerEvent === void 0) { isListenerEvent = false; } if (!isListenerEvent) { colorModeManager.set(value); } else if (colorModeManager.get() && !useSystemColorMode) return; rawSetColorMode(value); }, [colorModeManager, useSystemColorMode]); var toggleColorMode = React.useCallback(function () { setColorMode(colorMode === "light" ? "dark" : "light"); }, [colorMode, setColorMode]); React.useEffect(function () { var shouldUseSystemListener = useSystemColorMode || initialColorMode === "system"; var removeListener; if (shouldUseSystemListener) { removeListener = addListener(setColorMode); } return function () { if (removeListener && shouldUseSystemListener) { removeListener(); } }; }, [setColorMode, useSystemColorMode, initialColorMode]); // presence of `value` indicates a controlled context var context = React.useMemo(function () { return { colorMode: value != null ? value : colorMode, toggleColorMode: value ? noop : toggleColorMode, setColorMode: value ? noop : setColorMode }; }, [colorMode, setColorMode, toggleColorMode, value]); return /*#__PURE__*/React.createElement(ColorModeContext.Provider, { value: context }, children); } if (__DEV__) { ColorModeProvider.displayName = "ColorModeProvider"; } /** * Locks the color mode to `dark`, without any way to change it. */ var DarkMode = function DarkMode(props) { var context = React.useMemo(function () { return { colorMode: "dark", toggleColorMode: noop, setColorMode: noop }; }, []); return /*#__PURE__*/React.createElement(ColorModeContext.Provider, _extends({ value: context }, props)); }; if (__DEV__) { DarkMode.displayName = "DarkMode"; } /** * Locks the color mode to `light` without any way to change it. */ var LightMode = function LightMode(props) { var context = React.useMemo(function () { return { colorMode: "light", toggleColorMode: noop, setColorMode: noop }; }, []); return /*#__PURE__*/React.createElement(ColorModeContext.Provider, _extends({ value: context }, props)); }; if (__DEV__) { LightMode.displayName = "LightMode"; } /** * Change value based on color mode. * * @param light the light mode value * @param dark the dark mode value * * @example * * ```js * const Icon = useColorModeValue(MoonIcon, SunIcon) * ``` */ function useColorModeValue(light, dark) { var _useColorMode = useColorMode(), colorMode = _useColorMode.colorMode; return colorMode === "dark" ? dark : light; } function setScript(initialValue) { var mql = window.matchMedia("(prefers-color-scheme: dark)"); var systemPreference = mql.matches ? "dark" : "light"; var persistedPreference; try { persistedPreference = localStorage.getItem("chakra-ui-color-mode"); } catch (error) { console.log("Chakra UI: localStorage is not available. Color mode persistence might not work as expected"); } var isInStorage = typeof persistedPreference === "string"; var colorMode; if (isInStorage) { colorMode = persistedPreference; } else { colorMode = initialValue === "system" ? systemPreference : initialValue; } if (colorMode) { var root = document.documentElement; root.style.setProperty("--chakra-ui-color-mode", colorMode); } } /** * Script to add to the root of your application when using localStorage, * to help prevent flash of color mode that can happen during page load. */ var ColorModeScript = function ColorModeScript(props) { var _props$initialColorMo = props.initialColorMode, initialColorMode = _props$initialColorMo === void 0 ? "light" : _props$initialColorMo; var html = "(" + String(setScript) + ")('" + initialColorMode + "')"; return /*#__PURE__*/React.createElement("script", { nonce: props.nonce, dangerouslySetInnerHTML: { __html: html } }); }; export { ColorModeContext, ColorModeProvider, ColorModeScript, DarkMode, LightMode, cookieStorageManager, localStorageManager, setScript, storageKey, useColorMode, useColorModeValue };