diff --git a/packages/react-dom/src/__tests__/DOMPropertyOperations-test.js b/packages/react-dom/src/__tests__/DOMPropertyOperations-test.js index 2959274d1c865..28776b1eb62fb 100644 --- a/packages/react-dom/src/__tests__/DOMPropertyOperations-test.js +++ b/packages/react-dom/src/__tests__/DOMPropertyOperations-test.js @@ -155,6 +155,355 @@ describe('DOMPropertyOperations', () => { // Regression test for https://github.com/facebook/react/issues/6119 expect(container.firstChild.hasAttribute('value')).toBe(false); }); + + // @gate enableCustomElementPropertySupport + it('custom element custom events lowercase', () => { + const oncustomevent = jest.fn(); + function Test() { + return ; + } + const container = document.createElement('div'); + ReactDOM.render(, container); + container + .querySelector('my-custom-element') + .dispatchEvent(new Event('customevent')); + expect(oncustomevent).toHaveBeenCalledTimes(1); + }); + + // @gate enableCustomElementPropertySupport + it('custom element custom events uppercase', () => { + const oncustomevent = jest.fn(); + function Test() { + return ; + } + const container = document.createElement('div'); + ReactDOM.render(, container); + container + .querySelector('my-custom-element') + .dispatchEvent(new Event('Customevent')); + expect(oncustomevent).toHaveBeenCalledTimes(1); + }); + + // @gate enableCustomElementPropertySupport + it('custom element custom event with dash in name', () => { + const oncustomevent = jest.fn(); + function Test() { + return ; + } + const container = document.createElement('div'); + ReactDOM.render(, container); + container + .querySelector('my-custom-element') + .dispatchEvent(new Event('custom-event')); + expect(oncustomevent).toHaveBeenCalledTimes(1); + }); + + // @gate enableCustomElementPropertySupport + it('custom element remove event handler', () => { + const oncustomevent = jest.fn(); + function Test(props) { + return ; + } + + const container = document.createElement('div'); + ReactDOM.render(, container); + const customElement = container.querySelector('my-custom-element'); + customElement.dispatchEvent(new Event('customevent')); + expect(oncustomevent).toHaveBeenCalledTimes(1); + + ReactDOM.render(, container); + // Make sure that the second render didn't create a new element. We want + // to make sure removeEventListener actually gets called on the same element. + expect(customElement).toBe(customElement); + customElement.dispatchEvent(new Event('customevent')); + expect(oncustomevent).toHaveBeenCalledTimes(1); + + ReactDOM.render(, container); + customElement.dispatchEvent(new Event('customevent')); + expect(oncustomevent).toHaveBeenCalledTimes(2); + + const oncustomevent2 = jest.fn(); + ReactDOM.render(, container); + customElement.dispatchEvent(new Event('customevent')); + expect(oncustomevent).toHaveBeenCalledTimes(2); + expect(oncustomevent2).toHaveBeenCalledTimes(1); + }); + + it('custom elements shouldnt have non-functions for on* attributes treated as event listeners', () => { + const container = document.createElement('div'); + ReactDOM.render( + , + container, + ); + const customElement = container.querySelector('my-custom-element'); + expect(customElement.getAttribute('onstring')).toBe('hello'); + expect(customElement.getAttribute('onobj')).toBe('[object Object]'); + expect(customElement.getAttribute('onarray')).toBe('one,two'); + expect(customElement.getAttribute('ontrue')).toBe('true'); + expect(customElement.getAttribute('onfalse')).toBe('false'); + + // Dispatch the corresponding event names to make sure that nothing crashes. + customElement.dispatchEvent(new Event('string')); + customElement.dispatchEvent(new Event('obj')); + customElement.dispatchEvent(new Event('array')); + customElement.dispatchEvent(new Event('true')); + customElement.dispatchEvent(new Event('false')); + }); + + it('custom elements should still have onClick treated like regular elements', () => { + let syntheticClickEvent = null; + const syntheticEventHandler = jest.fn( + event => (syntheticClickEvent = event), + ); + let nativeClickEvent = null; + const nativeEventHandler = jest.fn(event => (nativeClickEvent = event)); + function Test() { + return ; + } + + const container = document.createElement('div'); + document.body.appendChild(container); + ReactDOM.render(, container); + + const customElement = container.querySelector('my-custom-element'); + customElement.onclick = nativeEventHandler; + container.querySelector('my-custom-element').click(); + + expect(nativeEventHandler).toHaveBeenCalledTimes(1); + expect(syntheticEventHandler).toHaveBeenCalledTimes(1); + expect(syntheticClickEvent.nativeEvent).toBe(nativeClickEvent); + }); + + // @gate enableCustomElementPropertySupport + it('custom elements should allow custom events with capture event listeners', () => { + const oncustomeventCapture = jest.fn(); + const oncustomevent = jest.fn(); + function Test() { + return ( + +
+ + ); + } + const container = document.createElement('div'); + ReactDOM.render(, container); + container + .querySelector('my-custom-element > div') + .dispatchEvent(new Event('customevent', {bubbles: false})); + expect(oncustomeventCapture).toHaveBeenCalledTimes(1); + expect(oncustomevent).toHaveBeenCalledTimes(0); + }); + + it('innerHTML should not work on custom elements', () => { + const container = document.createElement('div'); + ReactDOM.render(, container); + const customElement = container.querySelector('my-custom-element'); + expect(customElement.getAttribute('innerHTML')).toBe(null); + expect(customElement.hasChildNodes()).toBe(false); + + // Render again to verify the update codepath doesn't accidentally let + // something through. + ReactDOM.render(, container); + expect(customElement.getAttribute('innerHTML')).toBe(null); + expect(customElement.hasChildNodes()).toBe(false); + }); + + // @gate enableCustomElementPropertySupport + it('innerText should not work on custom elements', () => { + const container = document.createElement('div'); + ReactDOM.render(, container); + const customElement = container.querySelector('my-custom-element'); + expect(customElement.getAttribute('innerText')).toBe(null); + expect(customElement.hasChildNodes()).toBe(false); + + // Render again to verify the update codepath doesn't accidentally let + // something through. + ReactDOM.render(, container); + expect(customElement.getAttribute('innerText')).toBe(null); + expect(customElement.hasChildNodes()).toBe(false); + }); + + // @gate enableCustomElementPropertySupport + it('textContent should not work on custom elements', () => { + const container = document.createElement('div'); + ReactDOM.render(, container); + const customElement = container.querySelector('my-custom-element'); + expect(customElement.getAttribute('textContent')).toBe(null); + expect(customElement.hasChildNodes()).toBe(false); + + // Render again to verify the update codepath doesn't accidentally let + // something through. + ReactDOM.render(, container); + expect(customElement.getAttribute('textContent')).toBe(null); + expect(customElement.hasChildNodes()).toBe(false); + }); + + // @gate enableCustomElementPropertySupport + it('values should not be converted to booleans when assigning into custom elements', () => { + const container = document.createElement('div'); + document.body.appendChild(container); + ReactDOM.render(, container); + const customElement = container.querySelector('my-custom-element'); + customElement.foo = null; + + // true => string + ReactDOM.render(, container); + expect(customElement.foo).toBe(true); + ReactDOM.render(, container); + expect(customElement.foo).toBe('bar'); + + // false => string + ReactDOM.render(, container); + expect(customElement.foo).toBe(false); + ReactDOM.render(, container); + expect(customElement.foo).toBe('bar'); + + // true => null + ReactDOM.render(, container); + expect(customElement.foo).toBe(true); + ReactDOM.render(, container); + expect(customElement.foo).toBe(null); + + // false => null + ReactDOM.render(, container); + expect(customElement.foo).toBe(false); + ReactDOM.render(, container); + expect(customElement.foo).toBe(null); + }); + + // @gate enableCustomElementPropertySupport + it('custom element custom event handlers assign multiple types', () => { + const container = document.createElement('div'); + document.body.appendChild(container); + const oncustomevent = jest.fn(); + + // First render with string + ReactDOM.render(, container); + const customelement = container.querySelector('my-custom-element'); + customelement.dispatchEvent(new Event('customevent')); + expect(oncustomevent).toHaveBeenCalledTimes(0); + expect(customelement.oncustomevent).toBe(undefined); + expect(customelement.getAttribute('oncustomevent')).toBe('foo'); + + // string => event listener + ReactDOM.render( + , + container, + ); + customelement.dispatchEvent(new Event('customevent')); + expect(oncustomevent).toHaveBeenCalledTimes(1); + expect(customelement.oncustomevent).toBe(undefined); + expect(customelement.getAttribute('oncustomevent')).toBe(null); + + // event listener => string + ReactDOM.render(, container); + customelement.dispatchEvent(new Event('customevent')); + expect(oncustomevent).toHaveBeenCalledTimes(1); + expect(customelement.oncustomevent).toBe(undefined); + expect(customelement.getAttribute('oncustomevent')).toBe('foo'); + + // string => nothing + ReactDOM.render(, container); + customelement.dispatchEvent(new Event('customevent')); + expect(oncustomevent).toHaveBeenCalledTimes(1); + expect(customelement.oncustomevent).toBe(undefined); + expect(customelement.getAttribute('oncustomevent')).toBe(null); + + // nothing => event listener + ReactDOM.render( + , + container, + ); + customelement.dispatchEvent(new Event('customevent')); + expect(oncustomevent).toHaveBeenCalledTimes(2); + expect(customelement.oncustomevent).toBe(undefined); + expect(customelement.getAttribute('oncustomevent')).toBe(null); + }); + + // @gate enableCustomElementPropertySupport + it('custom element custom event handlers assign multiple types with setter', () => { + const container = document.createElement('div'); + document.body.appendChild(container); + const oncustomevent = jest.fn(); + + // First render with nothing + ReactDOM.render(, container); + const customelement = container.querySelector('my-custom-element'); + // Install a setter to activate the `in` heuristic + Object.defineProperty(customelement, 'oncustomevent', { + set: function(x) { + this._oncustomevent = x; + }, + get: function() { + return this._oncustomevent; + }, + }); + expect(customelement.oncustomevent).toBe(undefined); + + // nothing => event listener + ReactDOM.render( + , + container, + ); + customelement.dispatchEvent(new Event('customevent')); + expect(oncustomevent).toHaveBeenCalledTimes(1); + expect(customelement.oncustomevent).toBe(null); + expect(customelement.getAttribute('oncustomevent')).toBe(null); + + // event listener => string + ReactDOM.render(, container); + customelement.dispatchEvent(new Event('customevent')); + expect(oncustomevent).toHaveBeenCalledTimes(1); + expect(customelement.oncustomevent).toBe('foo'); + expect(customelement.getAttribute('oncustomevent')).toBe(null); + + // string => event listener + ReactDOM.render( + , + container, + ); + customelement.dispatchEvent(new Event('customevent')); + expect(oncustomevent).toHaveBeenCalledTimes(2); + expect(customelement.oncustomevent).toBe(null); + expect(customelement.getAttribute('oncustomevent')).toBe(null); + + // event listener => nothing + ReactDOM.render(, container); + customelement.dispatchEvent(new Event('customevent')); + expect(oncustomevent).toHaveBeenCalledTimes(2); + expect(customelement.oncustomevent).toBe(null); + expect(customelement.getAttribute('oncustomevent')).toBe(null); + }); + + // @gate enableCustomElementPropertySupport + it('assigning to a custom element property should not remove attributes', () => { + const container = document.createElement('div'); + document.body.appendChild(container); + ReactDOM.render(, container); + const customElement = container.querySelector('my-custom-element'); + expect(customElement.getAttribute('foo')).toBe('one'); + + // Install a setter to activate the `in` heuristic + Object.defineProperty(customElement, 'foo', { + set: function(x) { + this._foo = x; + }, + get: function() { + return this._foo; + }, + }); + ReactDOM.render(, container); + expect(customElement.foo).toBe('two'); + expect(customElement.getAttribute('foo')).toBe('one'); + }); }); describe('deleteValueForProperty', () => { diff --git a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationAttributes-test.js b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationAttributes-test.js index c1c23e2185842..b32146d55e44e 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationAttributes-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationAttributes-test.js @@ -10,6 +10,7 @@ 'use strict'; const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegrationTestUtils'); +const ReactFeatureFlags = require('shared/ReactFeatureFlags'); let React; let ReactDOM; @@ -36,6 +37,7 @@ const { resetModules, itRenders, clientCleanRender, + clientRenderOnServerString, } = ReactDOMServerIntegrationUtils(initModules); describe('ReactDOMServerIntegration', () => { @@ -657,17 +659,28 @@ describe('ReactDOMServerIntegration', () => { }); itRenders('className for custom elements', async render => { - const e = await render(
, 0); - expect(e.getAttribute('className')).toBe('test'); + if (ReactFeatureFlags.enableCustomElementPropertySupport) { + const e = await render( +
, + render === clientRenderOnServerString ? 1 : 0, + ); + expect(e.getAttribute('className')).toBe(null); + expect(e.getAttribute('class')).toBe('test'); + } else { + const e = await render(
, 0); + expect(e.getAttribute('className')).toBe('test'); + } }); itRenders('htmlFor attribute on custom elements', async render => { const e = await render(
); expect(e.getAttribute('htmlFor')).toBe('test'); + expect(e.getAttribute('for')).toBe(null); }); itRenders('for attribute on custom elements', async render => { const e = await render(
); + expect(e.getAttribute('htmlFor')).toBe(null); expect(e.getAttribute('for')).toBe('test'); }); diff --git a/packages/react-dom/src/__tests__/ReactServerRendering-test.js b/packages/react-dom/src/__tests__/ReactServerRendering-test.js index b39d7c8ff5173..6489151b05504 100644 --- a/packages/react-dom/src/__tests__/ReactServerRendering-test.js +++ b/packages/react-dom/src/__tests__/ReactServerRendering-test.js @@ -1097,4 +1097,43 @@ describe('ReactDOMServer', () => { 'However, it is set to a string.', ); }); + + describe('custom element server rendering', () => { + it('String properties should be server rendered for custom elements', () => { + const output = ReactDOMServer.renderToString( + , + ); + expect(output).toBe(``); + }); + + it('Number properties should be server rendered for custom elements', () => { + const output = ReactDOMServer.renderToString( + , + ); + expect(output).toBe(``); + }); + + // @gate enableCustomElementPropertySupport + it('Object properties should not be server rendered for custom elements', () => { + const output = ReactDOMServer.renderToString( + , + ); + expect(output).toBe(``); + }); + + // @gate enableCustomElementPropertySupport + it('Array properties should not be server rendered for custom elements', () => { + const output = ReactDOMServer.renderToString( + , + ); + expect(output).toBe(``); + }); + + it('Function properties should not be server rendered for custom elements', () => { + const output = ReactDOMServer.renderToString( + console.log('bar')} />, + ); + expect(output).toBe(``); + }); + }); }); diff --git a/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js b/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js index 0069946767252..3d63954a0857f 100644 --- a/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js +++ b/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js @@ -530,4 +530,70 @@ describe('ReactDOMServerHydration', () => { 'Warning: Did not expect server HTML to contain a

in

', ); }); + + it('should warn when hydrating read-only properties', () => { + const readOnlyProperties = [ + 'offsetParent', + 'offsetTop', + 'offsetLeft', + 'offsetWidth', + 'offsetHeight', + 'isContentEditable', + 'outerText', + 'outerHTML', + ]; + readOnlyProperties.forEach(readOnlyProperty => { + const props = {}; + props[readOnlyProperty] = 'hello'; + const jsx = React.createElement('my-custom-element', props); + const element = document.createElement('div'); + element.innerHTML = ReactDOMServer.renderToString(jsx); + if (gate(flags => flags.enableCustomElementPropertySupport)) { + expect(() => ReactDOM.hydrate(jsx, element)).toErrorDev( + `Warning: Assignment to read-only property will result in a no-op: \`${readOnlyProperty}\``, + ); + } else { + ReactDOM.hydrate(jsx, element); + } + }); + }); + + // @gate enableCustomElementPropertySupport + it('should not re-assign properties on hydration', () => { + const container = document.createElement('div'); + document.body.appendChild(container); + + const jsx = React.createElement('my-custom-element', { + str: 'string', + obj: {foo: 'bar'}, + }); + + container.innerHTML = ReactDOMServer.renderToString(jsx); + const customElement = container.querySelector('my-custom-element'); + + // Install setters to activate `in` check + Object.defineProperty(customElement, 'str', { + set: function(x) { + this._str = x; + }, + get: function() { + return this._str; + }, + }); + Object.defineProperty(customElement, 'obj', { + set: function(x) { + this._obj = x; + }, + get: function() { + return this._obj; + }, + }); + + ReactDOM.hydrate(jsx, container); + + expect(customElement.getAttribute('str')).toBe('string'); + expect(customElement.getAttribute('obj')).toBe(null); + expect(customElement.str).toBe(undefined); + expect(customElement.obj).toBe(undefined); + }); }); diff --git a/packages/react-dom/src/client/DOMPropertyOperations.js b/packages/react-dom/src/client/DOMPropertyOperations.js index ad94f7ccb7026..71b53d703c6d4 100644 --- a/packages/react-dom/src/client/DOMPropertyOperations.js +++ b/packages/react-dom/src/client/DOMPropertyOperations.js @@ -19,8 +19,10 @@ import sanitizeURL from '../shared/sanitizeURL'; import { disableJavaScriptURLs, enableTrustedTypesIntegration, + enableCustomElementPropertySupport, } from 'shared/ReactFeatureFlags'; import {checkAttributeStringCoercion} from 'shared/CheckStringCoercion'; +import {getFiberCurrentPropsFromNode} from './ReactDOMComponentTree'; import type {PropertyInfo} from '../shared/DOMProperty'; @@ -149,9 +151,52 @@ export function setValueForProperty( if (shouldIgnoreAttribute(name, propertyInfo, isCustomComponentTag)) { return; } + + if ( + enableCustomElementPropertySupport && + isCustomComponentTag && + name[0] === 'o' && + name[1] === 'n' + ) { + let eventName = name.replace(/Capture$/, ''); + const useCapture = name !== eventName; + eventName = eventName.slice(2); + + const prevProps = getFiberCurrentPropsFromNode(node); + const prevValue = prevProps != null ? prevProps[name] : null; + if (typeof prevValue === 'function') { + node.removeEventListener(eventName, prevValue, useCapture); + } + if (typeof value === 'function') { + if (typeof prevValue !== 'function' && prevValue !== null) { + // If we previously assigned a non-function type into this node, then + // remove it when switching to event listener mode. + if (name in (node: any)) { + (node: any)[name] = null; + } else if (node.hasAttribute(name)) { + node.removeAttribute(name); + } + } + + // $FlowFixMe value can't be casted to EventListener. + node.addEventListener(eventName, (value: EventListener), useCapture); + return; + } + } + if (shouldRemoveAttribute(name, value, propertyInfo, isCustomComponentTag)) { value = null; } + + if ( + enableCustomElementPropertySupport && + isCustomComponentTag && + name in (node: any) + ) { + (node: any)[name] = value; + return; + } + // If the prop isn't in the special list, treat it as a simple attribute. if (isCustomComponentTag || propertyInfo === null) { if (isAttributeNameSafe(name)) { diff --git a/packages/react-dom/src/client/ReactDOMComponent.js b/packages/react-dom/src/client/ReactDOMComponent.js index aa9b0281e2f8d..ffd90c128d462 100644 --- a/packages/react-dom/src/client/ReactDOMComponent.js +++ b/packages/react-dom/src/client/ReactDOMComponent.js @@ -69,7 +69,10 @@ import {validateProperties as validateARIAProperties} from '../shared/ReactDOMIn import {validateProperties as validateInputProperties} from '../shared/ReactDOMNullInputValuePropHook'; import {validateProperties as validateUnknownProperties} from '../shared/ReactDOMUnknownPropertyHook'; -import {enableTrustedTypesIntegration} from 'shared/ReactFeatureFlags'; +import { + enableTrustedTypesIntegration, + enableCustomElementPropertySupport, +} from 'shared/ReactFeatureFlags'; import { mediaEventTypes, listenToNonDelegatedEvent, @@ -998,7 +1001,10 @@ export function diffHydratedProperties( ) { // Validate that the properties correspond to their expected values. let serverValue; - const propertyInfo = getPropertyInfo(propKey); + const propertyInfo = + isCustomComponentTag && enableCustomElementPropertySupport + ? null + : getPropertyInfo(propKey); if (suppressHydrationWarning) { // Don't bother comparing. We're ignoring all these warnings. } else if ( @@ -1031,7 +1037,27 @@ export function diffHydratedProperties( warnForPropDifference(propKey, serverValue, expectedStyle); } } - } else if (isCustomComponentTag) { + } else if ( + enableCustomElementPropertySupport && + isCustomComponentTag && + (propKey === 'offsetParent' || + propKey === 'offsetTop' || + propKey === 'offsetLeft' || + propKey === 'offsetWidth' || + propKey === 'offsetHeight' || + propKey === 'isContentEditable' || + propKey === 'outerText' || + propKey === 'outerHTML') + ) { + // $FlowFixMe - Should be inferred as not undefined. + extraAttributeNames.delete(propKey.toLowerCase()); + if (__DEV__) { + console.error( + 'Assignment to read-only property will result in a no-op: `%s`', + propKey, + ); + } + } else if (isCustomComponentTag && !enableCustomElementPropertySupport) { // $FlowFixMe - Should be inferred as not undefined. extraAttributeNames.delete(propKey.toLowerCase()); serverValue = getValueForAttribute(domElement, propKey, nextProp); @@ -1084,7 +1110,15 @@ export function diffHydratedProperties( serverValue = getValueForAttribute(domElement, propKey, nextProp); } - if (nextProp !== serverValue && !isMismatchDueToBadCasing) { + const dontWarnCustomElement = + enableCustomElementPropertySupport && + isCustomComponentTag && + (typeof nextProp === 'function' || typeof nextProp === 'object'); + if ( + !dontWarnCustomElement && + nextProp !== serverValue && + !isMismatchDueToBadCasing + ) { warnForPropDifference(propKey, serverValue, nextProp); } } diff --git a/packages/react-dom/src/client/ReactDOMHostConfig.js b/packages/react-dom/src/client/ReactDOMHostConfig.js index 64fd789b7a1e8..341d1fa7a3764 100644 --- a/packages/react-dom/src/client/ReactDOMHostConfig.js +++ b/packages/react-dom/src/client/ReactDOMHostConfig.js @@ -442,11 +442,11 @@ export function commitUpdate( newProps: Props, internalInstanceHandle: Object, ): void { + // Apply the diff to the DOM node. + updateProperties(domElement, updatePayload, type, oldProps, newProps); // Update the props handle so that we know which props are the ones with // with current event handlers. updateFiberProps(domElement, newProps); - // Apply the diff to the DOM node. - updateProperties(domElement, updatePayload, type, oldProps, newProps); } export function resetTextContent(domElement: Instance): void { diff --git a/packages/react-dom/src/server/ReactDOMServerFormatConfig.js b/packages/react-dom/src/server/ReactDOMServerFormatConfig.js index c4f8d89a8c907..5ea8d772732bd 100644 --- a/packages/react-dom/src/server/ReactDOMServerFormatConfig.js +++ b/packages/react-dom/src/server/ReactDOMServerFormatConfig.js @@ -17,7 +17,10 @@ import { import {Children} from 'react'; -import {enableFilterEmptyStringAttributesDOM} from 'shared/ReactFeatureFlags'; +import { + enableFilterEmptyStringAttributesDOM, + enableCustomElementPropertySupport, +} from 'shared/ReactFeatureFlags'; import type { Destination, @@ -1115,12 +1118,26 @@ function pushStartCustomElement( let children = null; let innerHTML = null; - for (const propKey in props) { + for (let propKey in props) { if (hasOwnProperty.call(props, propKey)) { const propValue = props[propKey]; if (propValue == null) { continue; } + if ( + enableCustomElementPropertySupport && + (typeof propValue === 'function' || typeof propValue === 'object') + ) { + // It is normal to render functions and objects on custom elements when + // client rendering, but when server rendering the output isn't useful, + // so skip it. + continue; + } + if (enableCustomElementPropertySupport && propKey === 'className') { + // className gets rendered as class on the client, so it should be + // rendered as class on the server. + propKey = 'class'; + } switch (propKey) { case 'children': children = propValue; diff --git a/packages/react-dom/src/shared/DOMProperty.js b/packages/react-dom/src/shared/DOMProperty.js index 83b48555ebd83..278a9fd12b006 100644 --- a/packages/react-dom/src/shared/DOMProperty.js +++ b/packages/react-dom/src/shared/DOMProperty.js @@ -7,7 +7,10 @@ * @flow */ -import {enableFilterEmptyStringAttributesDOM} from 'shared/ReactFeatureFlags'; +import { + enableFilterEmptyStringAttributesDOM, + enableCustomElementPropertySupport, +} from 'shared/ReactFeatureFlags'; import hasOwnProperty from 'shared/hasOwnProperty'; type PropertyType = 0 | 1 | 2 | 3 | 4 | 5 | 6; @@ -247,6 +250,9 @@ const reservedProps = [ 'suppressHydrationWarning', 'style', ]; +if (enableCustomElementPropertySupport) { + reservedProps.push('innerText', 'textContent'); +} reservedProps.forEach(name => { properties[name] = new PropertyInfoRecord( diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index 806fcdeb29d13..c5f798d559ddf 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -140,6 +140,12 @@ export const deletedTreeCleanUpLevel = 3; // Note that this should be an uncommon use case and can be avoided by using the transition API. export const enableSuspenseLayoutEffectSemantics = true; +// Changes the behavior for rendering custom elements in both server rendering +// and client rendering, mostly to allow JSX attributes to apply to the custom +// element's object properties instead of only HTML attributes. +// https://github.com/facebook/react/issues/11347 +export const enableCustomElementPropertySupport = __EXPERIMENTAL__; + // -------------------------- // Future APIs to be deprecated // -------------------------- diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index b1d5d68454788..0f30707a49f3a 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -72,6 +72,7 @@ export const disableSchedulerTimeoutInWorkLoop = false; export const enableLazyContextPropagation = false; export const enableSyncDefaultUpdates = true; export const allowConcurrentByDefault = true; +export const enableCustomElementPropertySupport = false; export const consoleManagedByDevToolsDuringStrictMode = false; export const enableUseMutableSource = true; diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index beb55a13fc0b7..a660772b7e354 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -64,6 +64,7 @@ export const enableLazyContextPropagation = false; export const enableSyncDefaultUpdates = true; export const allowConcurrentByDefault = false; export const enablePersistentOffscreenHostContainer = false; +export const enableCustomElementPropertySupport = false; export const consoleManagedByDevToolsDuringStrictMode = false; export const enableUseMutableSource = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index ba200210122f0..e6c4e5ec21fdd 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -64,6 +64,7 @@ export const enableLazyContextPropagation = false; export const enableSyncDefaultUpdates = true; export const allowConcurrentByDefault = false; export const enablePersistentOffscreenHostContainer = false; +export const enableCustomElementPropertySupport = false; export const consoleManagedByDevToolsDuringStrictMode = false; export const enableUseMutableSource = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index bf87304fae18f..51bbc5a1627df 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -64,6 +64,7 @@ export const enableLazyContextPropagation = false; export const enableSyncDefaultUpdates = true; export const allowConcurrentByDefault = true; export const enablePersistentOffscreenHostContainer = false; +export const enableCustomElementPropertySupport = false; export const consoleManagedByDevToolsDuringStrictMode = false; diff --git a/packages/shared/forks/ReactFeatureFlags.testing.js b/packages/shared/forks/ReactFeatureFlags.testing.js index 3ed2d5956834e..182bebe61441e 100644 --- a/packages/shared/forks/ReactFeatureFlags.testing.js +++ b/packages/shared/forks/ReactFeatureFlags.testing.js @@ -64,6 +64,7 @@ export const enableLazyContextPropagation = false; export const enableSyncDefaultUpdates = true; export const allowConcurrentByDefault = false; export const enablePersistentOffscreenHostContainer = false; +export const enableCustomElementPropertySupport = false; export const consoleManagedByDevToolsDuringStrictMode = false; export const enableUseMutableSource = false; diff --git a/packages/shared/forks/ReactFeatureFlags.testing.www.js b/packages/shared/forks/ReactFeatureFlags.testing.www.js index e96c9fc2023ac..f62dc8af4960d 100644 --- a/packages/shared/forks/ReactFeatureFlags.testing.www.js +++ b/packages/shared/forks/ReactFeatureFlags.testing.www.js @@ -64,6 +64,7 @@ export const enableLazyContextPropagation = false; export const enableSyncDefaultUpdates = true; export const allowConcurrentByDefault = true; export const enablePersistentOffscreenHostContainer = false; +export const enableCustomElementPropertySupport = false; export const consoleManagedByDevToolsDuringStrictMode = false; diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index a46b66fb20db0..cffa189840f8c 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -104,6 +104,8 @@ export const consoleManagedByDevToolsDuringStrictMode = true; // Some www surfaces are still using this. Remove once they have been migrated. export const enableUseMutableSource = true; +export const enableCustomElementPropertySupport = __EXPERIMENTAL__; + // Flow magic to verify the exports of this file match the original version. // eslint-disable-next-line no-unused-vars type Check<_X, Y: _X, X: Y = _X> = null;