mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Fix change events for custom elements (#22938)
* Bypass react event system for custom elements * Going to try fixing react event system instead * finally got it to call onChange, but perhaps too many times * update test * Removed ReactDOMComponent changes, now works but still doubles for bubbles * Maybe i should only support bubbling events * removed some old stuff * cleaned up changeeventplugin stuff * prettier, lint * removed changeeventplugin stuff * remove unneeded gate for onInput test * Go back to using ChangeEventPlugin * Add input+change test * lint * Move logic to shouldUseChangeEvent * use memoizedProps instead of pendingProps * Run form control behavior before custom element behavior * add bubbling test * forgot to append container to body * add child event target test * expand <input is=...> test expectations * Make tests more realistic * Add extra test * Add missing gating * Actually fix gating Co-authored-by: Dan Abramov <dan.abramov@me.com>
This commit is contained in:
co-authored by
Dan Abramov
parent
d8cfeaf221
commit
05a55a4b09
@@ -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(<my-custom-element onChange={eventHandler} />, 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(<my-custom-element />, container);
|
||||
customElement.dispatchEvent(new Event('change', {bubbles: true}));
|
||||
expect(eventHandler).toHaveBeenCalledTimes(expectedHandlerCallCount);
|
||||
ReactDOM.render(<my-custom-element onChange={eventHandler} />, 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(<my-custom-element onInput={eventHandler} />, 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(<my-custom-element />, container);
|
||||
customElement.dispatchEvent(new Event('input', {bubbles: true}));
|
||||
expect(eventHandler).toHaveBeenCalledTimes(expectedHandlerCallCount);
|
||||
ReactDOM.render(<my-custom-element onInput={eventHandler} />, 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(
|
||||
<my-custom-element
|
||||
onInput={inputEventHandler}
|
||||
onChange={changeEventHandler}
|
||||
/>,
|
||||
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(
|
||||
<my-custom-element oncustomevent={eventHandler} />,
|
||||
container,
|
||||
);
|
||||
|
||||
const customElement = container.querySelector('my-custom-element');
|
||||
customElement.dispatchEvent(new Event('customevent'));
|
||||
expect(eventHandler).toHaveBeenCalledTimes(1);
|
||||
|
||||
ReactDOM.render(<my-custom-element />, container);
|
||||
customElement.dispatchEvent(new Event('customevent'));
|
||||
expect(eventHandler).toHaveBeenCalledTimes(1);
|
||||
|
||||
ReactDOM.render(
|
||||
<my-custom-element oncustomevent={eventHandler} />,
|
||||
container,
|
||||
);
|
||||
customElement.dispatchEvent(new Event('customevent'));
|
||||
expect(eventHandler).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('<input is=...> should have the same onChange/onInput/onClick behavior as <input>', () => {
|
||||
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(
|
||||
<div>
|
||||
<input
|
||||
onInput={regularOnInputHandler}
|
||||
onChange={regularOnChangeHandler}
|
||||
onClick={regularOnClickHandler}
|
||||
/>
|
||||
<input
|
||||
is="my-custom-element"
|
||||
onInput={customOnInputHandler}
|
||||
onChange={customOnChangeHandler}
|
||||
onClick={customOnClickHandler}
|
||||
/>
|
||||
</div>,
|
||||
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('<input type=radio is=...> should have the same onChange/onInput/onClick behavior as <input type=radio>', () => {
|
||||
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(
|
||||
<div>
|
||||
<input
|
||||
type="radio"
|
||||
onInput={regularOnInputHandler}
|
||||
onChange={regularOnChangeHandler}
|
||||
onClick={regularOnClickHandler}
|
||||
/>
|
||||
<input
|
||||
is="my-custom-element"
|
||||
type="radio"
|
||||
onInput={customOnInputHandler}
|
||||
onChange={customOnChangeHandler}
|
||||
onClick={customOnClickHandler}
|
||||
/>
|
||||
</div>,
|
||||
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('<select is=...> should have the same onChange/onInput/onClick behavior as <select>', () => {
|
||||
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(
|
||||
<div>
|
||||
<select
|
||||
onInput={regularOnInputHandler}
|
||||
onChange={regularOnChangeHandler}
|
||||
onClick={regularOnClickHandler}
|
||||
/>
|
||||
<select
|
||||
is="my-custom-element"
|
||||
onInput={customOnInputHandler}
|
||||
onChange={customOnChangeHandler}
|
||||
onClick={customOnClickHandler}
|
||||
/>
|
||||
</div>,
|
||||
container,
|
||||
);
|
||||
|
||||
const regularSelect = container.querySelector(
|
||||
'select:not([is=my-custom-element])',
|
||||
);
|
||||
const customSelect = container.querySelector(
|
||||
'select[is=my-custom-element]',
|
||||
);
|
||||
expect(regularSelect).not.toBe(customSelect);
|
||||
|
||||
// Clicking should only trigger onClick on both inputs.
|
||||
clearMocks();
|
||||
regularSelect.dispatchEvent(new Event('click', {bubbles: true}));
|
||||
expect(regularOnInputHandler).toHaveBeenCalledTimes(0);
|
||||
expect(regularOnChangeHandler).toHaveBeenCalledTimes(0);
|
||||
expect(regularOnClickHandler).toHaveBeenCalledTimes(1);
|
||||
customSelect.dispatchEvent(new Event('click', {bubbles: true}));
|
||||
expect(customOnInputHandler).toHaveBeenCalledTimes(0);
|
||||
expect(customOnChangeHandler).toHaveBeenCalledTimes(0);
|
||||
expect(customOnClickHandler).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Native input event should only trigger onInput on both inputs.
|
||||
clearMocks();
|
||||
regularSelect.dispatchEvent(new Event('input', {bubbles: true}));
|
||||
expect(regularOnInputHandler).toHaveBeenCalledTimes(1);
|
||||
expect(regularOnChangeHandler).toHaveBeenCalledTimes(0);
|
||||
expect(regularOnClickHandler).toHaveBeenCalledTimes(0);
|
||||
customSelect.dispatchEvent(new Event('input', {bubbles: true}));
|
||||
expect(customOnInputHandler).toHaveBeenCalledTimes(1);
|
||||
expect(customOnChangeHandler).toHaveBeenCalledTimes(0);
|
||||
expect(customOnClickHandler).toHaveBeenCalledTimes(0);
|
||||
|
||||
// Native change event should trigger onChange.
|
||||
clearMocks();
|
||||
regularSelect.dispatchEvent(new Event('change', {bubbles: true}));
|
||||
expect(regularOnInputHandler).toHaveBeenCalledTimes(0);
|
||||
expect(regularOnChangeHandler).toHaveBeenCalledTimes(1);
|
||||
expect(regularOnClickHandler).toHaveBeenCalledTimes(0);
|
||||
customSelect.dispatchEvent(new Event('change', {bubbles: true}));
|
||||
expect(customOnInputHandler).toHaveBeenCalledTimes(0);
|
||||
expect(customOnChangeHandler).toHaveBeenCalledTimes(1);
|
||||
expect(customOnClickHandler).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
// @gate enableCustomElementPropertySupport
|
||||
it('onChange/onInput/onClick on div with various types of children', () => {
|
||||
const container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
const onChangeHandler = jest.fn();
|
||||
const onInputHandler = jest.fn();
|
||||
const onClickHandler = jest.fn();
|
||||
function clearMocks() {
|
||||
onChangeHandler.mockClear();
|
||||
onInputHandler.mockClear();
|
||||
onClickHandler.mockClear();
|
||||
}
|
||||
ReactDOM.render(
|
||||
<div
|
||||
onChange={onChangeHandler}
|
||||
onInput={onInputHandler}
|
||||
onClick={onClickHandler}>
|
||||
<my-custom-element />
|
||||
<input />
|
||||
<input is="my-custom-element" />
|
||||
</div>,
|
||||
container,
|
||||
);
|
||||
const customElement = container.querySelector('my-custom-element');
|
||||
const regularInput = container.querySelector(
|
||||
'input:not([is="my-custom-element"])',
|
||||
);
|
||||
const customInput = container.querySelector(
|
||||
'input[is="my-custom-element"]',
|
||||
);
|
||||
expect(regularInput).not.toBe(customInput);
|
||||
|
||||
// Custom element has no special logic for input/change.
|
||||
clearMocks();
|
||||
customElement.dispatchEvent(new Event('input', {bubbles: true}));
|
||||
expect(onChangeHandler).toBeCalledTimes(0);
|
||||
expect(onInputHandler).toBeCalledTimes(1);
|
||||
expect(onClickHandler).toBeCalledTimes(0);
|
||||
customElement.dispatchEvent(new Event('change', {bubbles: true}));
|
||||
expect(onChangeHandler).toBeCalledTimes(1);
|
||||
expect(onInputHandler).toBeCalledTimes(1);
|
||||
expect(onClickHandler).toBeCalledTimes(0);
|
||||
customElement.dispatchEvent(new Event('click', {bubbles: true}));
|
||||
expect(onChangeHandler).toBeCalledTimes(1);
|
||||
expect(onInputHandler).toBeCalledTimes(1);
|
||||
expect(onClickHandler).toBeCalledTimes(1);
|
||||
|
||||
// Regular input treats browser input as onChange.
|
||||
clearMocks();
|
||||
setUntrackedValue.call(regularInput, 'hello');
|
||||
regularInput.dispatchEvent(new Event('input', {bubbles: true}));
|
||||
expect(onChangeHandler).toBeCalledTimes(1);
|
||||
expect(onInputHandler).toBeCalledTimes(1);
|
||||
expect(onClickHandler).toBeCalledTimes(0);
|
||||
regularInput.dispatchEvent(new Event('change', {bubbles: true}));
|
||||
expect(onChangeHandler).toBeCalledTimes(1);
|
||||
expect(onInputHandler).toBeCalledTimes(1);
|
||||
expect(onClickHandler).toBeCalledTimes(0);
|
||||
regularInput.dispatchEvent(new Event('click', {bubbles: true}));
|
||||
expect(onChangeHandler).toBeCalledTimes(1);
|
||||
expect(onInputHandler).toBeCalledTimes(1);
|
||||
expect(onClickHandler).toBeCalledTimes(1);
|
||||
|
||||
// Custom input treats browser input as onChange.
|
||||
clearMocks();
|
||||
setUntrackedValue.call(customInput, 'hello');
|
||||
customInput.dispatchEvent(new Event('input', {bubbles: true}));
|
||||
expect(onChangeHandler).toBeCalledTimes(1);
|
||||
expect(onInputHandler).toBeCalledTimes(1);
|
||||
expect(onClickHandler).toBeCalledTimes(0);
|
||||
customInput.dispatchEvent(new Event('change', {bubbles: true}));
|
||||
expect(onChangeHandler).toBeCalledTimes(1);
|
||||
expect(onInputHandler).toBeCalledTimes(1);
|
||||
expect(onClickHandler).toBeCalledTimes(0);
|
||||
customInput.dispatchEvent(new Event('click', {bubbles: true}));
|
||||
expect(onChangeHandler).toBeCalledTimes(1);
|
||||
expect(onInputHandler).toBeCalledTimes(1);
|
||||
expect(onClickHandler).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('custom element onChange/onInput/onClick with event target input child', () => {
|
||||
const container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
const onChangeHandler = jest.fn();
|
||||
const onInputHandler = jest.fn();
|
||||
const onClickHandler = jest.fn();
|
||||
ReactDOM.render(
|
||||
<my-custom-element
|
||||
onChange={onChangeHandler}
|
||||
onInput={onInputHandler}
|
||||
onClick={onClickHandler}>
|
||||
<input />
|
||||
</my-custom-element>,
|
||||
container,
|
||||
);
|
||||
|
||||
const input = container.querySelector('input');
|
||||
setUntrackedValue.call(input, 'hello');
|
||||
input.dispatchEvent(new Event('input', {bubbles: true}));
|
||||
// Simulated onChange from the child's input event
|
||||
// bubbles to the parent custom element.
|
||||
expect(onChangeHandler).toBeCalledTimes(1);
|
||||
expect(onInputHandler).toBeCalledTimes(1);
|
||||
expect(onClickHandler).toBeCalledTimes(0);
|
||||
// Consequently, the native change event is ignored.
|
||||
input.dispatchEvent(new Event('change', {bubbles: true}));
|
||||
expect(onChangeHandler).toBeCalledTimes(1);
|
||||
expect(onInputHandler).toBeCalledTimes(1);
|
||||
expect(onClickHandler).toBeCalledTimes(0);
|
||||
input.dispatchEvent(new Event('click', {bubbles: true}));
|
||||
expect(onChangeHandler).toBeCalledTimes(1);
|
||||
expect(onInputHandler).toBeCalledTimes(1);
|
||||
expect(onClickHandler).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('custom element onChange/onInput/onClick with event target div child', () => {
|
||||
const container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
const onChangeHandler = jest.fn();
|
||||
const onInputHandler = jest.fn();
|
||||
const onClickHandler = jest.fn();
|
||||
ReactDOM.render(
|
||||
<my-custom-element
|
||||
onChange={onChangeHandler}
|
||||
onInput={onInputHandler}
|
||||
onClick={onClickHandler}>
|
||||
<div />
|
||||
</my-custom-element>,
|
||||
container,
|
||||
);
|
||||
|
||||
const div = container.querySelector('div');
|
||||
div.dispatchEvent(new Event('input', {bubbles: true}));
|
||||
expect(onChangeHandler).toBeCalledTimes(0);
|
||||
expect(onInputHandler).toBeCalledTimes(1);
|
||||
expect(onClickHandler).toBeCalledTimes(0);
|
||||
|
||||
div.dispatchEvent(new Event('change', {bubbles: true}));
|
||||
// React always ignores change event invoked on non-custom and non-input targets.
|
||||
// So change event emitted on a div does not propagate upwards.
|
||||
expect(onChangeHandler).toBeCalledTimes(0);
|
||||
expect(onInputHandler).toBeCalledTimes(1);
|
||||
expect(onClickHandler).toBeCalledTimes(0);
|
||||
|
||||
div.dispatchEvent(new Event('click', {bubbles: true}));
|
||||
expect(onChangeHandler).toBeCalledTimes(0);
|
||||
expect(onInputHandler).toBeCalledTimes(1);
|
||||
expect(onClickHandler).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('div onChange/onInput/onClick with event target div child', () => {
|
||||
const container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
const onChangeHandler = jest.fn();
|
||||
const onInputHandler = jest.fn();
|
||||
const onClickHandler = jest.fn();
|
||||
ReactDOM.render(
|
||||
<div
|
||||
onChange={onChangeHandler}
|
||||
onInput={onInputHandler}
|
||||
onClick={onClickHandler}>
|
||||
<div />
|
||||
</div>,
|
||||
container,
|
||||
);
|
||||
|
||||
const div = container.querySelector('div > div');
|
||||
div.dispatchEvent(new Event('input', {bubbles: true}));
|
||||
expect(onChangeHandler).toBeCalledTimes(0);
|
||||
expect(onInputHandler).toBeCalledTimes(1);
|
||||
expect(onClickHandler).toBeCalledTimes(0);
|
||||
|
||||
div.dispatchEvent(new Event('change', {bubbles: true}));
|
||||
// React always ignores change event invoked on non-custom and non-input targets.
|
||||
// So change event emitted on a div does not propagate upwards.
|
||||
expect(onChangeHandler).toBeCalledTimes(0);
|
||||
expect(onInputHandler).toBeCalledTimes(1);
|
||||
expect(onClickHandler).toBeCalledTimes(0);
|
||||
|
||||
div.dispatchEvent(new Event('click', {bubbles: true}));
|
||||
expect(onChangeHandler).toBeCalledTimes(0);
|
||||
expect(onInputHandler).toBeCalledTimes(1);
|
||||
expect(onClickHandler).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
// @gate enableCustomElementPropertySupport
|
||||
it('custom element onChange/onInput/onClick with event target custom element child', () => {
|
||||
const container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
const onChangeHandler = jest.fn();
|
||||
const onInputHandler = jest.fn();
|
||||
const onClickHandler = jest.fn();
|
||||
ReactDOM.render(
|
||||
<my-custom-element
|
||||
onChange={onChangeHandler}
|
||||
onInput={onInputHandler}
|
||||
onClick={onClickHandler}>
|
||||
<other-custom-element />
|
||||
</my-custom-element>,
|
||||
container,
|
||||
);
|
||||
|
||||
const customChild = container.querySelector('other-custom-element');
|
||||
customChild.dispatchEvent(new Event('input', {bubbles: true}));
|
||||
// There is no simulated onChange, only raw onInput is dispatched.
|
||||
expect(onChangeHandler).toBeCalledTimes(0);
|
||||
expect(onInputHandler).toBeCalledTimes(1);
|
||||
expect(onClickHandler).toBeCalledTimes(0);
|
||||
// The native change event propagates to the parent as onChange.
|
||||
customChild.dispatchEvent(new Event('change', {bubbles: true}));
|
||||
expect(onChangeHandler).toBeCalledTimes(1);
|
||||
expect(onInputHandler).toBeCalledTimes(1);
|
||||
expect(onClickHandler).toBeCalledTimes(0);
|
||||
customChild.dispatchEvent(new Event('click', {bubbles: true}));
|
||||
expect(onChangeHandler).toBeCalledTimes(1);
|
||||
expect(onInputHandler).toBeCalledTimes(1);
|
||||
expect(onClickHandler).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
// @gate enableCustomElementPropertySupport
|
||||
it('custom elements should allow custom events with capture event listeners', () => {
|
||||
const oncustomeventCapture = jest.fn();
|
||||
|
||||
+11
-1
@@ -23,12 +23,16 @@ import {updateValueIfChanged} from '../../client/inputValueTracking';
|
||||
import {setDefaultValue} from '../../client/ReactDOMInput';
|
||||
import {enqueueStateRestore} from '../ReactDOMControlledComponent';
|
||||
|
||||
import {disableInputAttributeSyncing} from 'shared/ReactFeatureFlags';
|
||||
import {
|
||||
disableInputAttributeSyncing,
|
||||
enableCustomElementPropertySupport,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
import {batchedUpdates} from '../ReactDOMUpdateBatching';
|
||||
import {
|
||||
processDispatchQueue,
|
||||
accumulateTwoPhaseListeners,
|
||||
} from '../DOMPluginEventSystem';
|
||||
import isCustomComponent from '../../shared/isCustomComponent';
|
||||
|
||||
function registerEvents() {
|
||||
registerTwoPhaseEvent('onChange', [
|
||||
@@ -292,6 +296,12 @@ function extractEvents(
|
||||
}
|
||||
} else if (shouldUseClickEvent(targetNode)) {
|
||||
getTargetInstFunc = getTargetInstForClickEvent;
|
||||
} else if (
|
||||
enableCustomElementPropertySupport &&
|
||||
targetInst &&
|
||||
isCustomComponent(targetInst.elementType, targetInst.memoizedProps)
|
||||
) {
|
||||
getTargetInstFunc = getTargetInstForChangeEvent;
|
||||
}
|
||||
|
||||
if (getTargetInstFunc) {
|
||||
|
||||
Reference in New Issue
Block a user