Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions packages/react-strict-dom/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

* [Native] React Strict DOM and React Strict Animated now require `react-native >=0.82.0` for native builds that use RN DOM Node APIs.

### Fixes

* [Native] Call inherited host node methods, such as `focus` and `blur`, on the host node so imperative ref APIs work again.

### Internal

* [Native] `useStrictDOMElement` now wraps the RN host node with `Object.create(node)` and defines only strict-dom overrides as own properties.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ type StrictRefTarget = {
const memoizedStrictRefs: WeakMap<HostInstance, StrictRefTarget> =
new WeakMap();

const objectPrototype: interface {} | null = Object.getPrototypeOf({});

const lengthPropertySet: ReadonlySet<string> = new Set([
'clientHeight',
'clientLeft',
Expand All @@ -64,6 +66,45 @@ const lengthPropertySet: ReadonlySet<string> = new Set([
'scrollWidth'
]);

/**
* Re-exposes the host node's inherited methods as own properties that call
* through to the node.
*
* Reading a method off the wrapper resolves it via the prototype chain but
* calls it with the wrapper as the receiver. RN's host node methods read
* internals off `this`, and those internals are not reachable from the
* wrapper, so `strictRef.focus()` is a no-op while `node.focus()` works.
* Own methods of the node are left alone: RN assigns those as closures over
* the node, so their receiver does not matter.
*/
function bindInheritedMethods(strictRef: StrictRefTarget, node: HostInstance) {
let proto: interface {} | null = Object.getPrototypeOf(node);
while (proto != null && proto !== objectPrototype) {
const names: ReadonlyArray<string> = Object.getOwnPropertyNames(proto);
for (const name of names) {
if (
name === 'constructor' ||
Object.getOwnPropertyDescriptor(strictRef, name) != null
) {
continue;
}
const descriptor = Object.getOwnPropertyDescriptor(proto, name);
// Accessors are left on the prototype chain: they read the plain
// instance fields that the wrapper already inherits.
if (descriptor == null || typeof descriptor.value !== 'function') {
continue;
}
Object.defineProperty(strictRef, name, {
value: (...args: Array<unknown>) =>
// $FlowFixMe[prop-missing] - dynamic method call on the RN host node.
node[name](...args),
configurable: true
});
}
proto = Object.getPrototypeOf(proto);
}
}

/**
* Uses the RN host node as the wrapper's prototype so non-overridden reads
* resolve via the prototype chain — including the symbol-keyed internals
Expand All @@ -88,6 +129,10 @@ function getOrCreateStrictRef(
// $FlowFixMe[class-object-subtyping] - read the host node's polyfill members.
const nodeInternals: StrictRefPolyfills = node;

// Runs before the definitions below so the strict-dom overrides, such as the
// viewport-scaled `getBoundingClientRect`, still win.
bindInheritedMethods(strictRef, node);

Object.defineProperty(strictRef, 'nodeName', {
value: tagName.toUpperCase(),
configurable: true
Expand Down
35 changes: 35 additions & 0 deletions packages/react-strict-dom/tests/html/html-refs-test.native.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,41 @@ describe('<html.*> refs', () => {
});
});

test('input ref calls inherited host node methods on the host node', () => {
// RN host instances define `focus` and `blur` on the prototype chain and
// read internals off `this`, so the receiver has to stay the host node.
const receivers = {};
let hostNode;
function createNodeMock() {
const prototype = {
blur() {
receivers.blur = this;
},
focus() {
receivers.focus = this;
}
};
const ownProperties = buildHostNodeMock();
delete ownProperties.blur;
delete ownProperties.focus;
hostNode = Object.assign(Object.create(prototype), ownProperties);
return hostNode;
}
act(() => {
create(
<html.input
ref={(node) => {
node.focus();
node.blur();
}}
/>,
{ createNodeMock }
);
});
expect(receivers.focus).toBe(hostNode);
expect(receivers.blur).toBe(hostNode);
});

test('input ref does not throw when host node is null', () => {
act(() => {
create(
Expand Down