UNPKG

@dr.pogodin/react-themes

Version:
256 lines (240 loc) 9.17 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.PRIORITY = exports.COMPOSE = void 0; exports.ThemeProvider = ThemeProvider; exports.default = void 0; var _react = require("react"); var _jsxRuntime = require("react/jsx-runtime"); //------------------------------------------------------------------------------ // TypeScript interfaces & types. // 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: KeyT should be a union of string literals - valid theme keys. //------------------------------------------------------------------------------ // Constants. /** Supported theme composition modes. */ let COMPOSE = exports.COMPOSE = /*#__PURE__*/function (COMPOSE) { COMPOSE["DEEP"] = "DEEP"; COMPOSE["SOFT"] = "SOFT"; COMPOSE["SWAP"] = "SWAP"; return COMPOSE; }({}); /** Supported theme priorities. */ let PRIORITY = exports.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__*/(0, _react.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. */ function ThemeProvider({ children, themes }) { const contextThemes = (0, _react.use)(Context); // useMemo() ensures we don't generate a new "value" on each render when both // "contextThemes" and "themes" are defined. const value = (0, _react.useMemo)(() => contextThemes && themes ? { ...contextThemes, ...themes } : contextThemes || themes || {}, [contextThemes, themes]); return /*#__PURE__*/(0, _jsxRuntime.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] || ''; /* eslint-disable no-restricted-syntax */ for (const key in high) { if (res[key]) { res[key] = `${res[key]} ${prefix} ${high[key]}`; } else res[key] = high[key]; } /* eslint-enable no-restricted-syntax */ return res; } case COMPOSE.SOFT: return { ...low, ...high }; case COMPOSE.SWAP: return high; default: throw new Error(INVALID_COMPOSE); } } else return high || low; } /** * 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('.'); if (aTag.length !== 2 || !aTag[0] || !aTag[1]) { throw new Error('Invalid adhoc theme tag'); } return ThemeableComponent => { const component = properties => { const { children, composeAdhocTheme, composeContextTheme, mapThemeProps, ref, theme, themePriority, ...rest } = properties; const context = (0, _react.use)(Context); const contextTheme = context?.[componentName]; /* Deduction of applicable theme composition and priority settings. */ const mapper = mapThemeProps || oMapThemeProps; const priority = themePriority || oThemePriority || PRIORITY.ADHOC_CONTEXT_DEFAULT; const composeAdhoc = composeAdhocTheme || oComposeAdhocTheme || COMPOSE.DEEP; 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(theme, res, composeAdhoc, aTag) || {}; /* Props deduction. */ const p = mapper ? mapper({ ...properties, ref }, res) : { ...rest, theme: res, ref }; /* eslint-disable react/jsx-props-no-spreading */ return /*#__PURE__*/(0, _jsxRuntime.jsx)(ThemeableComponent, { ...p, children: children }); /* eslint-enable react/jsx-props-no-spreading */ }; return component; }; } 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; } var _default = exports.default = themed; //# sourceMappingURL=index.js.map