Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add custom element property support behind a flag #22184

Merged
merged 43 commits into from
Dec 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
a2f57e0
custom element props
josepharhar Jan 11, 2021
92f1e1c
custom element events
josepharhar Jan 8, 2021
52166d9
use function type for on*
josepharhar Aug 17, 2021
a5bb048
tests, htmlFor
josepharhar Aug 25, 2021
660e770
className
josepharhar Aug 25, 2021
a84a2e6
fix ReactDOMComponent-test
josepharhar Aug 26, 2021
db7e13d
started on adding feature flag
josepharhar Aug 26, 2021
74a7d9d
added feature flag to all feature flag files
josepharhar Aug 26, 2021
7bb6fa4
everything passes
josepharhar Aug 26, 2021
55a1e3c
tried to fix getPropertyInfo
josepharhar Aug 26, 2021
23d406b
used @gate and __experimental__
josepharhar Aug 27, 2021
8a2651b
remove flag gating for test which already passes
josepharhar Aug 27, 2021
ae33345
fix onClick test
josepharhar Aug 31, 2021
9bec8b1
add __EXPERIMENTAL__ to www flags, rename eventProxy
josepharhar Aug 31, 2021
af292bc
Add innerText and textContent to reservedProps
josepharhar Sep 14, 2021
1a093e5
Emit warning when assigning to read only properties in client
josepharhar Sep 28, 2021
9d6d1dd
Revert "Emit warning when assigning to read only properties in client"
josepharhar Sep 29, 2021
dc1e6c2
Emit warning when assigning to read only properties during hydration
josepharhar Sep 29, 2021
6fa57fb
yarn prettier-all
josepharhar Sep 29, 2021
333d3d7
Gate hydration warning test on flag
josepharhar Nov 4, 2021
632c96c
Merge with 2 months of upstream commits
josepharhar Nov 5, 2021
b26e31f
Fix gating in hydration warning test
josepharhar Nov 5, 2021
ed4f899
Fix assignment to boolean properties
josepharhar Nov 5, 2021
4da5c57
Replace _listeners with random suffix matching
josepharhar Nov 6, 2021
91acb79
Improve gating for hydration warning test
josepharhar Nov 6, 2021
3cf8e44
Add outerText and outerHTML to server warning properties
josepharhar Nov 18, 2021
1fe88e2
remove nameLower logic
josepharhar Nov 30, 2021
7e6dc19
fix capture event listener test
josepharhar Nov 30, 2021
7f67c45
Add coverage for changing custom event listeners
josepharhar Nov 30, 2021
97ea2b4
yarn prettier-all
josepharhar Nov 30, 2021
5d641c2
yarn lint --fix
josepharhar Nov 30, 2021
fead37f
replace getCustomElementEventHandlersFromNode with getFiberCurrentPro…
josepharhar Nov 30, 2021
77afc53
Remove previous value when adding event listener
josepharhar Dec 3, 2021
c198d82
flow, lint, prettier
josepharhar Dec 3, 2021
3b0d45b
Add dispatchEvent to make sure nothing crashes
josepharhar Dec 6, 2021
7509c6d
Add state change to reserved attribute tests
josepharhar Dec 6, 2021
a59042e
Add missing feature flag test gate
josepharhar Dec 6, 2021
39b142e
Reimplement SSR changes in ReactDOMServerFormatConfig
josepharhar Dec 7, 2021
1c86699
Test hydration for objects and functions
josepharhar Dec 7, 2021
b043bfb
add missing test gate
josepharhar Dec 7, 2021
37ccabe
remove extraneous comment
josepharhar Dec 7, 2021
8fcf649
Add attribute->property test
josepharhar Dec 7, 2021
4bd3b44
Merge with 4 weeks of upstream commits
josepharhar Dec 7, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
349 changes: 349 additions & 0 deletions packages/react-dom/src/__tests__/DOMPropertyOperations-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 <my-custom-element oncustomevent={oncustomevent} />;
}
const container = document.createElement('div');
ReactDOM.render(<Test />, 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 <my-custom-element onCustomevent={oncustomevent} />;
}
const container = document.createElement('div');
ReactDOM.render(<Test />, 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 <my-custom-element oncustom-event={oncustomevent} />;
}
const container = document.createElement('div');
ReactDOM.render(<Test />, 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 <my-custom-element oncustomevent={props.handler} />;
}

