@samitouri / QOS-React-2 / commits / 24dd07bd26

Add custom element property support behind a flag (#22184)

* custom element props * custom element events * use function type for on* * tests, htmlFor * className * fix ReactDOMComponent-test * started on adding feature flag * added feature flag to all feature flag files * everything passes * tried to fix getPropertyInfo * used @gate and __experimental__ * remove flag gating for test which already passes * fix onClick test * add __EXPERIMENTAL__ to www flags, rename eventProxy * Add innerText and textContent to reservedProps * Emit warning when assigning to read only properties in client * Revert "Emit warning when assigning to read only properties in client" This reverts commit 1a093e584ce50e2e634aa743e04f9cb8fc2b3f7d. * Emit warning when assigning to read only properties during hydration * yarn prettier-all * Gate hydration warning test on flag * Fix gating in hydration warning test * Fix assignment to boolean properties * Replace _listeners with random suffix matching * Improve gating for hydration warning test * Add outerText and outerHTML to server warning properties * remove nameLower logic * fix capture event listener test * Add coverage for changing custom event listeners * yarn prettier-all * yarn lint --fix * replace getCustomElementEventHandlersFromNode with getFiberCurrentPropsFromNode * Remove previous value when adding event listener * flow, lint, prettier * Add dispatchEvent to make sure nothing crashes * Add state change to reserved attribute tests * Add missing feature flag test gate * Reimplement SSR changes in ReactDOMServerFormatConfig * Test hydration for objects and functions * add missing test gate * remove extraneous comment * Add attribute->property test

Joey Arhar committed Dec 8, 2021 at 07:11 UTC 24dd07bd269590ee5024b7f0f1906887d256ea86
17 files changed +594 -11
packages/react-dom/src/__tests__/DOMPropertyOperations-test.js
+349
@@ -155,6 +155,355 @@ describe('DOMPropertyOperations', () => {
155 // Regression test for https://github.com/facebook/react/issues/6119
156 expect(container.firstChild.hasAttribute('value')).toBe(false);
157 });
158 +
159 + // @gate enableCustomElementPropertySupport
160 + it('custom element custom events lowercase', () => {
161 + const oncustomevent = jest.fn();
162 + function Test() {
163 + return <my-custom-element oncustomevent={oncustomevent} />;
164 + }
165 + const container = document.createElement('div');
166 + ReactDOM.render(<Test />, container);
167 + container
168 + .querySelector('my-custom-element')
169 + .dispatchEvent(new Event('customevent'));
170 + expect(oncustomevent).toHaveBeenCalledTimes(1);
171 + });
172 +
173 + // @gate enableCustomElementPropertySupport
174 + it('custom element custom events uppercase', () => {
175 + const oncustomevent = jest.fn();
176 + function Test() {
177 + return <my-custom-element onCustomevent={oncustomevent} />;
178 + }
179 + const container = document.createElement('div');
180 + ReactDOM.render(<Test />, container);
181 + container
182 + .querySelector('my-custom-element')
183 + .dispatchEvent(new Event('Customevent'));
184 + expect(oncustomevent).toHaveBeenCalledTimes(1);
185 + });
186 +
187 + // @gate enableCustomElementPropertySupport
188 + it('custom element custom event with dash in name', () => {
189 + const oncustomevent = jest.fn();
190 + function Test() {
191 + return <my-custom-element oncustom-event={oncustomevent} />;
192 + }
193 + const container = document.createElement('div');
194 + ReactDOM.render(<Test />, container);
195 + container
196 + .querySelector('my-custom-element')
197 + .dispatchEvent(new Event('custom-event'));
198 + expect(oncustomevent).toHaveBeenCalledTimes(1);
199 + });
200 +
201 + // @gate enableCustomElementPropertySupport
202 + it('custom element remove event handler', () => {
203 + const oncustomevent = jest.fn();
204 + function Test(props) {
205 + return <my-custom-element oncustomevent={props.handler} />;
206 + }
207 +
208 + const container = document.createElement('div');
209 + ReactDOM.render(<Test handler={oncustomevent} />, container);
210 + const customElement = container.querySelector('my-custom-element');
211 + customElement.dispatchEvent(new Event('customevent'));
212 + expect(oncustomevent).toHaveBeenCalledTimes(1);
213 +
214 + ReactDOM.render(<Test handler={false} />, container);
215 + // Make sure that the second render didn't create a new element. We want
216 + // to make sure removeEventListener actually gets called on the same element.
217 + expect(customElement).toBe(customElement);
218 + customElement.dispatchEvent(new Event('customevent'));
219 + expect(oncustomevent).toHaveBeenCalledTimes(1);
220 +
221 + ReactDOM.render(<Test handler={oncustomevent} />, container);
222 + customElement.dispatchEvent(new Event('customevent'));
223 + expect(oncustomevent).toHaveBeenCalledTimes(2);
224 +
225 + const oncustomevent2 = jest.fn();
226 + ReactDOM.render(<Test handler={oncustomevent2} />, container);
227 + customElement.dispatchEvent(new Event('customevent'));
228 + expect(oncustomevent).toHaveBeenCalledTimes(2);
229 + expect(oncustomevent2).toHaveBeenCalledTimes(1);
230 + });
231 +
232 + it('custom elements shouldnt have non-functions for on* attributes treated as event listeners', () => {
233 + const container = document.createElement('div');
234 + ReactDOM.render(
235 + <my-custom-element
236 + onstring={'hello'}
237 + onobj={{hello: 'world'}}
238 + onarray={['one', 'two']}
239 + ontrue={true}
240 + onfalse={false}
241 + />,
242 + container,
243 + );
244 + const customElement = container.querySelector('my-custom-element');
245 + expect(customElement.getAttribute('onstring')).toBe('hello');
246 + expect(customElement.getAttribute('onobj')).toBe('[object Object]');
247 + expect(customElement.getAttribute('onarray')).toBe('one,two');
248 + expect(customElement.getAttribute('ontrue')).toBe('true');
249 + expect(customElement.getAttribute('onfalse')).toBe('false');
250 +
251 + // Dispatch the corresponding event names to make sure that nothing crashes.
252 + customElement.dispatchEvent(new Event('string'));
253 + customElement.dispatchEvent(new Event('obj'));
254 + customElement.dispatchEvent(new Event('array'));
255 + customElement.dispatchEvent(new Event('true'));
256 + customElement.dispatchEvent(new Event('false'));
257 + });
258 +
259 + it('custom elements should still have onClick treated like regular elements', () => {
260 + let syntheticClickEvent = null;
261 + const syntheticEventHandler = jest.fn(
262 + event => (syntheticClickEvent = event),
263 + );
264 + let nativeClickEvent = null;
265 + const nativeEventHandler = jest.fn(event => (nativeClickEvent = event));
266 + function Test() {
267 + return <my-custom-element onClick={syntheticEventHandler} />;
268 + }
269 +
270 + const container = document.createElement('div');
271 + document.body.appendChild(container);
272 + ReactDOM.render(<Test />, container);
273 +
274 + const customElement = container.querySelector('my-custom-element');
275 + customElement.onclick = nativeEventHandler;
276 + container.querySelector('my-custom-element').click();
277 +
278 + expect(nativeEventHandler).toHaveBeenCalledTimes(1);
279 + expect(syntheticEventHandler).toHaveBeenCalledTimes(1);
280 + expect(syntheticClickEvent.nativeEvent).toBe(nativeClickEvent);
281 + });
282 +
283 + // @gate enableCustomElementPropertySupport
284 + it('custom elements should allow custom events with capture event listeners', () => {
285 + const oncustomeventCapture = jest.fn();
286 + const oncustomevent = jest.fn();
287 + function Test() {
288 + return (
289 + <my-custom-element
290 + oncustomeventCapture={oncustomeventCapture}
291 + oncustomevent={oncustomevent}>
292 + <div />
293 + </my-custom-element>
294 + );
295 + }
296 + const container = document.createElement('div');
297 + ReactDOM.render(<Test />, container);
298 + container
299 + .querySelector('my-custom-element > div')
300 + .dispatchEvent(new Event('customevent', {bubbles: false}));
301 + expect(oncustomeventCapture).toHaveBeenCalledTimes(1);
302 + expect(oncustomevent).toHaveBeenCalledTimes(0);
303 + });
304 +
305 + it('innerHTML should not work on custom elements', () => {
306 + const container = document.createElement('div');
307 + ReactDOM.render(<my-custom-element innerHTML="foo" />, container);
308 + const customElement = container.querySelector('my-custom-element');
309 + expect(customElement.getAttribute('innerHTML')).toBe(null);
310 + expect(customElement.hasChildNodes()).toBe(false);
311 +
312 + // Render again to verify the update codepath doesn't accidentally let
313 + // something through.
314 + ReactDOM.render(<my-custom-element innerHTML="bar" />, container);
315 + expect(customElement.getAttribute('innerHTML')).toBe(null);
316 + expect(customElement.hasChildNodes()).toBe(false);
317 + });
318 +
319 + // @gate enableCustomElementPropertySupport
320 + it('innerText should not work on custom elements', () => {
321 + const container = document.createElement('div');
322 + ReactDOM.render(<my-custom-element innerText="foo" />, container);
323 + const customElement = container.querySelector('my-custom-element');
324 + expect(customElement.getAttribute('innerText')).toBe(null);
325 + expect(customElement.hasChildNodes()).toBe(false);
326 +
327 + // Render again to verify the update codepath doesn't accidentally let
328 + // something through.
329 + ReactDOM.render(<my-custom-element innerText="bar" />, container);
330 + expect(customElement.getAttribute('innerText')).toBe(null);
331 + expect(customElement.hasChildNodes()).toBe(false);
332 + });
333 +
334 + // @gate enableCustomElementPropertySupport
335 + it('textContent should not work on custom elements', () => {
336 + const container = document.createElement('div');
337 + ReactDOM.render(<my-custom-element textContent="foo" />, container);
338 + const customElement = container.querySelector('my-custom-element');
339 + expect(customElement.getAttribute('textContent')).toBe(null);
340 + expect(customElement.hasChildNodes()).toBe(false);
341 +
342 + // Render again to verify the update codepath doesn't accidentally let
343 + // something through.
344 + ReactDOM.render(<my-custom-element textContent="bar" />, container);
345 + expect(customElement.getAttribute('textContent')).toBe(null);
346 + expect(customElement.hasChildNodes()).toBe(false);
347 + });
348 +
349 + // @gate enableCustomElementPropertySupport
350 + it('values should not be converted to booleans when assigning into custom elements', () => {
351 + const container = document.createElement('div');
352 + document.body.appendChild(container);
353 + ReactDOM.render(<my-custom-element />, container);
354 + const customElement = container.querySelector('my-custom-element');
355 + customElement.foo = null;
356 +
357 + // true => string
358 + ReactDOM.render(<my-custom-element foo={true} />, container);
359 + expect(customElement.foo).toBe(true);
360 + ReactDOM.render(<my-custom-element foo="bar" />, container);
361 + expect(customElement.foo).toBe('bar');
362 +
363 + // false => string
364 + ReactDOM.render(<my-custom-element foo={false} />, container);
365 + expect(customElement.foo).toBe(false);
366 + ReactDOM.render(<my-custom-element foo="bar" />, container);
367 + expect(customElement.foo).toBe('bar');
368 +
369 + // true => null
370 + ReactDOM.render(<my-custom-element foo={true} />, container);
371 + expect(customElement.foo).toBe(true);
372 + ReactDOM.render(<my-custom-element foo={null} />, container);
373 + expect(customElement.foo).toBe(null);
374 +
375 + // false => null
376 + ReactDOM.render(<my-custom-element foo={false} />, container);
377 + expect(customElement.foo).toBe(false);
378 + ReactDOM.render(<my-custom-element foo={null} />, container);
379 + expect(customElement.foo).toBe(null);
380 + });
381 +
382 + // @gate enableCustomElementPropertySupport
383 + it('custom element custom event handlers assign multiple types', () => {
384 + const container = document.createElement('div');
385 + document.body.appendChild(container);
386 + const oncustomevent = jest.fn();
387 +
388 + // First render with string
389 + ReactDOM.render(<my-custom-element oncustomevent={'foo'} />, container);
390 + const customelement = container.querySelector('my-custom-element');
391 + customelement.dispatchEvent(new Event('customevent'));
392 + expect(oncustomevent).toHaveBeenCalledTimes(0);
393 + expect(customelement.oncustomevent).toBe(undefined);
394 + expect(customelement.getAttribute('oncustomevent')).toBe('foo');
395 +
396 + // string => event listener
397 + ReactDOM.render(
398 + <my-custom-element oncustomevent={oncustomevent} />,
399 + container,
400 + );
401 + customelement.dispatchEvent(new Event('customevent'));
402 + expect(oncustomevent).toHaveBeenCalledTimes(1);
403 + expect(customelement.oncustomevent).toBe(undefined);
404 + expect(customelement.getAttribute('oncustomevent')).toBe(null);
405 +
406 + // event listener => string
407 + ReactDOM.render(<my-custom-element oncustomevent={'foo'} />, container);
408 + customelement.dispatchEvent(new Event('customevent'));
409 + expect(oncustomevent).toHaveBeenCalledTimes(1);
410 + expect(customelement.oncustomevent).toBe(undefined);
411 + expect(customelement.getAttribute('oncustomevent')).toBe('foo');
412 +
413 + // string => nothing
414 + ReactDOM.render(<my-custom-element />, container);
415 + customelement.dispatchEvent(new Event('customevent'));
416 + expect(oncustomevent).toHaveBeenCalledTimes(1);
417 + expect(customelement.oncustomevent).toBe(undefined);
418 + expect(customelement.getAttribute('oncustomevent')).toBe(null);
419 +
420 + // nothing => event listener
421 + ReactDOM.render(
422 + <my-custom-element oncustomevent={oncustomevent} />,
423 + container,
424 + );
425 + customelement.dispatchEvent(new Event('customevent'));
426 + expect(oncustomevent).toHaveBeenCalledTimes(2);
427 + expect(customelement.oncustomevent).toBe(undefined);
428 + expect(customelement.getAttribute('oncustomevent')).toBe(null);
429 + });
430 +
431 + // @gate enableCustomElementPropertySupport
432 + it('custom element custom event handlers assign multiple types with setter', () => {
433 + const container = document.createElement('div');
434 + document.body.appendChild(container);
435 + const oncustomevent = jest.fn();
436 +
437 + // First render with nothing
438 + ReactDOM.render(<my-custom-element />, container);
439 + const customelement = container.querySelector('my-custom-element');
440 + // Install a setter to activate the `in` heuristic
441 + Object.defineProperty(customelement, 'oncustomevent', {
442 + set: function(x) {
443 + this._oncustomevent = x;
444 + },
445 + get: function() {
446 + return this._oncustomevent;
447 + },
448 + });
449 + expect(customelement.oncustomevent).toBe(undefined);
450 +
451 + // nothing => event listener
452 + ReactDOM.render(
453 + <my-custom-element oncustomevent={oncustomevent} />,
454 + container,
455 + );
456 + customelement.dispatchEvent(new Event('customevent'));
457 + expect(oncustomevent).toHaveBeenCalledTimes(1);
458 + expect(customelement.oncustomevent).toBe(null);
459 + expect(customelement.getAttribute('oncustomevent')).toBe(null);
460 +
461 + // event listener => string
462 + ReactDOM.render(<my-custom-element oncustomevent={'foo'} />, container);
463 + customelement.dispatchEvent(new Event('customevent'));
464 + expect(oncustomevent).toHaveBeenCalledTimes(1);
465 + expect(customelement.oncustomevent).toBe('foo');
466 + expect(customelement.getAttribute('oncustomevent')).toBe(null);
467 +
468 + // string => event listener
469 + ReactDOM.render(
470 + <my-custom-element oncustomevent={oncustomevent} />,
471 + container,
472 + );
473 + customelement.dispatchEvent(new Event('customevent'));
474 + expect(oncustomevent).toHaveBeenCalledTimes(2);
475 + expect(customelement.oncustomevent).toBe(null);
476 + expect(customelement.getAttribute('oncustomevent')).toBe(null);
477 +
478 + // event listener => nothing
479 + ReactDOM.render(<my-custom-element />, container);
480 + customelement.dispatchEvent(new Event('customevent'));
481 + expect(oncustomevent).toHaveBeenCalledTimes(2);
482 + expect(customelement.oncustomevent).toBe(null);
483 + expect(customelement.getAttribute('oncustomevent')).toBe(null);
484 + });
485 +
486 + // @gate enableCustomElementPropertySupport
487 + it('assigning to a custom element property should not remove attributes', () => {
488 + const container = document.createElement('div');
489 + document.body.appendChild(container);
490 + ReactDOM.render(<my-custom-element foo="one" />, container);
491 + const customElement = container.querySelector('my-custom-element');
492 + expect(customElement.getAttribute('foo')).toBe('one');
493 +
494 + // Install a setter to activate the `in` heuristic
495 + Object.defineProperty(customElement, 'foo', {
496 + set: function(x) {
497 + this._foo = x;
498 + },
499 + get: function() {
500 + return this._foo;
501 + },
502 + });
503 + ReactDOM.render(<my-custom-element foo="two" />, container);
504 + expect(customElement.foo).toBe('two');
505 + expect(customElement.getAttribute('foo')).toBe('one');
506 + });
507 });
508
509 describe('deleteValueForProperty', () => {
packages/react-dom/src/__tests__/ReactDOMServerIntegrationAttributes-test.js
+15 -2
@@ -10,6 +10,7 @@
10 'use strict';
11
12 const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegrationTestUtils');
13 +const ReactFeatureFlags = require('shared/ReactFeatureFlags');
14
15 let React;
16 let ReactDOM;
@@ -36,6 +37,7 @@ const {
37 resetModules,
38 itRenders,
39 clientCleanRender,
40 + clientRenderOnServerString,
41 } = ReactDOMServerIntegrationUtils(initModules);
42
43 describe('ReactDOMServerIntegration', () => {
@@ -657,17 +659,28 @@ describe('ReactDOMServerIntegration', () => {
659 });
660
661 itRenders('className for custom elements', async render => {
660 - const e = await render(<div is="custom-element" className="test" />, 0);
661 - expect(e.getAttribute('className')).toBe('test');
662 + if (ReactFeatureFlags.enableCustomElementPropertySupport) {
663 + const e = await render(
664 + <div is="custom-element" className="test" />,
665 + render === clientRenderOnServerString ? 1 : 0,
666 + );
667 + expect(e.getAttribute('className')).toBe(null);
668 + expect(e.getAttribute('class')).toBe('test');
669 + } else {
670 + const e = await render(<div is="custom-element" className="test" />, 0);
671 + expect(e.getAttribute('className')).toBe('test');
672 + }
673 });
674
675 itRenders('htmlFor attribute on custom elements', async render => {
676 const e = await render(<div is="custom-element" htmlFor="test" />);
677 expect(e.getAttribute('htmlFor')).toBe('test');
678 + expect(e.getAttribute('for')).toBe(null);
679 });
680
681 itRenders('for attribute on custom elements', async render => {
682 const e = await render(<div is="custom-element" for="test" />);
683 + expect(e.getAttribute('htmlFor')).toBe(null);
684 expect(e.getAttribute('for')).toBe('test');
685 });
686
packages/react-dom/src/__tests__/ReactServerRendering-test.js
+39
@@ -1097,4 +1097,43 @@ describe('ReactDOMServer', () => {
1097 'However, it is set to a string.',
1098 );
1099 });
1100 +
1101 + describe('custom element server rendering', () => {
1102 + it('String properties should be server rendered for custom elements', () => {
1103 + const output = ReactDOMServer.renderToString(
1104 + <my-custom-element foo="bar" />,
1105 + );
1106 + expect(output).toBe(`<my-custom-element foo="bar"></my-custom-element>`);
1107 + });
1108 +
1109 + it('Number properties should be server rendered for custom elements', () => {
1110 + const output = ReactDOMServer.renderToString(
1111 + <my-custom-element foo={5} />,
1112 + );
1113 + expect(output).toBe(`<my-custom-element foo="5"></my-custom-element>`);
1114 + });
1115 +
1116 + // @gate enableCustomElementPropertySupport
1117 + it('Object properties should not be server rendered for custom elements', () => {
1118 + const output = ReactDOMServer.renderToString(
1119 + <my-custom-element foo={{foo: 'bar'}} />,
1120 + );
1121 + expect(output).toBe(`<my-custom-element></my-custom-element>`);
1122 + });
1123 +
1124 + // @gate enableCustomElementPropertySupport
1125 + it('Array properties should not be server rendered for custom elements', () => {
1126 + const output = ReactDOMServer.renderToString(
1127 + <my-custom-element foo={['foo', 'bar']} />,
1128 + );
1129 + expect(output).toBe(`<my-custom-element></my-custom-element>`);
1130 + });
1131 +
1132 + it('Function properties should not be server rendered for custom elements', () => {
1133 + const output = ReactDOMServer.renderToString(
1134 + <my-custom-element foo={() => console.log('bar')} />,
1135 + );
1136 + expect(output).toBe(`<my-custom-element></my-custom-element>`);
1137 + });
1138 + });
1139 });
packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js
+66
@@ -530,4 +530,70 @@ describe('ReactDOMServerHydration', () => {
530 'Warning: Did not expect server HTML to contain a <p> in <div>',
531 );
532 });
533 +
534 + it('should warn when hydrating read-only properties', () => {
535 + const readOnlyProperties = [
536 + 'offsetParent',
537 + 'offsetTop',
538 + 'offsetLeft',
539 + 'offsetWidth',
540 + 'offsetHeight',
541 + 'isContentEditable',
542 + 'outerText',
543 + 'outerHTML',
544 + ];
545 + readOnlyProperties.forEach(readOnlyProperty => {
546 + const props = {};
547 + props[readOnlyProperty] = 'hello';
548 + const jsx = React.createElement('my-custom-element', props);
549 + const element = document.createElement('div');
550 + element.innerHTML = ReactDOMServer.renderToString(jsx);
551 + if (gate(flags => flags.enableCustomElementPropertySupport)) {
552 + expect(() => ReactDOM.hydrate(jsx, element)).toErrorDev(
553 + `Warning: Assignment to read-only property will result in a no-op: \`${readOnlyProperty}\``,
554 + );
555 + } else {
556 + ReactDOM.hydrate(jsx, element);
557 + }
558 + });
559 + });
560 +
561 + // @gate enableCustomElementPropertySupport
562 + it('should not re-assign properties on hydration', () => {
563 + const container = document.createElement('div');
564 + document.body.appendChild(container);
565 +
566 + const jsx = React.createElement('my-custom-element', {
567 + str: 'string',
568 + obj: {foo: 'bar'},
569 + });
570 +
571 + container.innerHTML = ReactDOMServer.renderToString(jsx);
572 + const customElement = container.querySelector('my-custom-element');
573 +
574 + // Install setters to activate `in` check
575 + Object.defineProperty(customElement, 'str', {
576 + set: function(x) {
577 + this._str = x;
578 + },
579 + get: function() {
580 + return this._str;
581 + },
582 + });
583 + Object.defineProperty(customElement, 'obj', {
584 + set: function(x) {
585 + this._obj = x;
586 + },
587 + get: function() {
588 + return this._obj;
589 + },
590 + });
591 +
592 + ReactDOM.hydrate(jsx, container);
593 +
594 + expect(customElement.getAttribute('str')).toBe('string');
595 + expect(customElement.getAttribute('obj')).toBe(null);
596 + expect(customElement.str).toBe(undefined);
597 + expect(customElement.obj).toBe(undefined);
598 + });
599 });
packages/react-dom/src/client/DOMPropertyOperations.js
+45
@@ -19,8 +19,10 @@ import sanitizeURL from '../shared/sanitizeURL';
19 import {
20 disableJavaScriptURLs,
21 enableTrustedTypesIntegration,
22 + enableCustomElementPropertySupport,
23 } from 'shared/ReactFeatureFlags';
24 import {checkAttributeStringCoercion} from 'shared/CheckStringCoercion';
25 +import {getFiberCurrentPropsFromNode} from './ReactDOMComponentTree';
26
27 import type {PropertyInfo} from '../shared/DOMProperty';
28
@@ -149,9 +151,52 @@ export function setValueForProperty(
151 if (shouldIgnoreAttribute(name, propertyInfo, isCustomComponentTag)) {
152 return;
153 }
154 +
155 + if (
156 + enableCustomElementPropertySupport &&
157 + isCustomComponentTag &&
158 + name[0] === 'o' &&
159 + name[1] === 'n'
160 + ) {
161 + let eventName = name.replace(/Capture$/, '');
162 + const useCapture = name !== eventName;
163 + eventName = eventName.slice(2);
164 +
165 + const prevProps = getFiberCurrentPropsFromNode(node);
166 + const prevValue = prevProps != null ? prevProps[name] : null;
167 + if (typeof prevValue === 'function') {
168 + node.removeEventListener(eventName, prevValue, useCapture);
169 + }
170 + if (typeof value === 'function') {
171 + if (typeof prevValue !== 'function' && prevValue !== null) {
172 + // If we previously assigned a non-function type into this node, then
173 + // remove it when switching to event listener mode.
174 + if (name in (node: any)) {
175 + (node: any)[name] = null;
176 + } else if (node.hasAttribute(name)) {
177 + node.removeAttribute(name);
178 + }
179 + }
180 +
181 + // $FlowFixMe value can't be casted to EventListener.
182 + node.addEventListener(eventName, (value: EventListener), useCapture);
183 + return;
184 + }
185 + }
186 +
187 if (shouldRemoveAttribute(name, value, propertyInfo, isCustomComponentTag)) {
188 value = null;
189 }
190 +
191 + if (
192 + enableCustomElementPropertySupport &&
193 + isCustomComponentTag &&
194 + name in (node: any)
195 + ) {
196 + (node: any)[name] = value;
197 + return;
198 + }
199 +
200 // If the prop isn't in the special list, treat it as a simple attribute.
201 if (isCustomComponentTag || propertyInfo === null) {
202 if (isAttributeNameSafe(name)) {
packages/react-dom/src/client/ReactDOMComponent.js
+38 -4
@@ -69,7 +69,10 @@ import {validateProperties as validateARIAProperties} from '../shared/ReactDOMIn
69 import {validateProperties as validateInputProperties} from '../shared/ReactDOMNullInputValuePropHook';
70 import {validateProperties as validateUnknownProperties} from '../shared/ReactDOMUnknownPropertyHook';
71
72 -import {enableTrustedTypesIntegration} from 'shared/ReactFeatureFlags';
72 +import {
73 + enableTrustedTypesIntegration,
74 + enableCustomElementPropertySupport,
75 +} from 'shared/ReactFeatureFlags';
76 import {
77 mediaEventTypes,
78 listenToNonDelegatedEvent,
@@ -998,7 +1001,10 @@ export function diffHydratedProperties(
1001 ) {
1002 // Validate that the properties correspond to their expected values.
1003 let serverValue;
1001 - const propertyInfo = getPropertyInfo(propKey);
1004 + const propertyInfo =
1005 + isCustomComponentTag && enableCustomElementPropertySupport
1006 + ? null
1007 + : getPropertyInfo(propKey);
1008 if (suppressHydrationWarning) {
1009 // Don't bother comparing. We're ignoring all these warnings.
1010 } else if (
@@ -1031,7 +1037,27 @@ export function diffHydratedProperties(
1037 warnForPropDifference(propKey, serverValue, expectedStyle);
1038 }
1039 }
1034 - } else if (isCustomComponentTag) {
1040 + } else if (
1041 + enableCustomElementPropertySupport &&
1042 + isCustomComponentTag &&
1043 + (propKey === 'offsetParent' ||
1044 + propKey === 'offsetTop' ||
1045 + propKey === 'offsetLeft' ||
1046 + propKey === 'offsetWidth' ||
1047 + propKey === 'offsetHeight' ||
1048 + propKey === 'isContentEditable' ||
1049 + propKey === 'outerText' ||
1050 + propKey === 'outerHTML')
1051 + ) {
1052 + // $FlowFixMe - Should be inferred as not undefined.
1053 + extraAttributeNames.delete(propKey.toLowerCase());
1054 + if (__DEV__) {
1055 + console.error(
1056 + 'Assignment to read-only property will result in a no-op: `%s`',
1057 + propKey,
1058 + );
1059 + }
1060 + } else if (isCustomComponentTag && !enableCustomElementPropertySupport) {
1061 // $FlowFixMe - Should be inferred as not undefined.
1062 extraAttributeNames.delete(propKey.toLowerCase());
1063 serverValue = getValueForAttribute(domElement, propKey, nextProp);
@@ -1084,7 +1110,15 @@ export function diffHydratedProperties(
1110 serverValue = getValueForAttribute(domElement, propKey, nextProp);
1111 }
1112
1087 - if (nextProp !== serverValue && !isMismatchDueToBadCasing) {
1113 + const dontWarnCustomElement =
1114 + enableCustomElementPropertySupport &&
1115 + isCustomComponentTag &&
1116 + (typeof nextProp === 'function' || typeof nextProp === 'object');
1117 + if (
1118 + !dontWarnCustomElement &&
1119 + nextProp !== serverValue &&
1120 + !isMismatchDueToBadCasing
1121 + ) {
1122 warnForPropDifference(propKey, serverValue, nextProp);
1123 }
1124 }
packages/react-dom/src/client/ReactDOMHostConfig.js
+2 -2
@@ -442,11 +442,11 @@ export function commitUpdate(
442 newProps: Props,
443 internalInstanceHandle: Object,
444 ): void {
445 + // Apply the diff to the DOM node.
446 + updateProperties(domElement, updatePayload, type, oldProps, newProps);
447 // Update the props handle so that we know which props are the ones with
448 // with current event handlers.
449 updateFiberProps(domElement, newProps);
448 - // Apply the diff to the DOM node.
449 - updateProperties(domElement, updatePayload, type, oldProps, newProps);
450 }
451
452 export function resetTextContent(domElement: Instance): void {
packages/react-dom/src/server/ReactDOMServerFormatConfig.js
+19 -2
@@ -17,7 +17,10 @@ import {
17
18 import {Children} from 'react';
19
20 -import {enableFilterEmptyStringAttributesDOM} from 'shared/ReactFeatureFlags';
20 +import {
21 + enableFilterEmptyStringAttributesDOM,
22 + enableCustomElementPropertySupport,
23 +} from 'shared/ReactFeatureFlags';
24
25 import type {
26 Destination,
@@ -1115,12 +1118,26 @@ function pushStartCustomElement(
1118
1119 let children = null;
1120 let innerHTML = null;
1118 - for (const propKey in props) {
1121 + for (let propKey in props) {
1122 if (hasOwnProperty.call(props, propKey)) {
1123 const propValue = props[propKey];
1124 if (propValue == null) {
1125 continue;
1126 }
1127 + if (
1128 + enableCustomElementPropertySupport &&
1129 + (typeof propValue === 'function' || typeof propValue === 'object')
1130 + ) {
1131 + // It is normal to render functions and objects on custom elements when
1132 + // client rendering, but when server rendering the output isn't useful,
1133 + // so skip it.
1134 + continue;
1135 + }
1136 + if (enableCustomElementPropertySupport && propKey === 'className') {
1137 + // className gets rendered as class on the client, so it should be
1138 + // rendered as class on the server.
1139 + propKey = 'class';
1140 + }
1141 switch (propKey) {
1142 case 'children':
1143 children = propValue;
packages/react-dom/src/shared/DOMProperty.js
+7 -1
@@ -7,7 +7,10 @@
7 * @flow
8 */
9
10 -import {enableFilterEmptyStringAttributesDOM} from 'shared/ReactFeatureFlags';
10 +import {
11 + enableFilterEmptyStringAttributesDOM,
12 + enableCustomElementPropertySupport,
13 +} from 'shared/ReactFeatureFlags';
14 import hasOwnProperty from 'shared/hasOwnProperty';
15
16 type PropertyType = 0 | 1 | 2 | 3 | 4 | 5 | 6;
@@ -247,6 +250,9 @@ const reservedProps = [
250 'suppressHydrationWarning',
251 'style',
252 ];
253 +if (enableCustomElementPropertySupport) {
254 + reservedProps.push('innerText', 'textContent');
255 +}
256
257 reservedProps.forEach(name => {
258 properties[name] = new PropertyInfoRecord(
packages/shared/ReactFeatureFlags.js
+6
@@ -140,6 +140,12 @@ export const deletedTreeCleanUpLevel = 3;
140 // Note that this should be an uncommon use case and can be avoided by using the transition API.
141 export const enableSuspenseLayoutEffectSemantics = true;
142
143 +// Changes the behavior for rendering custom elements in both server rendering
144 +// and client rendering, mostly to allow JSX attributes to apply to the custom
145 +// element's object properties instead of only HTML attributes.
146 +// https://github.com/facebook/react/issues/11347
147 +export const enableCustomElementPropertySupport = __EXPERIMENTAL__;
148 +
149 // --------------------------
150 // Future APIs to be deprecated
151 // --------------------------
packages/shared/forks/ReactFeatureFlags.native-fb.js
+1
@@ -72,6 +72,7 @@ export const disableSchedulerTimeoutInWorkLoop = false;
72 export const enableLazyContextPropagation = false;
73 export const enableSyncDefaultUpdates = true;
74 export const allowConcurrentByDefault = true;
75 +export const enableCustomElementPropertySupport = false;
76
77 export const consoleManagedByDevToolsDuringStrictMode = false;
78 export const enableUseMutableSource = true;
packages/shared/forks/ReactFeatureFlags.native-oss.js
+1
@@ -64,6 +64,7 @@ export const enableLazyContextPropagation = false;
64 export const enableSyncDefaultUpdates = true;
65 export const allowConcurrentByDefault = false;
66 export const enablePersistentOffscreenHostContainer = false;
67 +export const enableCustomElementPropertySupport = false;
68
69 export const consoleManagedByDevToolsDuringStrictMode = false;
70 export const enableUseMutableSource = false;
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+1
@@ -64,6 +64,7 @@ export const enableLazyContextPropagation = false;
64 export const enableSyncDefaultUpdates = true;
65 export const allowConcurrentByDefault = false;
66 export const enablePersistentOffscreenHostContainer = false;
67 +export const enableCustomElementPropertySupport = false;
68
69 export const consoleManagedByDevToolsDuringStrictMode = false;
70 export const enableUseMutableSource = false;
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+1
@@ -64,6 +64,7 @@ export const enableLazyContextPropagation = false;
64 export const enableSyncDefaultUpdates = true;
65 export const allowConcurrentByDefault = true;
66 export const enablePersistentOffscreenHostContainer = false;
67 +export const enableCustomElementPropertySupport = false;
68
69 export const consoleManagedByDevToolsDuringStrictMode = false;
70
packages/shared/forks/ReactFeatureFlags.testing.js
+1
@@ -64,6 +64,7 @@ export const enableLazyContextPropagation = false;
64 export const enableSyncDefaultUpdates = true;
65 export const allowConcurrentByDefault = false;
66 export const enablePersistentOffscreenHostContainer = false;
67 +export const enableCustomElementPropertySupport = false;
68
69 export const consoleManagedByDevToolsDuringStrictMode = false;
70 export const enableUseMutableSource = false;
packages/shared/forks/ReactFeatureFlags.testing.www.js
+1
@@ -64,6 +64,7 @@ export const enableLazyContextPropagation = false;
64 export const enableSyncDefaultUpdates = true;
65 export const allowConcurrentByDefault = true;
66 export const enablePersistentOffscreenHostContainer = false;
67 +export const enableCustomElementPropertySupport = false;
68
69 export const consoleManagedByDevToolsDuringStrictMode = false;
70
packages/shared/forks/ReactFeatureFlags.www.js
+2
@@ -104,6 +104,8 @@ export const consoleManagedByDevToolsDuringStrictMode = true;
104 // Some www surfaces are still using this. Remove once they have been migrated.
105 export const enableUseMutableSource = true;
106
107 +export const enableCustomElementPropertySupport = __EXPERIMENTAL__;
108 +
109 // Flow magic to verify the exports of this file match the original version.
110 // eslint-disable-next-line no-unused-vars
111 type Check<_X, Y: _X, X: Y = _X> = null;