main
js 259 lines 8.04 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8 import {shorthandToLonghand} from './CSSShorthandProperty';
9
10 import hyphenateStyleName from '../shared/hyphenateStyleName';
11 import warnValidStyle from '../shared/warnValidStyle';
12 import isUnitlessNumber from '../shared/isUnitlessNumber';
13 import {checkCSSPropertyStringCoercion} from 'shared/CheckStringCoercion';
14 import {trackHostMutation} from 'react-reconciler/src/ReactFiberMutationTracking';
15
16 /**
17 * Operations for dealing with CSS properties.
18 */
19
20 /**
21 * This creates a string that is expected to be equivalent to the style
22 * attribute generated by server-side rendering. It by-passes warnings and
23 * security checks so it's not safe to use this value for anything other than
24 * comparison. It is only used in DEV for SSR validation.
25 */
26 export function createDangerousStringForStyles(styles) {
27 if (__DEV__) {
28 let serialized = '';
29 let delimiter = '';
30 for (const styleName in styles) {
31 if (!styles.hasOwnProperty(styleName)) {
32 continue;
33 }
34 const value = styles[styleName];
35 if (value != null && typeof value !== 'boolean' && value !== '') {
36 const isCustomProperty = styleName.indexOf('--') === 0;
37 if (isCustomProperty) {
38 if (__DEV__) {
39 checkCSSPropertyStringCoercion(value, styleName);
40 }
41 serialized += delimiter + styleName + ':' + ('' + value).trim();
42 } else {
43 if (
44 typeof value === 'number' &&
45 value !== 0 &&
46 !isUnitlessNumber(styleName)
47 ) {
48 serialized +=
49 delimiter + hyphenateStyleName(styleName) + ':' + value + 'px';
50 } else {
51 if (__DEV__) {
52 checkCSSPropertyStringCoercion(value, styleName);
53 }
54 serialized +=
55 delimiter +
56 hyphenateStyleName(styleName) +
57 ':' +
58 ('' + value).trim();
59 }
60 }
61 delimiter = ';';
62 }
63 }
64 return serialized || null;
65 }
66 }
67
68 function setValueForStyle(style, styleName, value) {
69 const isCustomProperty = styleName.indexOf('--') === 0;
70 if (__DEV__) {
71 if (!isCustomProperty) {
72 warnValidStyle(styleName, value);
73 }
74 }
75
76 if (value == null || typeof value === 'boolean' || value === '') {
77 if (isCustomProperty) {
78 style.setProperty(styleName, '');
79 } else if (styleName === 'float') {
80 style.cssFloat = '';
81 } else {
82 style[styleName] = '';
83 }
84 } else if (isCustomProperty) {
85 style.setProperty(styleName, value);
86 } else if (
87 typeof value === 'number' &&
88 value !== 0 &&
89 !isUnitlessNumber(styleName)
90 ) {
91 style[styleName] = value + 'px'; // Presumes implicit 'px' suffix for unitless numbers
92 } else {
93 if (styleName === 'float') {
94 style.cssFloat = value;
95 } else {
96 if (__DEV__) {
97 checkCSSPropertyStringCoercion(value, styleName);
98 }
99 style[styleName] = ('' + value).trim();
100 }
101 }
102 }
103
104 /**
105 * Sets the value for multiple styles on a node. If a value is specified as
106 * '' (empty string), the corresponding style property will be unset.
107 *
108 * @param {DOMElement} node
109 * @param {object} styles
110 */
111 export function setValueForStyles(node, styles, prevStyles) {
112 if (styles != null && typeof styles !== 'object') {
113 throw new Error(
114 'The `style` prop expects a mapping from style properties to values, ' +
115 "not a string. For example, style={{marginRight: spacing + 'em'}} when " +
116 'using JSX.',
117 );
118 }
119 if (__DEV__) {
120 if (styles) {
121 // Freeze the next style object so that we can assume it won't be
122 // mutated. We have already warned for this in the past.
123 Object.freeze(styles);
124 }
125 }
126
127 const style = node.style;
128
129 if (prevStyles != null) {
130 if (__DEV__) {
131 validateShorthandPropertyCollisionInDev(prevStyles, styles);
132 }
133
134 for (const styleName in prevStyles) {
135 if (
136 prevStyles.hasOwnProperty(styleName) &&
137 (styles == null || !styles.hasOwnProperty(styleName))
138 ) {
139 // Clear style
140 const isCustomProperty = styleName.indexOf('--') === 0;
141 if (isCustomProperty) {
142 style.setProperty(styleName, '');
143 } else if (styleName === 'float') {
144 style.cssFloat = '';
145 } else {
146 style[styleName] = '';
147 }
148 trackHostMutation();
149 }
150 }
151 for (const styleName in styles) {
152 const value = styles[styleName];
153 if (styles.hasOwnProperty(styleName) && prevStyles[styleName] !== value) {
154 setValueForStyle(style, styleName, value);
155 trackHostMutation();
156 }
157 }
158 } else {
159 for (const styleName in styles) {
160 if (styles.hasOwnProperty(styleName)) {
161 const value = styles[styleName];
162 setValueForStyle(style, styleName, value);
163 }
164 }
165 }
166 }
167
168 function isValueEmpty(value) {
169 return value == null || typeof value === 'boolean' || value === '';
170 }
171
172 /**
173 * Given {color: 'red', overflow: 'hidden'} returns {
174 * color: 'color',
175 * overflowX: 'overflow',
176 * overflowY: 'overflow',
177 * }. This can be read as "the overflowY property was set by the overflow
178 * shorthand". That is, the values are the property that each was derived from.
179 */
180 function expandShorthandMap(styles) {
181 const expanded = {};
182 for (const key in styles) {
183 const longhands = shorthandToLonghand[key] || [key];
184 for (let i = 0; i < longhands.length; i++) {
185 expanded[longhands[i]] = key;
186 }
187 }
188 return expanded;
189 }
190
191 /**
192 * When mixing shorthand and longhand property names, we warn during updates if
193 * we expect an incorrect result to occur. In particular, we warn for:
194 *
195 * Updating a shorthand property (longhand gets overwritten):
196 * {font: 'foo', fontVariant: 'bar'} -> {font: 'baz', fontVariant: 'bar'}
197 * becomes .style.font = 'baz'
198 * Removing a shorthand property (longhand gets lost too):
199 * {font: 'foo', fontVariant: 'bar'} -> {fontVariant: 'bar'}
200 * becomes .style.font = ''
201 * Removing a longhand property (should revert to shorthand; doesn't):
202 * {font: 'foo', fontVariant: 'bar'} -> {font: 'foo'}
203 * becomes .style.fontVariant = ''
204 */
205 function validateShorthandPropertyCollisionInDev(prevStyles, nextStyles) {
206 if (__DEV__) {
207 if (!nextStyles) {
208 return;
209 }
210
211 // Compute the diff as it would happen elsewhere.
212 const expandedUpdates = {};
213 if (prevStyles) {
214 for (const key in prevStyles) {
215 if (prevStyles.hasOwnProperty(key) && !nextStyles.hasOwnProperty(key)) {
216 const longhands = shorthandToLonghand[key] || [key];
217 for (let i = 0; i < longhands.length; i++) {
218 expandedUpdates[longhands[i]] = key;
219 }
220 }
221 }
222 }
223 for (const key in nextStyles) {
224 if (
225 nextStyles.hasOwnProperty(key) &&
226 (!prevStyles || prevStyles[key] !== nextStyles[key])
227 ) {
228 const longhands = shorthandToLonghand[key] || [key];
229 for (let i = 0; i < longhands.length; i++) {
230 expandedUpdates[longhands[i]] = key;
231 }
232 }
233 }
234
235 const expandedStyles = expandShorthandMap(nextStyles);
236 const warnedAbout = {};
237 for (const key in expandedUpdates) {
238 const originalKey = expandedUpdates[key];
239 const correctOriginalKey = expandedStyles[key];
240 if (correctOriginalKey && originalKey !== correctOriginalKey) {
241 const warningKey = originalKey + ',' + correctOriginalKey;
242 if (warnedAbout[warningKey]) {
243 continue;
244 }
245 warnedAbout[warningKey] = true;
246 console.error(
247 '%s a style property during rerender (%s) when a ' +
248 'conflicting property is set (%s) can lead to styling bugs. To ' +
249 "avoid this, don't mix shorthand and non-shorthand properties " +
250 'for the same value; instead, replace the shorthand with ' +
251 'separate values.',
252 isValueEmpty(nextStyles[originalKey]) ? 'Removing' : 'Updating',
253 originalKey,
254 correctOriginalKey,
255 );
256 }
257 }
258 }
259 }