react-native
Version:
A framework for building native apps using React
208 lines (178 loc) • 6.37 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 {ResizeObserverId} from './internals/ResizeObserverManager';
import type ResizeObserverEntry from './ResizeObserverEntry';
import ReactNativeElement from '../dom/nodes/ReactNativeElement';
import {setPlatformObject} from '../webidl/PlatformObjects';
import * as ResizeObserverManager from './internals/ResizeObserverManager';
export type ResizeObserverCallback = (
entries: ReadonlyArray<ResizeObserverEntry>,
observer: ResizeObserver,
) => unknown;
/**
* Corresponds to the `box` option of `ResizeObserver#observe`.
* https://drafts.csswg.org/resize-observer/#resize-observer-box-options
*/
export type ResizeObserverBoxOptions =
'content-box' | 'border-box' | 'device-pixel-content-box';
export interface ResizeObserverOptions {
box?: ResizeObserverBoxOptions;
}
/**
* The [`ResizeObserver`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver)
* interface of the Resize Observer API reports changes to the dimensions of
* an element's content or border box.
*
* `ResizeObserver` avoids infinite callback loops and cyclic dependencies
* that are often created when resizing via a callback function, as it only
* observes block and inline sizes on the content box or border box, as
* opposed to observing changes in pixels.
*
* You can add/remove multiple target elements to/from a single
* `ResizeObserver` instance.
*/
export default class ResizeObserver {
_callback: ResizeObserverCallback;
_observationTargets: Map<ReactNativeElement, ResizeObserverBoxOptions> =
new Map();
_resizeObserverId: ?ResizeObserverId;
constructor(callback: ResizeObserverCallback): void {
if (callback == null) {
throw new TypeError(
"Failed to construct 'ResizeObserver': 1 argument required, but only 0 present.",
);
}
if (typeof callback !== 'function') {
throw new TypeError(
"Failed to construct 'ResizeObserver': parameter 1 is not of type 'Function'.",
);
}
this._callback = callback;
}
/**
* Starts observing the specified `ReactNativeElement`.
* One observer has one set of observation targets, and each target can be
* observed using either the target's content box or its border box (as
* specified by the `box` option).
* To stop observing an element, call `ResizeObserver.unobserve()`.
*/
observe(target: ReactNativeElement, options?: ResizeObserverOptions): void {
if (target == null) {
throw new TypeError(
"Failed to execute 'observe' on 'ResizeObserver': parameter 1 is null or undefined.",
);
}
if (!(target instanceof ReactNativeElement)) {
throw new TypeError(
"Failed to execute 'observe' on 'ResizeObserver': parameter 1 is not of type 'ReactNativeElement'.",
);
}
const box = normalizeBoxOption(options?.box);
if (this._observationTargets.has(target)) {
// Re-observing a target already observed with the SAME box is a no-op
// and does not re-deliver. This matches observed browser behavior.
const previousBox = this._observationTargets.get(target);
if (previousBox === box) {
return;
}
ResizeObserverManager.unobserve(
this._getOrCreateResizeObserverId(),
target,
);
this._observationTargets.delete(target);
}
const resizeObserverId = this._getOrCreateResizeObserverId();
const didObserve = ResizeObserverManager.observe({
resizeObserverId,
target,
box,
});
if (didObserve) {
this._observationTargets.set(target, box);
} else if (this._observationTargets.size === 0) {
// Target couldn't be observed (e.g. disconnected). Don't record it so a
// later `observe` can retry, and release the registration since nothing
// is observed.
ResizeObserverManager.unregisterObserver(resizeObserverId);
this._resizeObserverId = null;
}
}
/**
* Ends the observing of the specified `ReactNativeElement`.
*/
unobserve(target: ReactNativeElement): void {
if (!(target instanceof ReactNativeElement)) {
throw new TypeError(
"Failed to execute 'unobserve' on 'ResizeObserver': parameter 1 is not of type 'ReactNativeElement'.",
);
}
if (!this._observationTargets.has(target)) {
return;
}
const resizeObserverId = this._resizeObserverId;
if (resizeObserverId == null) {
// This is unexpected if the target is in `_observationTargets`.
console.error(
"Unexpected state in 'ResizeObserver': could not find observer ID to unobserve target.",
);
return;
}
ResizeObserverManager.unobserve(resizeObserverId, target);
this._observationTargets.delete(target);
if (this._observationTargets.size === 0) {
ResizeObserverManager.unregisterObserver(resizeObserverId);
this._resizeObserverId = null;
}
}
/**
* Unobserves all observed `ReactNativeElement` targets.
*/
disconnect(): void {
for (const target of this._observationTargets.keys()) {
this.unobserve(target);
}
}
_getOrCreateResizeObserverId(): ResizeObserverId {
let resizeObserverId = this._resizeObserverId;
if (resizeObserverId == null) {
resizeObserverId = ResizeObserverManager.registerObserver(
this,
this._callback,
);
this._resizeObserverId = resizeObserverId;
}
return resizeObserverId;
}
// Only for tests
__getObserverID(): ?ResizeObserverId {
return this._resizeObserverId;
}
}
setPlatformObject(ResizeObserver);
function normalizeBoxOption(
box: ?ResizeObserverBoxOptions,
): ResizeObserverBoxOptions {
if (box == null) {
return 'content-box';
}
if (
box !== 'content-box' &&
box !== 'border-box' &&
box !== 'device-pixel-content-box'
) {
throw new TypeError(
`Failed to execute 'observe' on 'ResizeObserver': Failed to read the 'box' property from 'ResizeObserverOptions': The provided value '${String(
box,
)}' is not a valid enum value of type ResizeObserverBoxOptions.`,
);
}
return box;
}