@samitouri / QOS-React-2 / commits / 43a70a610d

Limit the meaning of "custom element" to not include `is` (#26524)

This PR has a bunch of surrounding refactoring. See individual commits. The main change is that we no longer special case `typeof is === 'string'` as a special case according to the `enableCustomElementPropertySupport` flag. Effectively this means that you can't use custom properties/events, other than the ones React knows about on `<input is="my-input">` extensions. This is unfortunate but there's too many paths that are forked in inconsistent ways since we fork based on tag name. I think __the solution is to let all React elements set unknown properties/events in the same way as this flag__ but that's a bigger change than this flag implies. Since `is` is not universally supported yet anyway, this doesn't seem like a huge loss. Attributes still work. We still support passing the `is` prop and turn that into the appropriate createElement call. @josepharhar

Sebastian Markbåge committed Mar 30, 2023 at 18:38 UTC 43a70a610dd2cc298ab5592deebfbf8f7bbac127
8 files changed +185 -253
packages/react-dom-bindings/src/client/DOMPropertyOperations.js
+8 -40
@@ -253,10 +253,6 @@ export function getValueForAttributeOnCustomComponent(
253 }
254 return expected === undefined ? undefined : null;
255 }
256 - if (enableCustomElementPropertySupport && name === 'className') {
257 - // className is a special cased property on the server to render as an attribute.
258 - name = 'class';
259 - }
256 const value = node.getAttribute(name);
257
258 if (enableCustomElementPropertySupport) {
@@ -448,11 +444,7 @@ export function setValueForPropertyOnCustomComponent(
444 name: string,
445 value: mixed,
446 ) {
451 - if (
452 - enableCustomElementPropertySupport &&
453 - name[0] === 'o' &&
454 - name[1] === 'n'
455 - ) {
447 + if (name[0] === 'o' && name[1] === 'n') {
448 const useCapture = name.endsWith('Capture');
449 const eventName = name.substr(2, useCapture ? name.length - 9 : undefined);
450
@@ -477,40 +469,16 @@ export function setValueForPropertyOnCustomComponent(
469 }
470 }
471
480 - if (enableCustomElementPropertySupport && name in (node: any)) {
472 + if (name in (node: any)) {
473 (node: any)[name] = value;
474 return;
475 }
476
485 - if (isAttributeNameSafe(name)) {
486 - // shouldRemoveAttribute
487 - if (value === null) {
488 - node.removeAttribute(name);
489 - return;
490 - }
491 - switch (typeof value) {
492 - case 'undefined':
493 - case 'function':
494 - case 'symbol': // eslint-disable-line
495 - node.removeAttribute(name);
496 - return;
497 - case 'boolean': {
498 - if (enableCustomElementPropertySupport) {
499 - if (value === true) {
500 - node.setAttribute(name, '');
501 - return;
502 - }
503 - node.removeAttribute(name);
504 - return;
505 - }
506 - }
507 - }
508 - if (__DEV__) {
509 - checkAttributeStringCoercion(value, name);
510 - }
511 - node.setAttribute(
512 - name,
513 - enableTrustedTypesIntegration ? (value: any) : '' + (value: any),
514 - );
477 + if (value === true) {
478 + node.setAttribute(name, '');
479 + return;
480 }
481 +
482 + // From here, it's the same as any attribute
483 + setValueForAttribute(node, name, value);
484 }
packages/react-dom-bindings/src/client/ReactDOMComponent.js
+39 -178
@@ -15,7 +15,6 @@ import {
15 } from '../events/EventRegistry';
16
17 import {canUseDOM} from 'shared/ExecutionEnvironment';
18 -import hasOwnProperty from 'shared/hasOwnProperty';
18 import {checkHtmlStringCoercion} from 'shared/CheckStringCoercion';
19
20 import {
@@ -59,15 +58,13 @@ import {
58 } from './CSSPropertyOperations';
59 import {HTML_NAMESPACE, getIntrinsicNamespace} from './DOMNamespaces';
60 import {getPropertyInfo} from '../shared/DOMProperty';
62 -import {DOCUMENT_NODE} from './HTMLNodeType';
63 -import isCustomComponent from '../shared/isCustomComponent';
61 +import isCustomElement from '../shared/isCustomElement';
62 import possibleStandardNames from '../shared/possibleStandardNames';
63 import {validateProperties as validateARIAProperties} from '../shared/ReactDOMInvalidARIAHook';
64 import {validateProperties as validateInputProperties} from '../shared/ReactDOMNullInputValuePropHook';
65 import {validateProperties as validateUnknownProperties} from '../shared/ReactDOMUnknownPropertyHook';
66
67 import {
70 - enableTrustedTypesIntegration,
68 enableCustomElementPropertySupport,
69 enableClientRenderFallbackOnTextMismatch,
70 enableHostSingletons,
@@ -79,25 +76,8 @@ import {
76 } from '../events/DOMPluginEventSystem';
77
78 let didWarnInvalidHydration = false;
82 -let didWarnScriptTags = false;
83 -
84 -let warnedUnknownTags: {
85 - [key: string]: boolean,
86 -};
79 let canDiffStyleForHydrationWarning;
88 -
80 if (__DEV__) {
90 - warnedUnknownTags = {
91 - // There are working polyfills for <dialog>. Let people use it.
92 - dialog: true,
93 - // Electron ships a custom <webview> tag to display external web content in
94 - // an isolated frame and process.
95 - // This tag is not present in non Electron environments such as JSDom which
96 - // is often used for testing purposes.
97 - // @see https://electronjs.org/docs/api/webview-tag
98 - webview: true,
99 - };
100 -
81 // IE 11 parses & normalizes the style attribute as opposed to other
82 // browsers. It adds spaces and sorts the properties in some
83 // non-alphabetical order. Handling that would require sorting CSS
@@ -264,14 +244,6 @@ export function checkForUnmatchedText(
244 }
245 }
246
267 -export function getOwnerDocumentFromRootContainer(
268 - rootContainerElement: Element | Document | DocumentFragment,
269 -): Document {
270 - return rootContainerElement.nodeType === DOCUMENT_NODE
271 - ? (rootContainerElement: any)
272 - : rootContainerElement.ownerDocument;
273 -}
274 -
247 function noop() {}
248
249 export function trapClickOnNonInteractiveElement(node: HTMLElement) {
@@ -292,7 +264,7 @@ function setProp(
264 tag: string,
265 key: string,
266 value: mixed,
295 - isCustomComponentTag: boolean,
267 + isCustomElementTag: boolean,
268 props: any,
269 ): void {
270 switch (key) {
@@ -418,8 +390,16 @@ function setProp(
390 warnForInvalidEventListener(key, value);
391 }
392 } else {
421 - if (isCustomComponentTag) {
422 - setValueForPropertyOnCustomComponent(domElement, key, value);
393 + if (isCustomElementTag) {
394 + if (enableCustomElementPropertySupport) {
395 + setValueForPropertyOnCustomComponent(domElement, key, value);
396 + } else {
397 + if (typeof value === 'boolean') {
398 + // Special case before the new flag is on
399 + value = '' + (value: any);
400 + }
401 + setValueForAttribute(domElement, key, value);
402 + }
403 } else {
404 if (
405 // shouldIgnoreAttribute
@@ -443,136 +423,6 @@ function setProp(
423 }
424 }
425
446 -// creates a script element that won't execute
447 -export function createPotentiallyInlineScriptElement(
448 - ownerDocument: Document,
449 -): Element {
450 - // Create the script via .innerHTML so its "parser-inserted" flag is
451 - // set to true and it does not execute
452 - const div = ownerDocument.createElement('div');
453 - if (__DEV__) {
454 - if (enableTrustedTypesIntegration && !didWarnScriptTags) {
455 - console.error(
456 - 'Encountered a script tag while rendering React component. ' +
457 - 'Scripts inside React components are never executed when rendering ' +
458 - 'on the client. Consider using template tag instead ' +
459 - '(https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template).',
460 - );
461 - didWarnScriptTags = true;
462 - }
463 - }
464 - div.innerHTML = '<script><' + '/script>'; // eslint-disable-line
465 - // This is guaranteed to yield a script element.
466 - const firstChild = ((div.firstChild: any): HTMLScriptElement);
467 - const element = div.removeChild(firstChild);
468 - return element;
469 -}
470 -
471 -export function createSelectElement(
472 - props: Object,
473 - ownerDocument: Document,
474 -): Element {
475 - let element;
476 - if (typeof props.is === 'string') {
477 - element = ownerDocument.createElement('select', {is: props.is});
478 - } else {
479 - // Separate else branch instead of using `props.is || undefined` above because of a Firefox bug.
480 - // See discussion in https://github.com/facebook/react/pull/6896
481 - // and discussion in https://bugzilla.mozilla.org/show_bug.cgi?id=1276240
482 - element = ownerDocument.createElement('select');
483 - }
484 - if (props.multiple) {
485 - element.multiple = true;
486 - } else if (props.size) {
487 - // Setting a size greater than 1 causes a select to behave like `multiple=true`, where
488 - // it is possible that no option is selected.
489 - //
490 - // This is only necessary when a select in "single selection mode".
491 - element.size = props.size;
492 - }
493 - return element;
494 -}
495 -
496 -// Creates elements in the HTML namesapce
497 -export function createHTMLElement(
498 - type: string,
499 - props: Object,
500 - ownerDocument: Document,
501 -): Element {
502 - if (__DEV__) {
503 - switch (type) {
504 - case 'script':
505 - case 'select':
506 - console.error(
507 - 'createHTMLElement was called with a "%s" type. This type has special creation logic in React and should use the create function implemented specifically for it. This is a bug in React.',
508 - type,
509 - );
510 - break;
511 - case 'svg':
512 - case 'math':
513 - console.error(
514 - 'createHTMLElement was called with a "%s" type. This type must be created with Document.createElementNS which this method does not implement. This is a bug in React.',
515 - type,
516 - );
517 - }
518 - }
519 -
520 - let isCustomComponentTag;
521 -
522 - let element: Element;
523 - if (__DEV__) {
524 - isCustomComponentTag = isCustomComponent(type, props);
525 - // Should this check be gated by parent namespace? Not sure we want to
526 - // allow <SVG> or <mATH>.
527 - if (!isCustomComponentTag && type !== type.toLowerCase()) {
528 - console.error(
529 - '<%s /> is using incorrect casing. ' +
530 - 'Use PascalCase for React components, ' +
531 - 'or lowercase for HTML elements.',
532 - type,
533 - );
534 - }
535 - }
536 -
537 - if (typeof props.is === 'string') {
538 - element = ownerDocument.createElement(type, {is: props.is});
539 - } else {
540 - // Separate else branch instead of using `props.is || undefined` above because of a Firefox bug.
541 - // See discussion in https://github.com/facebook/react/pull/6896
542 - // and discussion in https://bugzilla.mozilla.org/show_bug.cgi?id=1276240
543 - element = ownerDocument.createElement(type);
544 - }
545 -
546 - if (__DEV__) {
547 - if (
548 - !isCustomComponentTag &&
549 - // $FlowFixMe[method-unbinding]
550 - Object.prototype.toString.call(element) ===
551 - '[object HTMLUnknownElement]' &&
552 - !hasOwnProperty.call(warnedUnknownTags, type)
553 - ) {
554 - warnedUnknownTags[type] = true;
555 - console.error(
556 - 'The tag <%s> is unrecognized in this browser. ' +
557 - 'If you meant to render a React component, start its name with ' +
558 - 'an uppercase letter.',
559 - type,
560 - );
561 - }
562 - }
563 -
564 - return element;
565 -}
566 -
567 -export function createTextNode(
568 - text: string,
569 - rootContainerElement: Element | Document | DocumentFragment,
570 -): Text {
571 - return getOwnerDocumentFromRootContainer(rootContainerElement).createTextNode(
572 - text,
573 - );
574 -}
575 -
426 export function setInitialProperties(
427 domElement: Element,
428 tag: string,
@@ -807,7 +657,6 @@ export function setInitialProperties(
657 }
658 // defaultChecked and defaultValue are ignored by setProp
659 default: {
810 - // TODO: If the `is` prop is specified, this should go through the isCustomComponentTag flow.
660 setProp(domElement, tag, propKey, propValue, false, props);
661 }
662 }
@@ -816,7 +665,7 @@ export function setInitialProperties(
665 }
666 }
667
819 - const isCustomComponentTag = isCustomComponent(tag, props);
668 + const isCustomElementTag = isCustomElement(tag, props);
669 for (const propKey in props) {
670 if (!props.hasOwnProperty(propKey)) {
671 continue;
@@ -825,7 +674,7 @@ export function setInitialProperties(
674 if (propValue == null) {
675 continue;
676 }
828 - setProp(domElement, tag, propKey, propValue, isCustomComponentTag, props);
677 + setProp(domElement, tag, propKey, propValue, isCustomElementTag, props);
678 }
679 }
680
@@ -920,6 +769,13 @@ export function diffProperties(
769 }
770 break;
771 }
772 + case 'is':
773 + if (__DEV__) {
774 + console.error(
775 + 'Cannot update the "is" prop after it has been initialized.',
776 + );
777 + }
778 + // eslint-disable-next-line no-fallthrough
779 default: {
780 (updatePayload = updatePayload || []).push(propKey, nextProp);
781 }
@@ -1095,7 +951,6 @@ export function updateProperties(
951 }
952 // defaultChecked and defaultValue are ignored by setProp
953 default: {
1098 - // TODO: If the `is` prop is specified, this should go through the isCustomComponentTag flow.
954 setProp(domElement, tag, propKey, propValue, false, nextProps);
955 }
956 }
@@ -1104,21 +959,12 @@ export function updateProperties(
959 }
960 }
961
1107 - // TODO: Handle wasCustomComponentTag. Changing "is" isn't valid.
1108 - // const wasCustomComponentTag = isCustomComponent(tag, lastProps);
1109 - const isCustomComponentTag = isCustomComponent(tag, nextProps);
962 + const isCustomElementTag = isCustomElement(tag, nextProps);
963 // Apply the diff.
964 for (let i = 0; i < updatePayload.length; i += 2) {
965 const propKey = updatePayload[i];
966 const propValue = updatePayload[i + 1];
1114 - setProp(
1115 - domElement,
1116 - tag,
1117 - propKey,
1118 - propValue,
1119 - isCustomComponentTag,
1120 - nextProps,
1121 - );
967 + setProp(domElement, tag, propKey, propValue, isCustomElementTag, nextProps);
968 }
969 }
970
@@ -1218,6 +1064,21 @@ function diffHydratedCustomComponent(
1064 continue;
1065 }
1066 // eslint-disable-next-line no-fallthrough
1067 + case 'className':
1068 + if (enableCustomElementPropertySupport) {
1069 + // className is a special cased property on the server to render as an attribute.
1070 + extraAttributeNames.delete('class');
1071 + const serverValue = getValueForAttributeOnCustomComponent(
1072 + domElement,
1073 + 'class',
1074 + nextProp,
1075 + );
1076 + if (nextProp !== serverValue) {
1077 + warnForPropDifference('className', serverValue, nextProp);
1078 + }
1079 + continue;
1080 + }
1081 + // eslint-disable-next-line no-fallthrough
1082 default: {
1083 let ownNamespaceDev = parentNamespaceDev;
1084 if (ownNamespaceDev === HTML_NAMESPACE) {
@@ -1504,7 +1365,7 @@ export function diffHydratedProperties(
1365 extraAttributeNames.add(attributes[i].name);
1366 }
1367 }
1507 - if (isCustomComponent(tag, props)) {
1368 + if (isCustomElement(tag, props)) {
1369 diffHydratedCustomComponent(
1370 domElement,
1371 tag,
packages/react-dom-bindings/src/client/ReactDOMHostConfig.js
+113 -15
@@ -44,10 +44,6 @@ import {
44 export {detachDeletedInstance};
45 import {hasRole} from './DOMAccessibilityRoles';
46 import {
47 - createHTMLElement,
48 - createPotentiallyInlineScriptElement,
49 - createSelectElement,
50 - createTextNode,
47 setInitialProperties,
48 diffProperties,
49 updateProperties,
@@ -59,7 +55,6 @@ import {
55 warnForDeletedHydratableText,
56 warnForInsertedHydratedElement,
57 warnForInsertedHydratedText,
62 - getOwnerDocumentFromRootContainer,
58 } from './ReactDOMComponent';
59 import {getSelectionInformation, restoreSelection} from './ReactInputSelection';
60 import setTextContent from './setTextContent';
@@ -90,6 +85,7 @@ import {
85 enableScopeAPI,
86 enableFloat,
87 enableHostSingletons,
88 + enableTrustedTypesIntegration,
89 } from 'shared/ReactFeatureFlags';
90 import {
91 HostComponent,
@@ -119,6 +115,9 @@ export type Props = {
115 left?: null | number,
116 right?: null | number,
117 top?: null | number,
118 + is?: string,
119 + size?: number,
120 + multiple?: boolean,
121 ...
122 };
123 type RawProps = {
@@ -182,6 +181,14 @@ let selectionInformation: null | SelectionInformation = null;
181
182 export * from 'react-reconciler/src/ReactFiberHostConfigWithNoPersistence';
183
184 +function getOwnerDocumentFromRootContainer(
185 + rootContainerElement: Element | Document | DocumentFragment,
186 +): Document {
187 + return rootContainerElement.nodeType === DOCUMENT_NODE
188 + ? (rootContainerElement: any)
189 + : rootContainerElement.ownerDocument;
190 +}
191 +
192 export function getRootHostContext(
193 rootContainerInstance: Container,
194 ): HostContext {
@@ -285,7 +292,8 @@ export function createHoistableInstance(
292 const ownerDocument = getOwnerDocumentFromRootContainer(
293 rootContainerInstance,
294 );
288 - const domElement: Instance = createHTMLElement(type, props, ownerDocument);
295 +
296 + const domElement: Instance = ownerDocument.createElement(type);
297 precacheFiberNode(internalInstanceHandle, domElement);
298 updateFiberProps(domElement, props);
299 setInitialProperties(domElement, type, props);
@@ -293,6 +301,20 @@ export function createHoistableInstance(
301 return domElement;
302 }
303
304 +let didWarnScriptTags = false;
305 +const warnedUnknownTags: {
306 + [key: string]: boolean,
307 +} = {
308 + // There are working polyfills for <dialog>. Let people use it.
309 + dialog: true,
310 + // Electron ships a custom <webview> tag to display external web content in
311 + // an isolated frame and process.
312 + // This tag is not present in non Electron environments such as JSDom which
313 + // is often used for testing purposes.
314 + // @see https://electronjs.org/docs/api/webview-tag
315 + webview: true,
316 +};
317 +
318 export function createInstance(
319 type: string,
320 props: Props,
@@ -334,20 +356,94 @@ export function createInstance(
356 break;
357 default:
358 switch (type) {
337 - case 'svg':
359 + case 'svg': {
360 domElement = ownerDocument.createElementNS(SVG_NAMESPACE, type);
361 break;
340 - case 'math':
362 + }
363 + case 'math': {
364 domElement = ownerDocument.createElementNS(MATH_NAMESPACE, type);
365 break;
343 - case 'script':
344 - domElement = createPotentiallyInlineScriptElement(ownerDocument);
366 + }
367 + case 'script': {
368 + // Create the script via .innerHTML so its "parser-inserted" flag is
369 + // set to true and it does not execute
370 + const div = ownerDocument.createElement('div');
371 + if (__DEV__) {
372 + if (enableTrustedTypesIntegration && !didWarnScriptTags) {
373 + console.error(
374 + 'Encountered a script tag while rendering React component. ' +
375 + 'Scripts inside React components are never executed when rendering ' +
376 + 'on the client. Consider using template tag instead ' +
377 + '(https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template).',
378 + );
379 + didWarnScriptTags = true;
380 + }
381 + }
382 + div.innerHTML = '<script><' + '/script>'; // eslint-disable-line
383 + // This is guaranteed to yield a script element.
384 + const firstChild = ((div.firstChild: any): HTMLScriptElement);
385 + domElement = div.removeChild(firstChild);
386 break;
346 - case 'select':
347 - domElement = createSelectElement(props, ownerDocument);
387 + }
388 + case 'select': {
389 + if (typeof props.is === 'string') {
390 + domElement = ownerDocument.createElement('select', {is: props.is});
391 + } else {
392 + // Separate else branch instead of using `props.is || undefined` above because of a Firefox bug.
393 + // See discussion in https://github.com/facebook/react/pull/6896
394 + // and discussion in https://bugzilla.mozilla.org/show_bug.cgi?id=1276240
395 + domElement = ownerDocument.createElement('select');
396 + }
397 + if (props.multiple) {
398 + domElement.multiple = true;
399 + } else if (props.size) {
400 + // Setting a size greater than 1 causes a select to behave like `multiple=true`, where
401 + // it is possible that no option is selected.
402 + //
403 + // This is only necessary when a select in "single selection mode".
404 + domElement.size = props.size;
405 + }
406 break;
349 - default:
350 - domElement = createHTMLElement(type, props, ownerDocument);
407 + }
408 + default: {
409 + if (typeof props.is === 'string') {
410 + domElement = ownerDocument.createElement(type, {is: props.is});
411 + } else {
412 + // Separate else branch instead of using `props.is || undefined` above because of a Firefox bug.
413 + // See discussion in https://github.com/facebook/react/pull/6896
414 + // and discussion in https://bugzilla.mozilla.org/show_bug.cgi?id=1276240
415 + domElement = ownerDocument.createElement(type);
416 + }
417 +
418 + if (__DEV__) {
419 + if (type.indexOf('-') === -1) {
420 + // We're not SVG/MathML and we don't have a dash, so we're not a custom element
421 + // Even if you use `is`, these should be of known type and lower case.
422 + if (type !== type.toLowerCase()) {
423 + console.error(
424 + '<%s /> is using incorrect casing. ' +
425 + 'Use PascalCase for React components, ' +
426 + 'or lowercase for HTML elements.',
427 + type,
428 + );
429 + }
430 + if (
431 + // $FlowFixMe[method-unbinding]
432 + Object.prototype.toString.call(domElement) ===
433 + '[object HTMLUnknownElement]' &&
434 + !hasOwnProperty.call(warnedUnknownTags, type)
435 + ) {
436 + warnedUnknownTags[type] = true;
437 + console.error(
438 + 'The tag <%s> is unrecognized in this browser. ' +
439 + 'If you meant to render a React component, start its name with ' +
440 + 'an uppercase letter.',
441 + type,
442 + );
443 + }
444 + }
445 + }
446 + }
447 }
448 }
449 precacheFiberNode(internalInstanceHandle, domElement);
@@ -429,7 +525,9 @@ export function createTextInstance(
525 const hostContextDev = ((hostContext: any): HostContextDev);
526 validateDOMNesting(null, text, hostContextDev.ancestorInfo);
527 }
432 - const textNode: TextInstance = createTextNode(text, rootContainerInstance);
528 + const textNode: TextInstance = getOwnerDocumentFromRootContainer(
529 + rootContainerInstance,
530 + ).createTextNode(text);
531 precacheFiberNode(internalInstanceHandle, textNode);
532 return textNode;
533 }
packages/react-dom-bindings/src/events/plugins/ChangeEventPlugin.js
+2 -2
@@ -35,7 +35,7 @@ import {
35 processDispatchQueue,
36 accumulateTwoPhaseListeners,
37 } from '../DOMPluginEventSystem';
38 -import isCustomComponent from '../../shared/isCustomComponent';
38 +import isCustomElement from '../../shared/isCustomElement';
39
40 function registerEvents() {
41 registerTwoPhaseEvent('onChange', [
@@ -312,7 +312,7 @@ function extractEvents(
312 } else if (
313 enableCustomElementPropertySupport &&
314 targetInst &&
315 - isCustomComponent(targetInst.elementType, targetInst.memoizedProps)
315 + isCustomElement(targetInst.elementType, targetInst.memoizedProps)
316 ) {
317 getTargetInstFunc = getTargetInstForChangeEvent;
318 }
packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js
+2 -6
@@ -2483,11 +2483,7 @@ export function pushStartInstance(
2483 formatContext.insertionMode !== SVG_MODE &&
2484 formatContext.insertionMode !== MATHML_MODE
2485 ) {
2486 - if (
2487 - type.indexOf('-') === -1 &&
2488 - typeof props.is !== 'string' &&
2489 - type.toLowerCase() !== type
2490 - ) {
2486 + if (type.indexOf('-') === -1 && type.toLowerCase() !== type) {
2487 console.error(
2488 '<%s /> is using incorrect casing. ' +
2489 'Use PascalCase for React components, ' +
@@ -2608,7 +2604,7 @@ export function pushStartInstance(
2604 );
2605 }
2606 default: {
2611 - if (type.indexOf('-') === -1 && typeof props.is !== 'string') {
2607 + if (type.indexOf('-') === -1) {
2608 // Generic element
2609 return pushStartGenericElement(target, props, type);
2610 } else {
packages/react-dom-bindings/src/shared/ReactDOMUnknownPropertyHook.js
+2 -2
@@ -7,7 +7,7 @@
7
8 import {BOOLEAN, getPropertyInfo} from './DOMProperty';
9 import {ATTRIBUTE_NAME_CHAR} from './isAttributeNameSafe';
10 -import isCustomComponent from './isCustomComponent';
10 +import isCustomElement from './isCustomElement';
11 import possibleStandardNames from './possibleStandardNames';
12 import hasOwnProperty from 'shared/hasOwnProperty';
13 import {enableCustomElementPropertySupport} from 'shared/ReactFeatureFlags';
@@ -308,7 +308,7 @@ function warnUnknownProperties(type, props, eventRegistry) {
308 }
309
310 export function validateProperties(type, props, eventRegistry) {
311 - if (isCustomComponent(type, props)) {
311 + if (isCustomElement(type, props) || typeof props.is === 'string') {
312 return;
313 }
314 warnUnknownProperties(type, props, eventRegistry);
packages/react-dom-bindings/src/shared/isCustomElement.js renamed
+3 -3
@@ -7,9 +7,9 @@
7 * @flow
8 */
9
10 -function isCustomComponent(tagName: string, props: Object): boolean {
10 +function isCustomElement(tagName: string, props: Object): boolean {
11 if (tagName.indexOf('-') === -1) {
12 - return typeof props.is === 'string';
12 + return false;
13 }
14 switch (tagName) {
15 // These are reserved SVG and MathML elements.
@@ -30,4 +30,4 @@ function isCustomComponent(tagName: string, props: Object): boolean {
30 }
31 }
32
33 -export default isCustomComponent;
33 +export default isCustomElement;
packages/react-dom/src/__tests__/ReactDOMServerIntegrationAttributes-test.js
+16 -7
@@ -34,7 +34,7 @@ function initModules() {
34 };
35 }
36
37 -const {resetModules, itRenders, clientCleanRender, clientRenderOnServerString} =
37 +const {resetModules, itRenders, clientCleanRender} =
38 ReactDOMServerIntegrationUtils(initModules);
39
40 describe('ReactDOMServerIntegration', () => {
@@ -655,22 +655,31 @@ describe('ReactDOMServerIntegration', () => {
655 expect(e.getAttribute('class')).toBe('test');
656 });
657
658 + itRenders('className for is elements', async render => {
659 + const e = await render(<div is="custom-element" className="test" />, 0);
660 + expect(e.getAttribute('className')).toBe(null);
661 + expect(e.getAttribute('class')).toBe('test');
662 + });
663 +
664 itRenders('className for custom elements', async render => {
665 + const e = await render(<custom-element className="test" />, 0);
666 if (ReactFeatureFlags.enableCustomElementPropertySupport) {
660 - const e = await render(
661 - <div is="custom-element" className="test" />,
662 - render === clientRenderOnServerString ? 1 : 0,
663 - );
667 expect(e.getAttribute('className')).toBe(null);
668 expect(e.getAttribute('class')).toBe('test');
669 } else {
667 - const e = await render(<div is="custom-element" className="test" />, 0);
670 expect(e.getAttribute('className')).toBe('test');
671 + expect(e.getAttribute('class')).toBe(null);
672 }
673 });
674
672 - itRenders('htmlFor attribute on custom elements', async render => {
675 + itRenders('htmlFor property on is elements', async render => {
676 const e = await render(<div is="custom-element" htmlFor="test" />);
677 + expect(e.getAttribute('htmlFor')).toBe(null);
678 + expect(e.getAttribute('for')).toBe('test');
679 + });
680 +
681 + itRenders('htmlFor attribute on custom elements', async render => {
682 + const e = await render(<custom-element htmlFor="test" />);
683 expect(e.getAttribute('htmlFor')).toBe('test');
684 expect(e.getAttribute('for')).toBe(null);
685 });