diff --git a/packages/react-strict-dom/CHANGELOG.md b/packages/react-strict-dom/CHANGELOG.md index b27380ef..87c80169 100644 --- a/packages/react-strict-dom/CHANGELOG.md +++ b/packages/react-strict-dom/CHANGELOG.md @@ -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. diff --git a/packages/react-strict-dom/src/native/modules/useStrictDOMElement.js b/packages/react-strict-dom/src/native/modules/useStrictDOMElement.js index a8a6c7bd..eaf42291 100644 --- a/packages/react-strict-dom/src/native/modules/useStrictDOMElement.js +++ b/packages/react-strict-dom/src/native/modules/useStrictDOMElement.js @@ -49,6 +49,8 @@ type StrictRefTarget = { const memoizedStrictRefs: WeakMap = new WeakMap(); +const objectPrototype: interface {} | null = Object.getPrototypeOf({}); + const lengthPropertySet: ReadonlySet = new Set([ 'clientHeight', 'clientLeft', @@ -64,6 +66,45 @@ const lengthPropertySet: ReadonlySet = 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 = 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) => + // $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 @@ -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 diff --git a/packages/react-strict-dom/tests/html/html-refs-test.native.js b/packages/react-strict-dom/tests/html/html-refs-test.native.js index 5b80dbe2..20dfda8e 100644 --- a/packages/react-strict-dom/tests/html/html-refs-test.native.js +++ b/packages/react-strict-dom/tests/html/html-refs-test.native.js @@ -94,6 +94,41 @@ describe(' 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( + { + 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(