react-native
Version:
A framework for building native apps using React
381 lines (337 loc) • 12.9 kB
JavaScript
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
// flowlint unsafe-getters-setters:off
import type {
InternalInstanceHandle,
ViewConfig,
} from '../../../../../Libraries/Renderer/shims/ReactNativeTypes';
import type {
HostInstance,
MeasureInWindowOnSuccessCallback,
MeasureLayoutOnSuccessCallback,
MeasureOnSuccessCallback,
} from '../../../types/HostInstance';
import type Event from '../events/Event';
import type {InstanceHandle} from './internals/NodeInternals';
import type ReactNativeDocument from './ReactNativeDocument';
import TextInputState from '../../../../../Libraries/Components/TextInput/TextInputState';
import {Commands as ViewCommands} from '../../../../../Libraries/Components/View/ViewNativeComponent';
import {create as createAttributePayload} from '../../../../../Libraries/ReactNative/ReactFabricPublicInstance/ReactNativeAttributePayload';
import warnForStyleProps from '../../../../../Libraries/ReactNative/ReactFabricPublicInstance/warnForStyleProps';
import * as ReactNativeFeatureFlags from '../../../featureflags/ReactNativeFeatureFlags';
import {getEventTypePropName} from '../../../renderer/events/ReactNativeEventTypeMapping';
import {
getBubbledPropName,
getCapturedPropName,
} from '../events/internals/EventInternals';
import {EVENT_TARGET_GET_DECLARATIVE_LISTENER_KEY} from '../events/internals/EventTargetInternals';
import {
getCurrentProps,
getNativeElementReference,
getPublicInstanceFromInstanceHandle,
setInstanceHandle,
setOwnerDocument,
} from './internals/NodeInternals';
import ReadOnlyElement, {getBoundingClientRect} from './ReadOnlyElement';
import NativeDOM from './specs/NativeDOM';
const noop = () => {};
// Ideally, this class would be exported as-is, but this implementation is
// significantly slower than the existing `ReactFabricHostComponent`.
// This is a very hot code path (this class is instantiated once per rendered
// host component in the tree) and we can't regress performance here.
//
// This implementation is slower because this is a subclass and we have to call
// super(), which is a very slow operation the way that Babel transforms it at
// the moment.
//
// The optimization we're doing is using an old-style function constructor,
// where we're not required to use `super()`, and we make that constructor
// extend this class so it inherits all the methods and it sets the class
// hierarchy correctly.
//
// An alternative implementation was to implement the constructor as a function
// returning a manually constructed instance using `Object.create()` but that
// was slower than this method because the engine has to create an object than
// we then discard to create a new one.
/** @build-types protected-constructor */
class ReactNativeElement extends ReadOnlyElement {
// These need to be accessible from `ReactFabricPublicInstanceUtils`.
__nativeTag: number;
__internalInstanceHandle: InstanceHandle;
__viewConfig: ViewConfig;
// This constructor isn't really used. See the `ReactNativeElement` function
// below.
constructor(
tag: number,
viewConfig: ViewConfig,
instanceHandle: InstanceHandle,
ownerDocument: ReactNativeDocument,
) {
super(instanceHandle, ownerDocument);
this.__nativeTag = tag;
this.__internalInstanceHandle = instanceHandle;
this.__viewConfig = viewConfig;
}
get offsetHeight(): number {
return Math.round(
getBoundingClientRect(this, {includeTransform: false}).height,
);
}
get offsetLeft(): number {
const node = getNativeElementReference(this);
if (node != null) {
const offset = NativeDOM.getOffset(node);
return Math.round(offset[2]);
}
return 0;
}
get offsetParent(): ReadOnlyElement | null {
const node = getNativeElementReference(this);
if (node != null) {
const offset = NativeDOM.getOffset(node);
// For children of the root node we currently return offset data
// but a `null` parent because the root node is not accessible
// in JavaScript yet.
if (offset[0] != null) {
const offsetParentInstanceHandle = offset[0];
const offsetParent = getPublicInstanceFromInstanceHandle(
offsetParentInstanceHandle,
);
// $FlowExpectedError[incompatible-type] The value returned by `getOffset` is always an instance handle for `ReadOnlyElement`.
const offsetParentElement: ReadOnlyElement | null = offsetParent;
return offsetParentElement;
}
}
return null;
}
get offsetTop(): number {
const node = getNativeElementReference(this);
if (node != null) {
const offset = NativeDOM.getOffset(node);
return Math.round(offset[1]);
}
return 0;
}
get offsetWidth(): number {
return Math.round(
getBoundingClientRect(this, {includeTransform: false}).width,
);
}
/**
* React Native compatibility methods
*/
/**
* Removes focus from an input or view. This is the opposite of `focus()`.
*/
blur(): void {
if (TextInputState.isTextInput(this)) {
TextInputState.blurTextInput(this);
} else if (ReactNativeFeatureFlags.enableImperativeFocus()) {
ViewCommands.blur(this);
}
}
/**
* Requests focus for the given input or view. The exact behavior triggered
* will depend on the platform and type of view.
*/
focus() {
if (TextInputState.isTextInput(this)) {
TextInputState.focusTextInput(this);
} else if (ReactNativeFeatureFlags.enableImperativeFocus()) {
ViewCommands.focus(this);
}
}
/**
* Determines the location on screen, width, and height of the given view and
* returns the values via an async callback. If successful, the callback will
* be called with the following arguments:
*
* - x
* - y
* - width
* - height
* - pageX
* - pageY
*
* Note that these measurements are not available until after the rendering
* has been completed in native. If you need the measurements as soon as
* possible, consider using the [`onLayout`
* prop](docs/view.html#onlayout) instead.
*/
measure(callback: MeasureOnSuccessCallback) {
const node = getNativeElementReference(this);
if (node != null) {
NativeDOM.measure(node, callback);
}
}
/**
* Determines the location of the given view in the window and returns the
* values via an async callback. If the React root view is embedded in
* another native view, this will give you the absolute coordinates. If
* successful, the callback will be called with the following
* arguments:
*
* - x
* - y
* - width
* - height
*
* Note that these measurements are not available until after the rendering
* has been completed in native.
*/
measureInWindow(callback: MeasureInWindowOnSuccessCallback) {
const node = getNativeElementReference(this);
if (node != null) {
NativeDOM.measureInWindow(node, callback);
}
}
/**
* Like [`measure()`](#measure), but measures the view relative an ancestor,
* specified as `relativeToNativeComponentRef`. This means that the returned x, y
* are relative to the origin x, y of the ancestor view.
* _Can also be called with a relativeNativeNodeHandle but is deprecated._
*/
measureLayout(
relativeToNativeNode: number | HostInstance,
onSuccess: MeasureLayoutOnSuccessCallback,
onFail?: () => void /* currently unused */,
) {
if (!(relativeToNativeNode instanceof ReactNativeElement)) {
if (__DEV__) {
console.error(
'Warning: ref.measureLayout must be called with a ref to a native component.',
);
}
return;
}
const toStateNode = getNativeElementReference(this);
const fromStateNode = getNativeElementReference(relativeToNativeNode);
if (toStateNode != null && fromStateNode != null) {
NativeDOM.measureLayout(
toStateNode,
fromStateNode,
onFail != null ? onFail : noop,
onSuccess != null ? onSuccess : noop,
);
}
}
/**
* This function sends props straight to native. They will not participate in
* future diff process - this means that if you do not include them in the
* next render, they will remain active (see [Direct
* Manipulation](https://reactnative.dev/docs/the-new-architecture/direct-manipulation-new-architecture)).
*/
setNativeProps(nativeProps: {...}): void {
if (__DEV__) {
warnForStyleProps(nativeProps, this.__viewConfig.validAttributes);
}
const updatePayload = createAttributePayload(
nativeProps,
this.__viewConfig.validAttributes,
);
const node = getNativeElementReference(this);
if (node != null && updatePayload != null) {
NativeDOM.setNativeProps(node, updatePayload);
}
}
// Provide event listeners from React props during EventTarget dispatch.
// This is called by EventTarget.invoke() before explicit addEventListener
// listeners, allowing prop-based handlers to be resolved at dispatch time
// without registering them via addEventListener during commit.
//
// Fast path: when `event` is a `LegacySyntheticEvent` (always the case for
// events created by `dispatchNativeEvent`), the React prop names have been
// pre-resolved on the event during construction. Reading them directly
// avoids the `getEventTypePropName(eventType, isCapture)` hash lookup per
// ancestor per phase.
// $FlowExpectedError[unsupported-syntax]
// $FlowFixMe[illegal-key]
[EVENT_TARGET_GET_DECLARATIVE_LISTENER_KEY](
event: Event,
isCapture: boolean,
): ((event: Event) => void) | null {
const currentProps = getCurrentProps(this);
if (currentProps == null) {
return null;
}
let propName = isCapture
? getCapturedPropName(event)
: getBubbledPropName(event);
if (propName === undefined) {
// The event wasn't created via `dispatchNativeEvent` (e.g.,
// user-dispatched). Fall back to the mapping table.
propName = getEventTypePropName(event.type, isCapture);
}
if (propName == null) {
return null;
}
const handler = currentProps[propName];
return typeof handler === 'function' ? handler : null;
}
}
type ReactNativeElementT = ReactNativeElement;
function replaceConstructorWithoutSuper(
ReactNativeElementClass: Class<ReactNativeElementT>,
): Class<ReactNativeElementT> {
// Alternative constructor just implemented to provide a better performance than
// calling super() in the original class.
// eslint-disable-next-line no-shadow
function ReactNativeElement(
this: ReactNativeElementT,
tag: number,
viewConfig: ViewConfig,
internalInstanceHandle: InternalInstanceHandle,
ownerDocument: ReactNativeDocument,
) {
// Inlined from `ReadOnlyNode`
setOwnerDocument(this, ownerDocument);
setInstanceHandle(this, internalInstanceHandle);
this.__nativeTag = tag;
this.__internalInstanceHandle = internalInstanceHandle;
this.__viewConfig = viewConfig;
}
ReactNativeElement.prototype = ReactNativeElementClass.prototype;
// $FlowExpectedError[incompatible-type]
return ReactNativeElement;
}
export default replaceConstructorWithoutSuper(
ReactNativeElement,
) as typeof ReactNativeElement;
export const ReactNativeElement_public: typeof ReactNativeElement =
// $FlowExpectedError[incompatible-type]
function HTMLElement() {
throw new TypeError(
"Failed to construct 'HTMLElement': Nodes cannot be imperatively created in React Native",
);
};
// $FlowExpectedError[prop-missing]
ReactNativeElement_public.prototype = ReactNativeElement.prototype;
// The public imperative EventTarget API (`addEventListener`,
// `removeEventListener`, `dispatchEvent`) is only inherited by this final class
// when `enableNativeEventTargetEventDispatching` is enabled (which makes
// `ReadOnlyNode` extend `EventTarget`). Until that public API is finalized, it
// is gated behind `enableImperativeEvents` (or its deprecated JS-only
// counterpart, `enableImperativeEvents_DEPRECATED`): when both flags are off we
// remove those methods from this final class. Native/internal event dispatch
// does not rely on these public methods, so removing them is safe.
if (
ReactNativeFeatureFlags.enableNativeEventTargetEventDispatching() &&
!ReactNativeFeatureFlags.enableImperativeEvents() &&
!ReactNativeFeatureFlags.enableImperativeEvents_DEPRECATED()
) {
const prototype: interface {
addEventListener?: unknown,
removeEventListener?: unknown,
dispatchEvent?: unknown,
} = ReactNativeElement.prototype;
prototype.addEventListener = undefined;
prototype.removeEventListener = undefined;
prototype.dispatchEvent = undefined;
}