diff --git a/packages/react-dom/src/__tests__/DOMPropertyOperations-test.js b/packages/react-dom/src/__tests__/DOMPropertyOperations-test.js
index 99f92cba9b3..ab5ca9a5495 100644
--- a/packages/react-dom/src/__tests__/DOMPropertyOperations-test.js
+++ b/packages/react-dom/src/__tests__/DOMPropertyOperations-test.js
@@ -22,6 +22,17 @@ describe('DOMPropertyOperations', () => {
ReactDOM = require('react-dom');
});
+ // Sets a value in a way that React doesn't see,
+ // so that a subsequent "change" event will trigger the event handler.
+ const setUntrackedValue = Object.getOwnPropertyDescriptor(
+ HTMLInputElement.prototype,
+ 'value',
+ ).set;
+ const setUntrackedChecked = Object.getOwnPropertyDescriptor(
+ HTMLInputElement.prototype,
+ 'checked',
+ ).set;
+
describe('setValueForProperty', () => {
it('should set values as properties by default', () => {
const container = document.createElement('div');
@@ -280,6 +291,575 @@ describe('DOMPropertyOperations', () => {
expect(syntheticClickEvent.nativeEvent).toBe(nativeClickEvent);
});
+ // @gate enableCustomElementPropertySupport
+ it('custom elements should have working onChange event listeners', () => {
+ let reactChangeEvent = null;
+ const eventHandler = jest.fn(event => (reactChangeEvent = event));
+ const container = document.createElement('div');
+ document.body.appendChild(container);
+ ReactDOM.render(, container);
+ const customElement = container.querySelector('my-custom-element');
+ let expectedHandlerCallCount = 0;
+
+ const changeEvent = new Event('change', {bubbles: true});
+ customElement.dispatchEvent(changeEvent);
+ expectedHandlerCallCount++;
+ expect(eventHandler).toHaveBeenCalledTimes(expectedHandlerCallCount);
+ expect(reactChangeEvent.nativeEvent).toBe(changeEvent);
+
+ // Also make sure that removing and re-adding the event listener works
+ ReactDOM.render(, container);
+ customElement.dispatchEvent(new Event('change', {bubbles: true}));
+ expect(eventHandler).toHaveBeenCalledTimes(expectedHandlerCallCount);
+ ReactDOM.render(, container);
+ customElement.dispatchEvent(new Event('change', {bubbles: true}));
+ expectedHandlerCallCount++;
+ expect(eventHandler).toHaveBeenCalledTimes(expectedHandlerCallCount);
+ });
+
+ it('custom elements should have working onInput event listeners', () => {
+ let reactInputEvent = null;
+ const eventHandler = jest.fn(event => (reactInputEvent = event));
+ const container = document.createElement('div');
+ document.body.appendChild(container);
+ ReactDOM.render(, container);
+ const customElement = container.querySelector('my-custom-element');
+ let expectedHandlerCallCount = 0;
+
+ const inputEvent = new Event('input', {bubbles: true});
+ customElement.dispatchEvent(inputEvent);
+ expectedHandlerCallCount++;
+ expect(eventHandler).toHaveBeenCalledTimes(expectedHandlerCallCount);
+ expect(reactInputEvent.nativeEvent).toBe(inputEvent);
+
+ // Also make sure that removing and re-adding the event listener works
+ ReactDOM.render(, container);
+ customElement.dispatchEvent(new Event('input', {bubbles: true}));
+ expect(eventHandler).toHaveBeenCalledTimes(expectedHandlerCallCount);
+ ReactDOM.render(, container);
+ customElement.dispatchEvent(new Event('input', {bubbles: true}));
+ expectedHandlerCallCount++;
+ expect(eventHandler).toHaveBeenCalledTimes(expectedHandlerCallCount);
+ });
+
+ // @gate enableCustomElementPropertySupport
+ it('custom elements should have separate onInput and onChange handling', () => {
+ const container = document.createElement('div');
+ document.body.appendChild(container);
+ const inputEventHandler = jest.fn();
+ const changeEventHandler = jest.fn();
+ ReactDOM.render(
+ ,
+ container,
+ );
+ const customElement = container.querySelector('my-custom-element');
+
+ customElement.dispatchEvent(new Event('input', {bubbles: true}));
+ expect(inputEventHandler).toHaveBeenCalledTimes(1);
+ expect(changeEventHandler).toHaveBeenCalledTimes(0);
+
+ customElement.dispatchEvent(new Event('change', {bubbles: true}));
+ expect(inputEventHandler).toHaveBeenCalledTimes(1);
+ expect(changeEventHandler).toHaveBeenCalledTimes(1);
+ });
+
+ // @gate enableCustomElementPropertySupport
+ it('custom elements should be able to remove and re-add custom event listeners', () => {
+ const container = document.createElement('div');
+ document.body.appendChild(container);
+ const eventHandler = jest.fn();
+ ReactDOM.render(
+ ,
+ container,
+ );
+
+ const customElement = container.querySelector('my-custom-element');
+ customElement.dispatchEvent(new Event('customevent'));
+ expect(eventHandler).toHaveBeenCalledTimes(1);
+
+ ReactDOM.render(, container);
+ customElement.dispatchEvent(new Event('customevent'));
+ expect(eventHandler).toHaveBeenCalledTimes(1);
+
+ ReactDOM.render(
+ ,
+ container,
+ );
+ customElement.dispatchEvent(new Event('customevent'));
+ expect(eventHandler).toHaveBeenCalledTimes(2);
+ });
+
+ it(' should have the same onChange/onInput/onClick behavior as ', () => {
+ const container = document.createElement('div');
+ document.body.appendChild(container);
+ const regularOnInputHandler = jest.fn();
+ const regularOnChangeHandler = jest.fn();
+ const regularOnClickHandler = jest.fn();
+ const customOnInputHandler = jest.fn();
+ const customOnChangeHandler = jest.fn();
+ const customOnClickHandler = jest.fn();
+ function clearMocks() {
+ regularOnInputHandler.mockClear();
+ regularOnChangeHandler.mockClear();
+ regularOnClickHandler.mockClear();
+ customOnInputHandler.mockClear();
+ customOnChangeHandler.mockClear();
+ customOnClickHandler.mockClear();
+ }
+ ReactDOM.render(
+
+
+
+
,
+ container,
+ );
+
+ const regularInput = container.querySelector(
+ 'input:not([is=my-custom-element])',
+ );
+ const customInput = container.querySelector(
+ 'input[is=my-custom-element]',
+ );
+ expect(regularInput).not.toBe(customInput);
+
+ // Typing should trigger onInput and onChange for both kinds of inputs.
+ clearMocks();
+ setUntrackedValue.call(regularInput, 'hello');
+ regularInput.dispatchEvent(new Event('input', {bubbles: true}));
+ expect(regularOnInputHandler).toHaveBeenCalledTimes(1);
+ expect(regularOnChangeHandler).toHaveBeenCalledTimes(1);
+ expect(regularOnClickHandler).toHaveBeenCalledTimes(0);
+ setUntrackedValue.call(customInput, 'hello');
+ customInput.dispatchEvent(new Event('input', {bubbles: true}));
+ expect(customOnInputHandler).toHaveBeenCalledTimes(1);
+ expect(customOnChangeHandler).toHaveBeenCalledTimes(1);
+ expect(customOnClickHandler).toHaveBeenCalledTimes(0);
+
+ // The native change event itself does not produce extra React events.
+ clearMocks();
+ regularInput.dispatchEvent(new Event('change', {bubbles: true}));
+ expect(regularOnInputHandler).toHaveBeenCalledTimes(0);
+ expect(regularOnChangeHandler).toHaveBeenCalledTimes(0);
+ expect(regularOnClickHandler).toHaveBeenCalledTimes(0);
+ customInput.dispatchEvent(new Event('change', {bubbles: true}));
+ expect(customOnInputHandler).toHaveBeenCalledTimes(0);
+ expect(customOnChangeHandler).toHaveBeenCalledTimes(0);
+ expect(customOnClickHandler).toHaveBeenCalledTimes(0);
+
+ // The click event is handled by both inputs.
+ clearMocks();
+ regularInput.dispatchEvent(new Event('click', {bubbles: true}));
+ expect(regularOnInputHandler).toHaveBeenCalledTimes(0);
+ expect(regularOnChangeHandler).toHaveBeenCalledTimes(0);
+ expect(regularOnClickHandler).toHaveBeenCalledTimes(1);
+ customInput.dispatchEvent(new Event('click', {bubbles: true}));
+ expect(customOnInputHandler).toHaveBeenCalledTimes(0);
+ expect(customOnChangeHandler).toHaveBeenCalledTimes(0);
+ expect(customOnClickHandler).toHaveBeenCalledTimes(1);
+
+ // Typing again should trigger onInput and onChange for both kinds of inputs.
+ clearMocks();
+ setUntrackedValue.call(regularInput, 'goodbye');
+ regularInput.dispatchEvent(new Event('input', {bubbles: true}));
+ expect(regularOnInputHandler).toHaveBeenCalledTimes(1);
+ expect(regularOnChangeHandler).toHaveBeenCalledTimes(1);
+ expect(regularOnClickHandler).toHaveBeenCalledTimes(0);
+ setUntrackedValue.call(customInput, 'goodbye');
+ customInput.dispatchEvent(new Event('input', {bubbles: true}));
+ expect(customOnInputHandler).toHaveBeenCalledTimes(1);
+ expect(customOnChangeHandler).toHaveBeenCalledTimes(1);
+ expect(customOnClickHandler).toHaveBeenCalledTimes(0);
+ });
+
+ it(' should have the same onChange/onInput/onClick behavior as ', () => {
+ const container = document.createElement('div');
+ document.body.appendChild(container);
+ const regularOnInputHandler = jest.fn();
+ const regularOnChangeHandler = jest.fn();
+ const regularOnClickHandler = jest.fn();
+ const customOnInputHandler = jest.fn();
+ const customOnChangeHandler = jest.fn();
+ const customOnClickHandler = jest.fn();
+ function clearMocks() {
+ regularOnInputHandler.mockClear();
+ regularOnChangeHandler.mockClear();
+ regularOnClickHandler.mockClear();
+ customOnInputHandler.mockClear();
+ customOnChangeHandler.mockClear();
+ customOnClickHandler.mockClear();
+ }
+ ReactDOM.render(
+
+
+
+
,
+ container,
+ );
+
+ const regularInput = container.querySelector(
+ 'input:not([is=my-custom-element])',
+ );
+ const customInput = container.querySelector(
+ 'input[is=my-custom-element]',
+ );
+ expect(regularInput).not.toBe(customInput);
+
+ // Clicking should trigger onClick and onChange on both inputs.
+ clearMocks();
+ setUntrackedChecked.call(regularInput, true);
+ regularInput.dispatchEvent(new Event('click', {bubbles: true}));
+ expect(regularOnInputHandler).toHaveBeenCalledTimes(0);
+ expect(regularOnChangeHandler).toHaveBeenCalledTimes(1);
+ expect(regularOnClickHandler).toHaveBeenCalledTimes(1);
+ setUntrackedChecked.call(customInput, true);
+ customInput.dispatchEvent(new Event('click', {bubbles: true}));
+ expect(customOnInputHandler).toHaveBeenCalledTimes(0);
+ expect(customOnChangeHandler).toHaveBeenCalledTimes(1);
+ expect(customOnClickHandler).toHaveBeenCalledTimes(1);
+
+ // The native input event only produces a React onInput event.
+ clearMocks();
+ regularInput.dispatchEvent(new Event('input', {bubbles: true}));
+ expect(regularOnInputHandler).toHaveBeenCalledTimes(1);
+ expect(regularOnChangeHandler).toHaveBeenCalledTimes(0);
+ expect(regularOnClickHandler).toHaveBeenCalledTimes(0);
+ customInput.dispatchEvent(new Event('input', {bubbles: true}));
+ expect(customOnInputHandler).toHaveBeenCalledTimes(1);
+ expect(customOnChangeHandler).toHaveBeenCalledTimes(0);
+ expect(customOnClickHandler).toHaveBeenCalledTimes(0);
+
+ // Clicking again should trigger onClick and onChange on both inputs.
+ clearMocks();
+ setUntrackedChecked.call(regularInput, false);
+ regularInput.dispatchEvent(new Event('click', {bubbles: true}));
+ expect(regularOnInputHandler).toHaveBeenCalledTimes(0);
+ expect(regularOnChangeHandler).toHaveBeenCalledTimes(1);
+ expect(regularOnClickHandler).toHaveBeenCalledTimes(1);
+ setUntrackedChecked.call(customInput, false);
+ customInput.dispatchEvent(new Event('click', {bubbles: true}));
+ expect(customOnInputHandler).toHaveBeenCalledTimes(0);
+ expect(customOnChangeHandler).toHaveBeenCalledTimes(1);
+ expect(customOnClickHandler).toHaveBeenCalledTimes(1);
+ });
+
+ it('