@instructure/ui-themeable
Version:
A UI component library made by Instructure Inc.
312 lines (253 loc) • 12.7 kB
JavaScript
"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;