UNPKG

11.4 kBJavaScriptView Raw
1import _classCallCheck from "@babel/runtime/helpers/esm/classCallCheck";
2import _createClass from "@babel/runtime/helpers/esm/createClass";
3import _possibleConstructorReturn from "@babel/runtime/helpers/esm/possibleConstructorReturn";
4import _getPrototypeOf from "@babel/runtime/helpers/esm/getPrototypeOf";
5import _get from "@babel/runtime/helpers/esm/get";
6import _inherits from "@babel/runtime/helpers/esm/inherits";
7import _createSuper from "@babel/runtime/helpers/esm/createSuper";
8import { warn as _warn } from "@instructure/console";
9
10/*
11 * The MIT License (MIT)
12 *
13 * Copyright (c) 2015 - present Instructure, Inc.
14 *
15 * Permission is hereby granted, free of charge, to any person obtaining a copy
16 * of this software and associated documentation files (the "Software"), to deal
17 * in the Software without restriction, including without limitation the rights
18 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
19 * copies of the Software, and to permit persons to whom the Software is
20 * furnished to do so, subject to the following conditions:
21 *
22 * The above copyright notice and this permission notice shall be included in all
23 * copies or substantial portions of the Software.
24 *
25 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
26 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
27 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
28 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
29 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
30 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
31 * SOFTWARE.
32 */
33import React from 'react';
34import PropTypes from 'prop-types';
35import { decorator } from '@instructure/ui-decorator';
36import { isEmpty, shallowEqual, deepEqual, hash } from '@instructure/ui-utils';
37import { uid } from '@instructure/uid';
38import { findDOMNode } from '@instructure/ui-dom-utils';
39import { ThemeContext } from "./ThemeContext.js";
40import { applyVariablesToNode } from "./applyVariablesToNode.js";
41import { setTextDirection } from "./setTextDirection.js";
42import { generateComponentTheme, generateTheme, registerComponentTheme, mountComponentStyles } from "./ThemeRegistry.js";
43/**
44 * ---
45 * category: utilities/themes
46 * ---
47 * A decorator or higher order component that makes a component `themeable`.
48 *
49 * As a HOC:
50 *
51 * ```js
52 * import themeable from '@instructure/ui-themeable'
53 * import styles from 'styles.css'
54 * import theme from 'theme.js'
55 *
56 * class Example extends React.Component {
57 * render () {
58 * return <div className={styles.root}>Hello</div>
59 * }
60 * }
61 *
62 * export default themeable(theme, styles)(Example)
63 * ```
64 *
65 * Note: in the above example, the CSS file must be transformed into a JS object
66 * via [babel](#babel-plugin-themeable-styles) or [webpack](#ui-webpack-config) loader.
67 *
68 * Themeable components inject their themed styles into the document when they are mounted.
69 *
70 * After the initial mount, a themeable component's theme can be configured explicitly
71 * via its `theme` prop or passed via React context using the [ApplyTheme](#ApplyTheme) component.
72 *
73 * Themeable components register themselves with the [global theme registry](#registry)
74 * when they are imported into the application, so you will need to be sure to import them
75 * before you mount your application so that the default themed styles can be generated and injected.
76 *
77 * @param {function} theme - A function that generates the component theme variables.
78 * @param {object} styles - The component styles object.
79 * @param {function} adapter - A function for mapping deprecated theme vars to updated values.
80 * @return {function} composes the themeable component.
81 */
82
83var emptyObj = {};
84/*
85 * Note: there are consumers (like canvas-lms and other edu org repos) that are
86 * consuming this file directly from "/src" (as opposed to "/es" or "/lib" like normal)
87 * because they need this file to not have the babel "class" transform ran against it
88 * (aka they need it to use real es6 `class`es, since you can't extend real es6
89 * class from es5 transpiled code)
90 *
91 * Which means that for the time being, we can't use any other es6/7/8 features in
92 * here that aren't supported by "last 2 edge versions" since we can't rely on babel
93 * to transpile them for those apps.
94 *
95 * So, that means don't use "static" class properties (like `static PropTypes = {...}`),
96 * or object spread (like "{...foo, ..bar}")" in this file until instUI 7 is released.
97 * Once we release instUI 7, the plan is to stop transpiling the "/es" dir for ie11
98 * so once we do that, this caveat no longer applies.
99 */
100
101var themeable = decorator(function (ComposedComponent, theme) {
102 var styles = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : {};
103 var adapter = arguments.length > 3 ? arguments[3] : void 0;
104 var displayName = ComposedComponent.displayName || ComposedComponent.name;
105 var componentId = "".concat(styles && styles.componentId || hash(ComposedComponent, 8));
106
107 if (process.env.NODE_ENV !== 'production') {
108 componentId = "".concat(displayName, "__").concat(componentId);
109
110 /*#__PURE__*/
111
112 /*#__PURE__*/
113 _warn(parseInt(React.version) >= 15, "[themeable] React 15 or higher is required. You are running React version ".concat(React.version, "."));
114 }
115
116 var contextKey = Symbol(componentId);
117
118 var template = function template() {};
119
120 if (styles && styles.template) {
121 template = typeof styles.template === 'function' ? styles.template : function () {
122 /*#__PURE__*/
123
124 /*#__PURE__*/
125 _warn(false, '[themeable] Invalid styles for: %O. Use @instructure/babel-plugin-themeable-styles to transform CSS imports.', displayName);
126
127 return '';
128 };
129 }
130
131 registerComponentTheme(contextKey, theme);
132
133 var getContext = function getContext(context) {
134 var themeContext = ThemeContext.getThemeContext(context);
135 return themeContext || emptyObj;
136 };
137
138 var getThemeFromContext = function getThemeFromContext(context) {
139 var _getContext = getContext(context),
140 theme = _getContext.theme;
141
142 if (theme && theme[contextKey]) {
143 return Object.assign({}, theme[contextKey]);
144 } else {
145 return emptyObj;
146 }
147 };
148
149 var generateThemeForContextKey = function generateThemeForContextKey(themeKey, overrides) {
150 return generateComponentTheme(contextKey, themeKey, overrides);
151 };
152
153 var ThemeableComponent = /*#__PURE__*/function (_ComposedComponent) {
154 _inherits(ThemeableComponent, _ComposedComponent);
155
156 var _super = _createSuper(ThemeableComponent);
157
158 function ThemeableComponent() {
159 var _this;
160
161 _classCallCheck(this, ThemeableComponent);
162
163 var res = _this = _super.apply(this, arguments);
164
165 _this._themeCache = null;
166 _this._instanceId = uid(displayName);
167 var defaultTheme = generateThemeForContextKey();
168 mountComponentStyles(template, defaultTheme, componentId);
169 return _possibleConstructorReturn(_this, res);
170 }
171
172 _createClass(ThemeableComponent, [{
173 key: "componentDidMount",
174 value: function componentDidMount() {
175 this.applyTheme();
176 setTextDirection();
177
178 if (_get(_getPrototypeOf(ThemeableComponent.prototype), "componentDidMount", this)) {
179 _get(_getPrototypeOf(ThemeableComponent.prototype), "componentDidMount", this).call(this);
180 }
181 }
182 }, {
183 key: "shouldComponentUpdate",
184 value: function shouldComponentUpdate(nextProps, nextState, nextContext) {
185 var themeContextWillChange = !deepEqual(ThemeContext.getThemeContext(this.context), ThemeContext.getThemeContext(nextContext));
186 if (themeContextWillChange) return true;
187
188 if (_get(_getPrototypeOf(ThemeableComponent.prototype), "shouldComponentUpdate", this)) {
189 return _get(_getPrototypeOf(ThemeableComponent.prototype), "shouldComponentUpdate", this).call(this, nextProps, nextState, nextContext);
190 }
191
192 return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState) || !shallowEqual(this.context, nextContext);
193 }
194 }, {
195 key: "componentDidUpdate",
196 value: function componentDidUpdate(prevProps, prevState, prevContext) {
197 if (!deepEqual(prevProps.theme, this.props.theme) || !deepEqual(getThemeFromContext(prevContext), getThemeFromContext(this.context))) {
198 this._themeCache = null;
199 }
200
201 this.applyTheme();
202
203 if (_get(_getPrototypeOf(ThemeableComponent.prototype), "componentDidUpdate", this)) {
204 _get(_getPrototypeOf(ThemeableComponent.prototype), "componentDidUpdate", this).call(this, prevProps, prevState, prevContext);
205 }
206 }
207 }, {
208 key: "applyTheme",
209 value: function applyTheme(DOMNode) {
210 if (isEmpty(this.theme)) {
211 return;
212 }
213
214 var defaultTheme = generateThemeForContextKey();
215 applyVariablesToNode(DOMNode || findDOMNode(this), // eslint-disable-line react/no-find-dom-node
216 this.theme, defaultTheme, componentId);
217 }
218 }, {
219 key: "scope",
220 get: function get() {
221 return "".concat(componentId, "__").concat(this._instanceId);
222 }
223 }, {
224 key: "theme",
225 get: function get() {
226 if (this._themeCache !== null) {
227 return this._themeCache;
228 }
229
230 var _getContext2 = getContext(this.context),
231 immutable = _getContext2.immutable;
232
233 var theme = getThemeFromContext(this.context);
234
235 if (this.props.theme && !isEmpty(this.props.theme)) {
236 if (!theme) {
237 theme = this.props.theme;
238 } else if (immutable) {
239 /*#__PURE__*/
240
241 /*#__PURE__*/
242 _warn(false, '[themeable] Parent theme is immutable. Cannot apply theme: %O', this.props.theme);
243 } else {
244 theme = isEmpty(theme) ? this.props.theme : Object.assign({}, theme, this.props.theme);
245 }
246 } // If adapter is provided, pass any overrides
247
248
249 if (typeof adapter === 'function') {
250 theme = adapter({
251 theme: theme,
252 displayName: displayName
253 });
254 } // pass in the component theme as overrides
255
256
257 this._themeCache = generateThemeForContextKey(null, theme);
258 return this._themeCache;
259 }
260 }]);
261
262 return ThemeableComponent;
263 }(ComposedComponent);
264
265 ThemeableComponent.componentId = componentId;
266 ThemeableComponent.theme = contextKey;
267 ThemeableComponent.contextTypes = Object.assign({}, ComposedComponent.contextTypes, ThemeContext.types);
268 ThemeableComponent.propTypes = Object.assign({}, ComposedComponent.propTypes, {
269 theme: PropTypes.object
270 } // eslint-disable-line react/forbid-prop-types
271 );
272 ThemeableComponent.generateTheme = generateThemeForContextKey;
273 return ThemeableComponent;
274});
275/**
276 * Utility to generate a theme for all themeable components that have been registered.
277 * This theme can be applied using the [ApplyTheme](#ApplyTheme) component.
278 *
279 * @param {String} themeKey The theme to use (for global theme variables across components)
280 * @param {Object} overrides theme variable overrides (usually for dynamic/user defined values)
281 * @return {Object} A theme config to use with `<ApplyTheme />`
282 */
283
284themeable.generateTheme = generateTheme;
285export default themeable;
286export { themeable };
\No newline at end of file