UNPKG

@dr.pogodin/react-themes

Version:
302 lines (277 loc) 11.6 kB
import { createContext, use, useMemo } from 'react'; // ----------------------------------------------------------------------------- // TypeScript interfaces & types, constants. // Note: Support of custom specifity-manipulation classes in TypeScript is too // cumbersome, thus although it remains a functional feature for pure JavaScript, // the TypeScript assumes these classes are always "ad", "hoc", and "context". // NOTE: Keep it as interface, to allow, in theory, consumer to redefine these // default keys. // eslint-disable-next-line @typescript-eslint/consistent-type-definitions // NOTE: KeyT should be a union of string literals - valid theme keys. // TODO: Revise, should we change it to type? // eslint-disable-next-line @typescript-eslint/consistent-type-definitions, @typescript-eslint/consistent-indexed-object-style // TODO: Revise, should we change it to type? // eslint-disable-next-line @typescript-eslint/consistent-type-definitions // TODO: Revise, should we change it to type? // eslint-disable-next-line @typescript-eslint/consistent-type-definitions import { jsx as _jsx } from "react/jsx-runtime"; /** Supported theme composition modes. */ export let COMPOSE = /*#__PURE__*/function (COMPOSE) { COMPOSE["DEEP"] = "DEEP"; COMPOSE["SOFT"] = "SOFT"; COMPOSE["SWAP"] = "SWAP"; return COMPOSE; }({}); /** Supported theme priorities. */ export let PRIORITY = /*#__PURE__*/function (PRIORITY) { PRIORITY["ADHOC_CONTEXT_DEFAULT"] = "ADHOC_CONTEXT_DEFAULT"; PRIORITY["ADHOC_DEFAULT_CONTEXT"] = "ADHOC_DEFAULT_CONTEXT"; return PRIORITY; }({}); const INVALID_COMPOSE = 'Invalid composition mode'; const Context = /*#__PURE__*/createContext(undefined); // ----------------------------------------------------------------------------- // Here comes the logic. /** * Theme provider defines style contexts. It accepts a single property * `themes` (`theme` in compatibility modes). * * In case of nested context, the context theme from the closest context takes * the effect on a component. If the context theme for a component is not set in * the closest context, but it is set in an outer context, the theme from outer * context will be applied. * * @param props.children React content to render in-place of * <ThemeProvider> component. * * @param props.themes The mapping of between themeable component names * (the first parameter passed into themed() function for such components * registration), and context themes to apply to them within the context. * * @param props.theme Fallback mapping for backward compatibility * with `react-css-themr` and `react-css-super-themr` libraries. */ export const ThemeProvider = ({ children, themes }) => { const contextThemes = use(Context); // useMemo() ensures we don't generate a new "value" on each render when both // "contextThemes" and "themes" are defined. const value = useMemo(() => { var _ref; return contextThemes && themes ? { ...contextThemes, ...themes } : (_ref = contextThemes !== null && contextThemes !== void 0 ? contextThemes : themes) !== null && _ref !== void 0 ? _ref : {}; }, [contextThemes, themes]); return /*#__PURE__*/_jsx(Context, { value: value, children: children }); }; /** * Composes two themes. * @param high High priorty theme. * @param low Low priority theme. * @param mode Composition mode. * @param tag Specifity tag(s). * @return Composed theme. */ function compose(high, low, mode, tag) { if (high && low) { switch (mode) { case COMPOSE.DEEP: { const res = { ...low }; const prefix = Array.isArray(tag) ? `${high[tag[0]] || ''} ${high[tag[1]] || ''}` : high[tag] || ''; for (const key in high) { if (res[key]) { res[key] = `${res[key]} ${prefix} ${high[key]}`; } else res[key] = high[key]; } return res; } case COMPOSE.SOFT: return { ...low, ...high }; case COMPOSE.SWAP: return high; default: throw new Error(INVALID_COMPOSE); } } else return high !== null && high !== void 0 ? high : low; } /** * @deprecated * * Registers a themeable component under given name, and with an optional * default theme. * @param componentName Themed component name, which should be used to * provide its context theme via <ThemeProvider>. * @param [defaultTheme] Default theme, in form of theme key to * CSS class name mapping. If you have CSS modules and SCSS loader correctly * configured, the import `import theme from 'some.theme.scss';` will result * in `theme` object you can pass here. In some cases, it might be also legit * to construct theme object in a diffent way. * @param [options] Additional parameters. * @param [options.composeAdhocTheme=COMPOSE.DEEP] Composition type for * _ad hoc_ theme, which is merged into the result of composition of lower * priority themes. Must be one of COMPOSE values. * @param [options.composeContextTheme=COMPOSE.DEEP] Composition type * for context theme into default theme (or vice verca, if opted by * `themePriority` override). Must be one of COMPOSE values. * @param [options.themePriority=ADHOC_CONTEXT_DEFAULT] Theme * priorities. Must be one of PRIORITY values. * @param [options.mapThemeProps] By default, the themeable * component * created by `themed()` does not pass into the original wrapped component any * properties introduced by this library. It only passes down properties it * does not recognize, alongside the composed `theme`, and forwarded DOM `ref`. * In case a different behavior is needed, the property mapper can be * specified with this option. It should be a function with * ThemePropsMapper signature, and if present the result from this * function will be passed down the wrapped component as its props. * @param [options.contextTag=context] Override of `context` theme * key. * @param [options.adhocTag=ad.hoc] Override of `ad.hoc` theme key. * @param [options.composeTheme] Compatibility compose mode. * @param [options.mapThemrProps] Compatibility prop mapper. * @return Themeable component, registered under * given name. */ function themedImpl(componentName, defaultTheme, options = {}) { const { adhocTag = 'ad.hoc', contextTag = 'context', composeAdhocTheme: oComposeAdhocTheme, composeContextTheme: oComposeContextTheme, mapThemeProps: oMapThemeProps, themePriority: oThemePriority } = options; const aTag = adhocTag.split('.'); // TODO: Should we remove this runtime safeguard, assuming by now all // host projects should use TypeScript, which should prevent the error // we safeguard against here? // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (aTag.length !== 2 || !aTag[0] || !aTag[1]) { throw new Error('Invalid adhoc theme tag'); } return ThemeableComponent => { const Component = properties => { var _ref2, _compose; const { children, composeAdhocTheme, composeContextTheme, mapThemeProps, ref, theme, themePriority, ...rest } = properties; const context = use(Context); const contextTheme = context === null || context === void 0 ? void 0 : context[componentName]; /* Deduction of applicable theme composition and priority settings. */ const mapper = mapThemeProps !== null && mapThemeProps !== void 0 ? mapThemeProps : oMapThemeProps; const priority = (_ref2 = themePriority !== null && themePriority !== void 0 ? themePriority : oThemePriority) !== null && _ref2 !== void 0 ? _ref2 : PRIORITY.ADHOC_CONTEXT_DEFAULT; // TODO: Revise. // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, @typescript-eslint/non-nullable-type-assertion-style const composeAdhoc = composeAdhocTheme || oComposeAdhocTheme || COMPOSE.DEEP; // TODO: Revise. // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, @typescript-eslint/non-nullable-type-assertion-style const composeContext = composeContextTheme || oComposeContextTheme || COMPOSE.DEEP; /* Theme composition. */ let res = priority === PRIORITY.ADHOC_DEFAULT_CONTEXT ? compose(defaultTheme, contextTheme, composeContext, contextTag) : compose(contextTheme, defaultTheme, composeContext, contextTag); res = (_compose = compose(theme, res, composeAdhoc, aTag)) !== null && _compose !== void 0 ? _compose : {}; /* Props deduction. */ const p = mapper ? mapper(properties, res) : { ...rest, ref, theme: res }; /* eslint-disable react/jsx-props-no-spreading */ return /*#__PURE__*/_jsx(ThemeableComponent, { ...p, children: children }); /* eslint-enable react/jsx-props-no-spreading */ }; return Component; }; } /** @deprecated */ function themed( // 1st argument. componentOrComponentName, // 2nd argument. componentNameOrDefaultTheme, // 3rd argument. defaultThemeOrOptions, // 4th argument. options) { let component; let componentName; let defaultTheme; let ops; if (typeof componentOrComponentName === 'string') { // 1st argument: component name. componentName = componentOrComponentName; // 2nd argument: default theme. if (typeof componentNameOrDefaultTheme === 'string') { throw Error('Second argument is not expected to be a string'); } defaultTheme = componentNameOrDefaultTheme; // 3rd argument: options. ops = defaultThemeOrOptions; // 4th argument: none. if (options) throw Error('4th argument is not expected'); } else { // 1st argument: component. component = componentOrComponentName; // 2nd argument: component name. if (typeof componentNameOrDefaultTheme !== 'string') { throw Error('Second argument is not a string'); } componentName = componentNameOrDefaultTheme; // 3rd argument: default theme. defaultTheme = defaultThemeOrOptions; // 4th argument: options. ops = options; } const impl = themedImpl(componentName, defaultTheme, ops); return component ? impl(component) : impl; } /** @deprecated */ export default themed; /** * React hook for theme composition. */ export function useTheme(componentName, defaultTheme, adHocTheme, options) { var _compose2; const { adhocTag = 'ad.hoc', contextTag = 'context', composeAdhocTheme = COMPOSE.DEEP, composeContextTheme = COMPOSE.DEEP, themePriority = PRIORITY.ADHOC_CONTEXT_DEFAULT } = options !== null && options !== void 0 ? options : {}; const aTag = adhocTag.split('.'); // TODO: Should we remove this runtime safeguard, assuming by now all // host projects should use TypeScript, which should prevent the error // we safeguard against here? // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (aTag.length !== 2 || !aTag[0] || !aTag[1]) { throw new Error('Invalid adhoc theme tag'); } const context = use(Context); const contextTheme = context === null || context === void 0 ? void 0 : context[componentName]; let res = themePriority === PRIORITY.ADHOC_DEFAULT_CONTEXT ? compose(defaultTheme, contextTheme, composeContextTheme, contextTag) : compose(contextTheme, defaultTheme, composeContextTheme, contextTag); res = (_compose2 = compose(adHocTheme, res, composeAdhocTheme, aTag)) !== null && _compose2 !== void 0 ? _compose2 : {}; return res; } //# sourceMappingURL=index.js.map