const container = document.createElement('div');
ReactDOM.render(<Test handler={oncustomevent} />, container);
const customElement = container.querySelector('my-custom-element');
customElement.dispatchEvent(new Event('customevent'));
expect(oncustomevent).toHaveBeenCalledTimes(1);

ReactDOM.render(<Test handler={false} />, 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(<Test handler={oncustomevent} />, container);
customElement.dispatchEvent(new Event('customevent'));
expect(oncustomevent).toHaveBeenCalledTimes(2);

const oncustomevent2 = jest.fn();
ReactDOM.render(<Test handler={oncustomevent2} />, container);
customElement.dispatchEvent(new Event('customevent'));
expect(oncustomevent).toHaveBeenCalledTimes(2);
expect(oncustomevent2).toHaveBeenCalledTimes(1);
});
josepharhar marked this conversation as resolved.
Show resolved Hide resolved

it('custom elements shouldnt have non-functions for on* attributes treated as event listeners', () => {
const container = document.createElement('div');
ReactDOM.render(
<my-custom-element
onstring={'hello'}
onobj={{hello: 'world'}}
onarray={['one', 'two']}
ontrue={true}
onfalse={false}
/>,
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');
josepharhar marked this conversation as resolved.
Show resolved Hide resolved

// 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', () => {
gaearon marked this conversation as resolved.
Show resolved Hide resolved
let syntheticClickEvent = null;
const syntheticEventHandler = jest.fn(
event => (syntheticClickEvent = event),
);
let nativeClickEvent = null;
const nativeEventHandler = jest.fn(event => (nativeClickEvent = event));
function Test() {
return <my-custom-element onClick={syntheticEventHandler} />;
}

const container = document.createElement('div');
document.body.appendChild(container);
ReactDOM.render(<Test />, 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 (
<my-custom-element
oncustomeventCapture={oncustomeventCapture}
oncustomevent={oncustomevent}>
<div />
</my-custom-element>
);
}
const container = document.createElement('div');
ReactDOM.render(<Test />, 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(<my-custom-element innerHTML="foo" />, 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(<my-custom-element innerHTML="bar" />, 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(<my-custom-element innerText="foo" />, container);
const customElement = container.querySelector('my-custom-element');
expect(customElement.getAttribute('innerText')).toBe(null);
expect(customElement.hasChildNodes()).toBe(false);
josepharhar marked this conversation as resolved.
Show resolved Hide resolved

// Render again to verify the update codepath doesn't accidentally let
// something through.
ReactDOM.render(<my-custom-element innerText="bar" />, 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(<my-custom-element textContent="foo" />, 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(<my-custom-element textContent="bar" />, 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(<my-custom-element />, container);
const customElement = container.querySelector('my-custom-element');
customElement.foo = null;

// true => string
ReactDOM.render(<my-custom-element foo={true} />, container);
expect(customElement.foo).toBe(true);
ReactDOM.render(<my-custom-element foo="bar" />, container);
expect(customElement.foo).toBe('bar');

// false => string
ReactDOM.render(<my-custom-element foo={false} />, container);
expect(customElement.foo).toBe(false);
ReactDOM.render(<my-custom-element foo="bar" />, container);
expect(customElement.foo).toBe('bar');

// true => null
ReactDOM.render(<my-custom-element foo={true} />, container);
expect(customElement.foo).toBe(true);
ReactDOM.render(<my-custom-element foo={null} />, container);
expect(customElement.foo).toBe(null);

// false => null
ReactDOM.render(<my-custom-element foo={false} />, container);
expect(customElement.foo).toBe(false);
ReactDOM.render(<my-custom-element foo={null} />, 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(<my-custom-element oncustomevent={'foo'} />, 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(
<my-custom-element oncustomevent={oncustomevent} />,
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(<my-custom-element oncustomevent={'foo'} />, 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(<my-custom-element />, 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(
<my-custom-element oncustomevent={oncustomevent} />,
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(<my-custom-element />, 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(
<my-custom-element oncustomevent={oncustomevent} />,
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(<my-custom-element oncustomevent={'foo'} />, 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(
<my-custom-element oncustomevent={oncustomevent} />,
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(<my-custom-element />, 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(<my-custom-element foo="one" />, 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(<my-custom-element foo="two" />, container);
expect(customElement.foo).toBe('two');
expect(customElement.getAttribute('foo')).toBe('one');
});
});

describe('deleteValueForProperty', () => {
Expand Down
Loading