UNPKG

@instructure/ui-themeable

Version:

A UI component library made by Instructure Inc.

312 lines (253 loc) • 12.7 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.themeable = exports.default = void 0; var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck")); var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass")); var _possibleConstructorReturn2 = _interopRequireDefault(require("@babel/runtime/helpers/possibleConstructorReturn")); var _getPrototypeOf2 = _interopRequireDefault(require("@babel/runtime/helpers/getPrototypeOf")); var _get2 = _interopRequireDefault(require("@babel/runtime/helpers/get")); var _inherits2 = _interopRequireDefault(require("@babel/runtime/helpers/inherits")); var _createSuper2 = _interopRequireDefault(require("@babel/runtime/helpers/createSuper")); var _console = require("@instructure/console"); var _react = _interopRequireDefault(require("react")); var _propTypes = _interopRequireDefault(require("prop-types")); var _decorator = require("@instructure/ui-decorator/lib/decorator.js"); var _isEmpty = require("@instructure/ui-utils/lib/isEmpty.js"); var _shallowEqual = require("@instructure/ui-utils/lib/shallowEqual.js"); var _deepEqual = require("@instructure/ui-utils/lib/deepEqual.js"); var _hash = require("@instructure/ui-utils/lib/hash.js"); var _uid = require("@instructure/uid"); var _findDOMNode = require("@instructure/ui-dom-utils/lib/findDOMNode.js"); var _ThemeContext = require("./ThemeContext.js"); var _applyVariablesToNode = require("./applyVariablesToNode.js"); var _setTextDirection = require("./setTextDirection.js"); var _ThemeRegistry = require("./ThemeRegistry.js"); /* * The MIT License (MIT) * * Copyright (c) 2015 - present Instructure, Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ /** * --- * category: utilities/themes * --- * A decorator or higher order component that makes a component `themeable`. * * As a HOC: * * ```js * import themeable from '@instructure/ui-themeable' * import styles from 'styles.css' * import theme from 'theme.js' * * class Example extends React.Component { * render () { * return <div className={styles.root}>Hello</div> * } * } * * export default themeable(theme, styles)(Example) * ``` * * Note: in the above example, the CSS file must be transformed into a JS object * via [babel](#babel-plugin-themeable-styles) or [webpack](#ui-webpack-config) loader. * * Themeable components inject their themed styles into the document when they are mounted. * * After the initial mount, a themeable component's theme can be configured explicitly * via its `theme` prop or passed via React context using the [ApplyTheme](#ApplyTheme) component. * * Themeable components register themselves with the [global theme registry](#registry) * when they are imported into the application, so you will need to be sure to import them * before you mount your application so that the default themed styles can be generated and injected. * * @param {function} theme - A function that generates the component theme variables. * @param {object} styles - The component styles object. * @param {function} adapter - A function for mapping deprecated theme vars to updated values. * @return {function} composes the themeable component. */ var emptyObj = {}; /* * Note: there are consumers (like canvas-lms and other edu org repos) that are * consuming this file directly from "/src" (as opposed to "/es" or "/lib" like normal) * because they need this file to not have the babel "class" transform ran against it * (aka they need it to use real es6 `class`es, since you can't extend real es6 * class from es5 transpiled code) * * Which means that for the time being, we can't use any other es6/7/8 features in * here that aren't supported by "last 2 edge versions" since we can't rely on babel * to transpile them for those apps. * * So, that means don't use "static" class properties (like `static PropTypes = {...}`), * or object spread (like "{...foo, ..bar}")" in this file until instUI 7 is released. * Once we release instUI 7, the plan is to stop transpiling the "/es" dir for ie11 * so once we do that, this caveat no longer applies. */ var themeable = (0, _decorator.decorator)(function (ComposedComponent, theme) { var styles = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : {}; var adapter = arguments.length > 3 ? arguments[3] : void 0; var displayName = ComposedComponent.displayName || ComposedComponent.name; var componentId = "".concat(styles && styles.componentId || (0, _hash.hash)(ComposedComponent, 8)); if (process.env.NODE_ENV !== 'production') { componentId = "".concat(displayName, "__").concat(componentId); /*#__PURE__*/ ( /*#__PURE__*/0, _console.warn)(parseInt(_react.default.version) >= 15, "[themeable] React 15 or higher is required. You are running React version ".concat(_react.default.version, ".")); } var contextKey = Symbol(componentId); var template = function template() {}; if (styles && styles.template) { template = typeof styles.template === 'function' ? styles.template : function () { /*#__PURE__*/ ( /*#__PURE__*/0, _console.warn)(false, '[themeable] Invalid styles for: %O. Use @instructure/babel-plugin-themeable-styles to transform CSS imports.', displayName); return ''; }; } (0, _ThemeRegistry.registerComponentTheme)(contextKey, theme); var getContext = function getContext(context) { var themeContext = _ThemeContext.ThemeContext.getThemeContext(context); return themeContext || emptyObj; }; var getThemeFromContext = function getThemeFromContext(context) { var _getContext = getContext(context), theme = _getContext.theme; if (theme && theme[contextKey]) { return Object.assign({}, theme[contextKey]); } else { return emptyObj; } }; var generateThemeForContextKey = function generateThemeForContextKey(themeKey, overrides) { return (0, _ThemeRegistry.generateComponentTheme)(contextKey, themeKey, overrides); }; var ThemeableComponent = /*#__PURE__*/function (_ComposedComponent) { (0, _inherits2.default)(ThemeableComponent, _ComposedComponent); var _super = (0, _createSuper2.default)(ThemeableComponent); function ThemeableComponent() { var _this; (0, _classCallCheck2.default)(this, ThemeableComponent); var res = _this = _super.apply(this, arguments); _this._themeCache = null; _this._instanceId = (0, _uid.uid)(displayName); var defaultTheme = generateThemeForContextKey(); (0, _ThemeRegistry.mountComponentStyles)(template, defaultTheme, componentId); return (0, _possibleConstructorReturn2.default)(_this, res); } (0, _createClass2.default)(ThemeableComponent, [{ key: "componentDidMount", value: function componentDidMount() { this.applyTheme(); (0, _setTextDirection.setTextDirection)(); if ((0, _get2.default)((0, _getPrototypeOf2.default)(ThemeableComponent.prototype), "componentDidMount", this)) { (0, _get2.default)((0, _getPrototypeOf2.default)(ThemeableComponent.prototype), "componentDidMount", this).call(this); } } }, { key: "shouldComponentUpdate", value: function shouldComponentUpdate(nextProps, nextState, nextContext) { var themeContextWillChange = !(0, _deepEqual.deepEqual)(_ThemeContext.ThemeContext.getThemeContext(this.context), _ThemeContext.ThemeContext.getThemeContext(nextContext)); if (themeContextWillChange) return true; if ((0, _get2.default)((0, _getPrototypeOf2.default)(ThemeableComponent.prototype), "shouldComponentUpdate", this)) { return (0, _get2.default)((0, _getPrototypeOf2.default)(ThemeableComponent.prototype), "shouldComponentUpdate", this).call(this, nextProps, nextState, nextContext); } return !(0, _shallowEqual.shallowEqual)(this.props, nextProps) || !(0, _shallowEqual.shallowEqual)(this.state, nextState) || !(0, _shallowEqual.shallowEqual)(this.context, nextContext); } }, { key: "componentDidUpdate", value: function componentDidUpdate(prevProps, prevState, prevContext) { if (!(0, _deepEqual.deepEqual)(prevProps.theme, this.props.theme) || !(0, _deepEqual.deepEqual)(getThemeFromContext(prevContext), getThemeFromContext(this.context))) { this._themeCache = null; } this.applyTheme(); if ((0, _get2.default)((0, _getPrototypeOf2.default)(ThemeableComponent.prototype), "componentDidUpdate", this)) { (0, _get2.default)((0, _getPrototypeOf2.default)(ThemeableComponent.prototype), "componentDidUpdate", this).call(this, prevProps, prevState, prevContext); } } }, { key: "applyTheme", value: function applyTheme(DOMNode) { if ((0, _isEmpty.isEmpty)(this.theme)) { return; } var defaultTheme = generateThemeForContextKey(); (0, _applyVariablesToNode.applyVariablesToNode)(DOMNode || (0, _findDOMNode.findDOMNode)(this), // eslint-disable-line react/no-find-dom-node this.theme, defaultTheme, componentId); } }, { key: "scope", get: function get() { return "".concat(componentId, "__").concat(this._instanceId); } }, { key: "theme", get: function get() { if (this._themeCache !== null) { return this._themeCache; } var _getContext2 = getContext(this.context), immutable = _getContext2.immutable; var theme = getThemeFromContext(this.context); if (this.props.theme && !(0, _isEmpty.isEmpty)(this.props.theme)) { if (!theme) { theme = this.props.theme; } else if (immutable) { /*#__PURE__*/ ( /*#__PURE__*/0, _console.warn)(false, '[themeable] Parent theme is immutable. Cannot apply theme: %O', this.props.theme); } else { theme = (0, _isEmpty.isEmpty)(theme) ? this.props.theme : Object.assign({}, theme, this.props.theme); } } // If adapter is provided, pass any overrides if (typeof adapter === 'function') { theme = adapter({ theme: theme, displayName: displayName }); } // pass in the component theme as overrides this._themeCache = generateThemeForContextKey(null, theme); return this._themeCache; } }]); return ThemeableComponent; }(ComposedComponent); ThemeableComponent.componentId = componentId; ThemeableComponent.theme = contextKey; ThemeableComponent.contextTypes = Object.assign({}, ComposedComponent.contextTypes, _ThemeContext.ThemeContext.types); ThemeableComponent.propTypes = Object.assign({}, ComposedComponent.propTypes, { theme: _propTypes.default.object } // eslint-disable-line react/forbid-prop-types ); ThemeableComponent.generateTheme = generateThemeForContextKey; return ThemeableComponent; }); /** * Utility to generate a theme for all themeable components that have been registered. * This theme can be applied using the [ApplyTheme](#ApplyTheme) component. * * @param {String} themeKey The theme to use (for global theme variables across components) * @param {Object} overrides theme variable overrides (usually for dynamic/user defined values) * @return {Object} A theme config to use with `<ApplyTheme />` */ exports.themeable = themeable; themeable.generateTheme = _ThemeRegistry.generateTheme; var _default = themeable; exports.default = _default;