react-native-reserved-regions 0.1.0-alpha.0 → 0.1.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,15 +5,19 @@ Provider-scoped display divisions and occlusions for React Native's New Architec
5
5
  ## Requirements
6
6
 
7
7
  - React Native New Architecture (Fabric)
8
- - iOS 27.1 for UIKit reserved regions; earlier iOS versions report an empty list
8
+ - iOS 27.1 SDK and runtime for UIKit reserved regions; older SDKs compile out observation and older runtimes report an empty list
9
9
  - Android 7.0 (API 24) or newer; folding features require a device supported by Jetpack WindowManager
10
10
 
11
11
  The example app uses React Native 0.88.0-rc.1.
12
+ Its iOS target adopts `UISceneDelegate` because the RC template currently crashes on iOS 27 without it ([React Native issue #58606](https://github.com/react/react-native/issues/58606)).
12
13
 
13
14
  ## Installation
14
15
 
16
+ `0.1.0-alpha.2` is the functional release candidate. Install the explicit version
17
+ once publication is verified; until then, run the repository example.
18
+
15
19
  ```sh
16
- npm install react-native-reserved-regions
20
+ npm install react-native-reserved-regions@0.1.0-alpha.2
17
21
  ```
18
22
 
19
23
  On iOS, install pods in your app's `ios` directory. Android links automatically through React Native autolinking.
@@ -21,13 +25,12 @@ On iOS, install pods in your app's `ios` directory. Android links automatically
21
25
  ## Usage
22
26
 
23
27
  ```tsx
24
- import {
25
- ReservedRegionsProvider,
26
- useReservedRegions,
27
- } from 'react-native-reserved-regions';
28
+ import { ReservedRegionsProvider, useReservedRegions, useReservedRegionsReady } from 'react-native-reserved-regions';
28
29
 
29
30
  function Screen() {
30
31
  const regions = useReservedRegions();
32
+ const isReady = useReservedRegionsReady();
33
+ if (!isReady) return null;
31
34
  return regions.map((region, index) => {
32
35
  if (region.kind === 'division') {
33
36
  console.log(region.frame, region.occludesContent);
@@ -47,7 +50,7 @@ export default function App() {
47
50
  }
48
51
  ```
49
52
 
50
- `useReservedRegions()` returns an empty array until the native view reports its first layout and when no active regions overlap the provider. It must be called beneath a `ReservedRegionsProvider`. Each region's `frame` uses logical points relative to that provider's top-left corner. A nested provider establishes its own coordinate space. The provider accepts standard React Native `View` props and should cover the content whose reserved regions you want to inspect.
53
+ `useReservedRegions()` always returns an array, initially `[]`. `useReservedRegionsReady()` is initially `false` and becomes `true` with the first measurement, including an empty result. It stays true for that provider’s lifetime. Unsupported platforms report a known empty result; readiness does not indicate hardware support or guarantee first-frame timing. Both hooks must be called beneath a `ReservedRegionsProvider`. Each region's `frame` uses logical points relative to that provider's top-left corner. A nested provider establishes its own coordinate space. The provider accepts standard React Native `View` props and should cover the content whose reserved regions you want to inspect.
51
54
 
52
55
  The public type is a tagged union:
53
56
 
@@ -68,22 +71,48 @@ A division splits the available display. `occludesContent` tells you whether con
68
71
 
69
72
  ## Native sources
70
73
 
71
- - iOS queries UIKit's active `UIView` reserved division and occlusion regions on the provider view. The selectors are resolved at runtime so the library can compile with an SDK older than iOS 27.1. [Apple's reserved regions overview](https://developer.apple.com/videos/play/tech-talks/111463/)
74
+ - iOS queries UIKit's active `UIView` reserved division and occlusion regions on the provider view. Typed UIKit calls are guarded by the SDK version and runtime availability. Building with an SDK older than iOS 27.1 compiles out this feature. [Apple's reserved regions overview](https://developer.apple.com/videos/play/tech-talks/111463/)
72
75
  - Android observes Jetpack WindowManager `FoldingFeature` values and Android display cutout rectangles. A separating or fully occluding fold becomes a division; `OcclusionType.FULL` sets `occludesContent` to `true`. Display cutouts become occlusions. [FoldingFeature reference](https://developer.android.com/reference/androidx/window/layout/FoldingFeature), [WindowInsets reference](https://developer.android.com/reference/android/view/WindowInsets)
73
76
 
74
77
  The library reports geometry and does not reposition content. Normal safe area insets, system bars, and keyboard insets remain separate concerns.
75
78
 
79
+ ## Measurement timing
80
+
81
+ Native events request synchronous delivery. Android measures after Fabric mounting
82
+ and also observes pre-draw changes. The iOS example tests same-frame delivery with
83
+ an [upstream React Native event-beat patch](docs/workflow.md#react-native-event-beat-test-patch).
84
+ That example patch is not installed into consuming apps. Readiness indicates a
85
+ completed measurement, not a universal first-frame guarantee.
86
+
87
+ ### Readiness-gated animated content
88
+
89
+ On the tested RN `0.88.0-rc.1` Android stack, conditionally mounting Reanimated
90
+ `4.7.0` content when `useReservedRegionsReady()` becomes true can throw
91
+ `__requestMapperRunFinalizer` is undefined: Worklets `0.13.0` can run synchronous
92
+ UI work ahead of queued mapper initialization. The combined hinges example uses
93
+ an [example-only Worklets FIFO patch](https://github.com/appandflow/react-native-hinges/blob/main/patches/react-native-worklets%400.13.0.patch).
94
+ Installing either library does not patch a consuming app's Worklets dependency.
95
+ Keeping the animated subtree mounted avoided this failure in the tested case;
96
+ apps that gate its mount need to apply the patch, rebuild the native app, and
97
+ validate it themselves.
98
+
76
99
  ## Development
77
100
 
78
101
  ```sh
79
- yarn install
80
- yarn typecheck
81
- yarn lint
82
- yarn test --watch=false
83
- yarn prepare
102
+ pnpm install --frozen-lockfile
103
+ pnpm run format:check
104
+ pnpm run lint
105
+ pnpm run typecheck
106
+ pnpm test
107
+ pnpm run build
108
+ pnpm run docs:build
84
109
  ```
85
110
 
86
- To run the example, use `yarn example start`, then `yarn example ios` or `yarn example android`.
111
+ The example includes full-screen, inset and content-box providers with
112
+ `react-native-safe-area-context` values and overlays for comparison.
113
+ See [the development workflow](docs/workflow.md) for native runs,
114
+ [the docs website](website/README.md) for local previews, and
115
+ [the release process](RELEASE.md) for publishing.
87
116
 
88
117
  ## License
89
118
 
@@ -36,7 +36,7 @@ if (project.extensions.findByName("kotlin") == null) {
36
36
  apply plugin: "com.facebook.react"
37
37
 
38
38
  android {
39
- namespace "com.reservedregions"
39
+ namespace "com.appandflow.reservedregions"
40
40
 
41
41
  compileSdkVersion getExtOrDefault("compileSdkVersion")
42
42
 
@@ -1,4 +1,4 @@
1
- package com.reservedregions
1
+ package com.appandflow.reservedregions
2
2
 
3
3
  import com.facebook.react.bridge.Arguments
4
4
  import com.facebook.react.bridge.WritableMap
@@ -12,6 +12,8 @@ internal class RegionsChangeEvent(
12
12
  ) : Event<RegionsChangeEvent>(surfaceId, viewTag) {
13
13
  override fun getEventName() = NAME
14
14
 
15
+ override fun experimental_isSynchronous() = true
16
+
15
17
  override fun getEventData(): WritableMap {
16
18
  val payload = Arguments.createMap()
17
19
  val regionArray = Arguments.createArray()
@@ -1,4 +1,4 @@
1
- package com.reservedregions
1
+ package com.appandflow.reservedregions
2
2
 
3
3
  import android.graphics.Rect
4
4
 
@@ -1,4 +1,4 @@
1
- package com.reservedregions
1
+ package com.appandflow.reservedregions
2
2
 
3
3
  import com.facebook.react.BaseReactPackage
4
4
  import com.facebook.react.bridge.NativeModule
@@ -1,45 +1,80 @@
1
- package com.reservedregions
1
+ package com.appandflow.reservedregions
2
2
 
3
3
  import android.content.Context
4
4
  import android.graphics.Rect
5
5
  import android.os.Build
6
6
  import android.view.ViewTreeObserver
7
- import android.view.WindowInsets
8
7
  import androidx.core.content.ContextCompat
9
8
  import androidx.core.util.Consumer
9
+ import androidx.window.WindowSdkExtensions
10
10
  import androidx.window.java.layout.WindowInfoTrackerCallbackAdapter
11
11
  import androidx.window.layout.FoldingFeature
12
12
  import androidx.window.layout.WindowInfoTracker
13
13
  import androidx.window.layout.WindowLayoutInfo
14
+ import com.facebook.react.bridge.UIManager
15
+ import com.facebook.react.bridge.UIManagerListener
16
+ import com.facebook.react.common.annotations.UnstableReactNativeAPI
14
17
  import com.facebook.react.uimanager.ThemedReactContext
18
+ import com.facebook.react.uimanager.UIManagerHelper
15
19
  import com.facebook.react.views.view.ReactViewGroup
16
20
  import kotlin.math.max
17
21
  import kotlin.math.min
18
22
 
19
23
  internal typealias RegionsChangeHandler = (ReservedRegionsView, List<ReservedRegion>) -> Unit
20
24
 
21
- class ReservedRegionsView(context: Context) : ReactViewGroup(context), ViewTreeObserver.OnPreDrawListener {
25
+ @OptIn(UnstableReactNativeAPI::class)
26
+ class ReservedRegionsView(context: Context) : ReactViewGroup(context), ViewTreeObserver.OnPreDrawListener, UIManagerListener {
27
+ private var uiManager: UIManager? = null
22
28
  private var tracker: WindowInfoTrackerCallbackAdapter? = null
23
29
  private val layoutInfoConsumer = Consumer<WindowLayoutInfo> { info ->
24
30
  foldingFeatures = info.displayFeatures.filterIsInstance<FoldingFeature>()
25
- updateRegions()
31
+ foldingFeaturesReady = true
32
+ invalidate()
26
33
  }
27
34
  private var foldingFeatures: List<FoldingFeature> = emptyList()
35
+ private var foldingFeaturesReady = false
28
36
  private var lastRegions: List<ReservedRegion>? = null
29
37
  private var onRegionsChange: RegionsChangeHandler? = null
30
38
 
39
+ override fun willDispatchViewUpdates(uiManager: UIManager) {}
40
+
41
+ override fun willMountItems(uiManager: UIManager) {}
42
+
43
+ override fun didScheduleMountItems(uiManager: UIManager) {}
44
+
45
+ override fun didDispatchMountItems(uiManager: UIManager) {}
46
+
47
+ override fun didMountItems(uiManager: UIManager) {
48
+ // Fabric installs event emitters after layout; its event beat runs before pre-draw.
49
+ updateRegions()
50
+ }
51
+
31
52
  override fun onAttachedToWindow() {
32
53
  super.onAttachedToWindow()
54
+ uiManager = UIManagerHelper.getUIManagerForReactTag(UIManagerHelper.getReactContext(this), id)
55
+ uiManager?.addUIManagerEventListener(this)
33
56
  viewTreeObserver.addOnPreDrawListener(this)
57
+ foldingFeatures = emptyList()
58
+ foldingFeaturesReady = false
59
+ lastRegions = null
34
60
  val activity = (context as? ThemedReactContext)?.currentActivity
35
61
  if (activity != null) {
36
- tracker = WindowInfoTrackerCallbackAdapter(WindowInfoTracker.getOrCreate(context))
62
+ val windowTracker = WindowInfoTracker.getOrCreate(context)
63
+ if (WindowSdkExtensions.getInstance().extensionVersion >= 9) {
64
+ foldingFeatures = windowTracker.getCurrentWindowLayoutInfo(activity)
65
+ .displayFeatures.filterIsInstance<FoldingFeature>()
66
+ foldingFeaturesReady = true
67
+ }
68
+ tracker = WindowInfoTrackerCallbackAdapter(windowTracker)
37
69
  tracker?.addWindowLayoutInfoListener(activity, ContextCompat.getMainExecutor(context), layoutInfoConsumer)
70
+ } else {
71
+ foldingFeaturesReady = true
38
72
  }
39
- updateRegions()
40
73
  }
41
74
 
42
75
  override fun onDetachedFromWindow() {
76
+ uiManager?.removeUIManagerEventListener(this)
77
+ uiManager = null
43
78
  tracker?.removeWindowLayoutInfoListener(layoutInfoConsumer)
44
79
  tracker = null
45
80
  viewTreeObserver.removeOnPreDrawListener(this)
@@ -53,12 +88,13 @@ class ReservedRegionsView(context: Context) : ReactViewGroup(context), ViewTreeO
53
88
 
54
89
  internal fun setOnRegionsChangeHandler(handler: RegionsChangeHandler) {
55
90
  onRegionsChange = handler
56
- updateRegions()
91
+ lastRegions = null
92
+ invalidate()
57
93
  }
58
94
 
59
95
  private fun updateRegions() {
60
96
  val handler = onRegionsChange ?: return
61
- if (!isAttachedToWindow || width == 0 || height == 0) return
97
+ if (!isAttachedToWindow || !foldingFeaturesReady || width == 0 || height == 0) return
62
98
 
63
99
  val windowLocation = IntArray(2)
64
100
  getLocationInWindow(windowLocation)
@@ -70,13 +106,7 @@ class ReservedRegionsView(context: Context) : ReactViewGroup(context), ViewTreeO
70
106
  regions.add(ReservedRegion("division", frame, feature.occlusionType == FoldingFeature.OcclusionType.FULL))
71
107
  }
72
108
 
73
- if (Build.VERSION.SDK_INT >= 35) {
74
- rootWindowInsets?.getBoundingRects(WindowInsets.Type.displayCutout())?.forEach { bounds ->
75
- clippedFrame(bounds, windowLocation[0], windowLocation[1])?.let { frame ->
76
- regions.add(ReservedRegion("occlusion", frame))
77
- }
78
- }
79
- } else if (Build.VERSION.SDK_INT >= 28) {
109
+ if (Build.VERSION.SDK_INT >= 28) {
80
110
  val screenLocation = IntArray(2)
81
111
  getLocationOnScreen(screenLocation)
82
112
  rootWindowInsets?.displayCutout?.boundingRects?.forEach { bounds ->
@@ -1,4 +1,4 @@
1
- package com.reservedregions
1
+ package com.appandflow.reservedregions
2
2
 
3
3
  import com.facebook.react.bridge.ReactContext
4
4
  import com.facebook.react.module.annotations.ReactModule
@@ -1,9 +1,6 @@
1
1
  #import "ReservedRegionsView.h"
2
2
 
3
- #import <objc/message.h>
4
-
5
3
  #import <react/renderer/components/ReservedRegionsViewSpec/ComponentDescriptors.h>
6
- #import <react/renderer/components/ReservedRegionsViewSpec/EventEmitters.h>
7
4
  #import <react/renderer/components/ReservedRegionsViewSpec/Props.h>
8
5
  #import <react/renderer/components/ReservedRegionsViewSpec/RCTComponentViewHelpers.h>
9
6
 
@@ -38,62 +35,62 @@ using namespace facebook::react;
38
35
  - (void)didMoveToWindow
39
36
  {
40
37
  [super didMoveToWindow];
41
- [self updateRegions];
38
+ [self setNeedsLayout];
39
+ }
40
+
41
+ - (void)updateEventEmitter:(EventEmitter::Shared const &)eventEmitter
42
+ {
43
+ [super updateEventEmitter:eventEmitter];
44
+ [self setNeedsLayout];
42
45
  }
43
46
 
44
47
  - (void)updateRegions
45
48
  {
46
- if (self.window == nil || !_eventEmitter) {
49
+ if (self.window == nil || !_eventEmitter || CGSizeEqualToSize(self.bounds.size, CGSizeZero)) {
47
50
  return;
48
51
  }
49
52
 
50
- // UIView reserved region selectors first ship in the iOS 27.1 SDK.
51
- SEL querySelector = NSSelectorFromString(@"reservedRegionsOfKind:options:");
52
- Class kindClass = NSClassFromString(@"UIViewReservedRegionKind");
53
53
  NSMutableArray<NSDictionary *> *regions = [NSMutableArray new];
54
-
55
- if ([self respondsToSelector:querySelector] && kindClass != nil) {
56
- NSArray<NSDictionary<NSString *, NSString *> *> *kinds = @[
57
- @{@"selector": @"divisionRegionKind", @"kind": @"division"},
58
- @{@"selector": @"occlusionRegionKind", @"kind": @"occlusion"},
54
+ #if defined(__IPHONE_27_1) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_27_1
55
+ if (@available(iOS 27.1, *)) {
56
+ NSArray<UIViewReservedRegionKind *> *kinds = @[
57
+ UIViewReservedRegionKind.divisionRegionKind,
58
+ UIViewReservedRegionKind.occlusionRegionKind,
59
59
  ];
60
-
61
- for (NSDictionary<NSString *, NSString *> *entry in kinds) {
62
- SEL kindSelector = NSSelectorFromString(entry[@"selector"]);
63
- if (![kindClass respondsToSelector:kindSelector]) {
64
- continue;
65
- }
66
-
67
- id kind = ((id (*)(id, SEL))objc_msgSend)(kindClass, kindSelector);
68
- NSArray *results = ((NSArray *(*)(id, SEL, id, NSUInteger))objc_msgSend)(self, querySelector, kind, 0);
69
- for (id region in results) {
70
- if (![region respondsToSelector:@selector(frame)]) {
71
- continue;
72
- }
73
- CGRect frame = [[region valueForKey:@"frame"] CGRectValue];
74
- [regions addObject:@{
75
- @"kind": entry[@"kind"],
76
- @"frame": [NSValue valueWithCGRect:frame],
77
- }];
60
+ NSArray<NSString *> *names = @[@"division", @"occlusion"];
61
+ for (NSUInteger index = 0; index < kinds.count; index++) {
62
+ for (UIViewReservedRegion *region in [self reservedRegionsOfKind:kinds[index]
63
+ options:UIViewReservedRegionQueryOptionsNone]) {
64
+ [regions addObject:@{@"kind": names[index], @"frame": [NSValue valueWithCGRect:region.frame]}];
78
65
  }
79
66
  }
80
67
  }
68
+ #endif
81
69
 
82
70
  if ([_currentRegions isEqualToArray:regions]) {
83
71
  return;
84
72
  }
85
73
  _currentRegions = [regions copy];
86
74
 
87
- ReservedRegionsViewEventEmitter::OnRegionsChange event;
75
+ auto regionPayloads = folly::dynamic::array();
88
76
  for (NSDictionary *region in regions) {
89
77
  CGRect frame = [region[@"frame"] CGRectValue];
90
- event.regions.push_back({
91
- [region[@"kind"] UTF8String],
92
- {frame.origin.x, frame.origin.y, frame.size.width, frame.size.height},
93
- false,
94
- });
78
+ regionPayloads.push_back(folly::dynamic::object
79
+ ("kind", [region[@"kind"] UTF8String])
80
+ ("frame", folly::dynamic::object
81
+ ("x", frame.origin.x)
82
+ ("y", frame.origin.y)
83
+ ("width", frame.size.width)
84
+ ("height", frame.size.height))
85
+ ("occludesContent", false));
95
86
  }
96
- std::static_pointer_cast<ReservedRegionsViewEventEmitter const>(_eventEmitter)->onRegionsChange(event);
87
+ auto eventEmitter = _eventEmitter;
88
+ eventEmitter->experimental_flushSync([eventEmitter, regions = std::move(regionPayloads)]() mutable {
89
+ eventEmitter->dispatchEvent(
90
+ "regionsChange",
91
+ folly::dynamic::object("regions", std::move(regions)),
92
+ RawEvent::Category::Discrete);
93
+ });
97
94
  }
98
95
 
99
96
  - (void)prepareForRecycle
@@ -1,11 +1,19 @@
1
1
  "use strict";
2
2
 
3
+ import { useEffect } from 'react';
3
4
  import { View } from 'react-native';
4
5
  import { jsx as _jsx } from "react/jsx-runtime";
5
6
  export function ReservedRegionsView({
6
- onRegionsChange: _onRegionsChange,
7
+ onRegionsChange,
7
8
  ...props
8
9
  }) {
10
+ useEffect(() => {
11
+ onRegionsChange?.({
12
+ nativeEvent: {
13
+ regions: []
14
+ }
15
+ });
16
+ }, [onRegionsChange]);
9
17
  return /*#__PURE__*/_jsx(View, {
10
18
  ...props
11
19
  });
@@ -1 +1 @@
1
- {"version":3,"names":["View","jsx","_jsx","ReservedRegionsView","onRegionsChange","_onRegionsChange","props"],"sourceRoot":"../../src","sources":["ReservedRegionsView.tsx"],"mappings":";;AAAA,SAASA,IAAI,QAAwB,cAAc;AAAC,SAAAC,GAAA,IAAAC,IAAA;AAOpD,OAAO,SAASC,mBAAmBA,CAAC;EAClCC,eAAe,EAAEC,gBAAgB;EACjC,GAAGC;AACqB,CAAC,EAAE;EAC3B,oBAAOJ,IAAA,CAACF,IAAI;IAAA,GAAKM;EAAK,CAAG,CAAC;AAC5B","ignoreList":[]}
1
+ {"version":3,"names":["useEffect","View","jsx","_jsx","ReservedRegionsView","onRegionsChange","props","nativeEvent","regions"],"sourceRoot":"../../src","sources":["ReservedRegionsView.tsx"],"mappings":";;AAAA,SAASA,SAAS,QAAQ,OAAO;AACjC,SAASC,IAAI,QAAwB,cAAc;AAAC,SAAAC,GAAA,IAAAC,IAAA;AAOpD,OAAO,SAASC,mBAAmBA,CAAC;EAAEC,eAAe;EAAE,GAAGC;AAAgC,CAAC,EAAE;EAC3FN,SAAS,CAAC,MAAM;IACdK,eAAe,GAAG;MAAEE,WAAW,EAAE;QAAEC,OAAO,EAAE;MAAG;IAAE,CAAC,CAAC;EACrD,CAAC,EAAE,CAACH,eAAe,CAAC,CAAC;EACrB,oBAAOF,IAAA,CAACF,IAAI;IAAA,GAAKK;EAAK,CAAG,CAAC;AAC5B","ignoreList":[]}
@@ -1,8 +1,4 @@
1
- import {
2
- codegenNativeComponent,
3
- type CodegenTypes,
4
- type ViewProps,
5
- } from 'react-native';
1
+ import { codegenNativeComponent, type CodegenTypes, type ViewProps } from 'react-native';
6
2
 
7
3
  export type RegionsChangeEvent = Readonly<{
8
4
  regions: {
@@ -14,29 +14,37 @@ export function ReservedRegionsProvider({
14
14
  children,
15
15
  ...props
16
16
  }) {
17
- const [regions, setRegions] = React.useState([]);
17
+ const [snapshot, setSnapshot] = React.useState({
18
+ regions: [],
19
+ isReady: false
20
+ });
21
+ const onRegionsChange = React.useCallback(event => {
22
+ const regions = event.nativeEvent.regions.flatMap(region => {
23
+ if (region.kind === 'division') {
24
+ return [{
25
+ kind: 'division',
26
+ frame: region.frame,
27
+ occludesContent: region.occludesContent
28
+ }];
29
+ }
30
+ if (region.kind === 'occlusion') {
31
+ return [{
32
+ kind: 'occlusion',
33
+ frame: region.frame
34
+ }];
35
+ }
36
+ return [];
37
+ });
38
+ setSnapshot({
39
+ regions,
40
+ isReady: true
41
+ });
42
+ }, []);
18
43
  return /*#__PURE__*/_jsx(ReservedRegionsContext.Provider, {
19
- value: regions,
44
+ value: snapshot,
20
45
  children: /*#__PURE__*/_jsx(ReservedRegionsView, {
21
46
  ...props,
22
- onRegionsChange: event => {
23
- setRegions(event.nativeEvent.regions.flatMap(region => {
24
- if (region.kind === 'division') {
25
- return [{
26
- kind: 'division',
27
- frame: region.frame,
28
- occludesContent: region.occludesContent
29
- }];
30
- }
31
- if (region.kind === 'occlusion') {
32
- return [{
33
- kind: 'occlusion',
34
- frame: region.frame
35
- }];
36
- }
37
- return [];
38
- }));
39
- },
47
+ onRegionsChange: onRegionsChange,
40
48
  children: children
41
49
  })
42
50
  });
@@ -44,10 +52,22 @@ export function ReservedRegionsProvider({
44
52
 
45
53
  /** Returns active regions reported by the nearest ReservedRegionsProvider. */
46
54
  export function useReservedRegions() {
47
- const regions = React.useContext(ReservedRegionsContext);
48
- if (regions === null) {
55
+ const snapshot = React.useContext(ReservedRegionsContext);
56
+ if (snapshot === null) {
49
57
  throw new Error('useReservedRegions must be used inside ReservedRegionsProvider');
50
58
  }
51
- return regions;
59
+ return snapshot.regions;
60
+ }
61
+
62
+ /**
63
+ * Whether the nearest provider has reported its first measurement, including an empty
64
+ * result. Remains true for that provider's lifetime; does not imply platform support.
65
+ */
66
+ export function useReservedRegionsReady() {
67
+ const snapshot = React.useContext(ReservedRegionsContext);
68
+ if (snapshot === null) {
69
+ throw new Error('useReservedRegionsReady must be used inside ReservedRegionsProvider');
70
+ }
71
+ return snapshot.isReady;
52
72
  }
53
73
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"names":["React","ReservedRegionsView","jsx","_jsx","ReservedRegionsContext","createContext","ReservedRegionsProvider","children","props","regions","setRegions","useState","Provider","value","onRegionsChange","event","nativeEvent","flatMap","region","kind","frame","occludesContent","useReservedRegions","useContext","Error"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":";;AAAA,OAAO,KAAKA,KAAK,MAAM,OAAO;AAE9B,SAASC,mBAAmB,QAAQ,uBAAuB;;AAE3D;;AAYA;AAAA,SAAAC,GAAA,IAAAC,IAAA;AAiBA,MAAMC,sBAAsB,gBAAGJ,KAAK,CAACK,aAAa,CAEhD,IAAI,CAAC;;AAEP;AACA,OAAO,SAASC,uBAAuBA,CAAC;EACtCC,QAAQ;EACR,GAAGC;AACM,CAAC,EAAqB;EAC/B,MAAM,CAACC,OAAO,EAAEC,UAAU,CAAC,GAAGV,KAAK,CAACW,QAAQ,CAA4B,EAAE,CAAC;EAE3E,oBACER,IAAA,CAACC,sBAAsB,CAACQ,QAAQ;IAACC,KAAK,EAAEJ,OAAQ;IAAAF,QAAA,eAC9CJ,IAAA,CAACF,mBAAmB;MAAA,GACdO,KAAK;MACTM,eAAe,EAAGC,KAAK,IAAK;QAC1BL,UAAU,CACRK,KAAK,CAACC,WAAW,CAACP,OAAO,CAACQ,OAAO,CAAEC,MAAM,IAAuB;UAC9D,IAAIA,MAAM,CAACC,IAAI,KAAK,UAAU,EAAE;YAC9B,OAAO,CACL;cACEA,IAAI,EAAE,UAAmB;cACzBC,KAAK,EAAEF,MAAM,CAACE,KAAK;cACnBC,eAAe,EAAEH,MAAM,CAACG;YAC1B,CAAC,CACF;UACH;UACA,IAAIH,MAAM,CAACC,IAAI,KAAK,WAAW,EAAE;YAC/B,OAAO,CAAC;cAAEA,IAAI,EAAE,WAAoB;cAAEC,KAAK,EAAEF,MAAM,CAACE;YAAM,CAAC,CAAC;UAC9D;UACA,OAAO,EAAE;QACX,CAAC,CACH,CAAC;MACH,CAAE;MAAAb,QAAA,EAEDA;IAAQ,CACU;EAAC,CACS,CAAC;AAEtC;;AAEA;AACA,OAAO,SAASe,kBAAkBA,CAAA,EAA8B;EAC9D,MAAMb,OAAO,GAAGT,KAAK,CAACuB,UAAU,CAACnB,sBAAsB,CAAC;EACxD,IAAIK,OAAO,KAAK,IAAI,EAAE;IACpB,MAAM,IAAIe,KAAK,CACb,gEACF,CAAC;EACH;EACA,OAAOf,OAAO;AAChB","ignoreList":[]}
1
+ {"version":3,"names":["React","ReservedRegionsView","jsx","_jsx","ReservedRegionsContext","createContext","ReservedRegionsProvider","children","props","snapshot","setSnapshot","useState","regions","isReady","onRegionsChange","useCallback","event","nativeEvent","flatMap","region","kind","frame","occludesContent","Provider","value","useReservedRegions","useContext","Error","useReservedRegionsReady"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":";;AAAA,OAAO,KAAKA,KAAK,MAAM,OAAO;AAE9B,SAASC,mBAAmB,QAAQ,uBAAuB;;AAG3D;;AAYA;AAAA,SAAAC,GAAA,IAAAC,IAAA;AAsBA,MAAMC,sBAAsB,gBAAGJ,KAAK,CAACK,aAAa,CAAiC,IAAI,CAAC;;AAExF;AACA,OAAO,SAASC,uBAAuBA,CAAC;EAAEC,QAAQ;EAAE,GAAGC;AAAiB,CAAC,EAAqB;EAC5F,MAAM,CAACC,QAAQ,EAAEC,WAAW,CAAC,GAAGV,KAAK,CAACW,QAAQ,CAA0B;IAAEC,OAAO,EAAE,EAAE;IAAEC,OAAO,EAAE;EAAM,CAAC,CAAC;EACxG,MAAMC,eAAe,GAAGd,KAAK,CAACe,WAAW,CAAEC,KAA0C,IAAK;IACxF,MAAMJ,OAAO,GAAGI,KAAK,CAACC,WAAW,CAACL,OAAO,CAACM,OAAO,CAAEC,MAAM,IAAuB;MAC9E,IAAIA,MAAM,CAACC,IAAI,KAAK,UAAU,EAAE;QAC9B,OAAO,CAAC;UAAEA,IAAI,EAAE,UAAU;UAAEC,KAAK,EAAEF,MAAM,CAACE,KAAK;UAAEC,eAAe,EAAEH,MAAM,CAACG;QAAgB,CAAC,CAAC;MAC7F;MACA,IAAIH,MAAM,CAACC,IAAI,KAAK,WAAW,EAAE;QAC/B,OAAO,CAAC;UAAEA,IAAI,EAAE,WAAW;UAAEC,KAAK,EAAEF,MAAM,CAACE;QAAM,CAAC,CAAC;MACrD;MACA,OAAO,EAAE;IACX,CAAC,CAAC;IACFX,WAAW,CAAC;MAAEE,OAAO;MAAEC,OAAO,EAAE;IAAK,CAAC,CAAC;EACzC,CAAC,EAAE,EAAE,CAAC;EAEN,oBACEV,IAAA,CAACC,sBAAsB,CAACmB,QAAQ;IAACC,KAAK,EAAEf,QAAS;IAAAF,QAAA,eAC/CJ,IAAA,CAACF,mBAAmB;MAAA,GAAKO,KAAK;MAAEM,eAAe,EAAEA,eAAgB;MAAAP,QAAA,EAC9DA;IAAQ,CACU;EAAC,CACS,CAAC;AAEtC;;AAEA;AACA,OAAO,SAASkB,kBAAkBA,CAAA,EAA8B;EAC9D,MAAMhB,QAAQ,GAAGT,KAAK,CAAC0B,UAAU,CAACtB,sBAAsB,CAAC;EACzD,IAAIK,QAAQ,KAAK,IAAI,EAAE;IACrB,MAAM,IAAIkB,KAAK,CAAC,gEAAgE,CAAC;EACnF;EACA,OAAOlB,QAAQ,CAACG,OAAO;AACzB;;AAEA;AACA;AACA;AACA;AACA,OAAO,SAASgB,uBAAuBA,CAAA,EAAY;EACjD,MAAMnB,QAAQ,GAAGT,KAAK,CAAC0B,UAAU,CAACtB,sBAAsB,CAAC;EACzD,IAAIK,QAAQ,KAAK,IAAI,EAAE;IACrB,MAAM,IAAIkB,KAAK,CAAC,qEAAqE,CAAC;EACxF;EACA,OAAOlB,QAAQ,CAACI,OAAO;AACzB","ignoreList":[]}
@@ -5,5 +5,5 @@ export type ReservedRegionsViewProps = ViewProps & {
5
5
  nativeEvent: RegionsChangeEvent;
6
6
  }) => void;
7
7
  };
8
- export declare function ReservedRegionsView({ onRegionsChange: _onRegionsChange, ...props }: ReservedRegionsViewProps): import("react").JSX.Element;
8
+ export declare function ReservedRegionsView({ onRegionsChange, ...props }: ReservedRegionsViewProps): import("react").JSX.Element;
9
9
  //# sourceMappingURL=ReservedRegionsView.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ReservedRegionsView.d.ts","sourceRoot":"","sources":["../../../src/ReservedRegionsView.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AACpD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sCAAsC,CAAC;AAE/E,MAAM,MAAM,wBAAwB,GAAG,SAAS,GAAG;IACjD,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,WAAW,EAAE,kBAAkB,CAAA;KAAE,KAAK,IAAI,CAAC;CACxE,CAAC;AAEF,wBAAgB,mBAAmB,CAAC,EAClC,eAAe,EAAE,gBAAgB,EACjC,GAAG,KAAK,EACT,EAAE,wBAAwB,+BAE1B"}
1
+ {"version":3,"file":"ReservedRegionsView.d.ts","sourceRoot":"","sources":["../../../src/ReservedRegionsView.tsx"],"names":[],"mappings":"AACA,OAAO,EAAQ,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AACpD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sCAAsC,CAAC;AAE/E,MAAM,MAAM,wBAAwB,GAAG,SAAS,GAAG;IACjD,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,WAAW,EAAE,kBAAkB,CAAA;KAAE,KAAK,IAAI,CAAC;CACxE,CAAC;AAEF,wBAAgB,mBAAmB,CAAC,EAAE,eAAe,EAAE,GAAG,KAAK,EAAE,EAAE,wBAAwB,+BAK1F"}
@@ -1 +1 @@
1
- {"version":3,"file":"ReservedRegionsViewNativeComponent.d.ts","sourceRoot":"","sources":["../../../src/ReservedRegionsViewNativeComponent.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,YAAY,EACjB,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAEtB,MAAM,MAAM,kBAAkB,GAAG,QAAQ,CAAC;IACxC,OAAO,EAAE;QACP,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,EAAE;YACL,CAAC,EAAE,YAAY,CAAC,MAAM,CAAC;YACvB,CAAC,EAAE,YAAY,CAAC,MAAM,CAAC;YACvB,KAAK,EAAE,YAAY,CAAC,MAAM,CAAC;YAC3B,MAAM,EAAE,YAAY,CAAC,MAAM,CAAC;SAC7B,CAAC;QACF,eAAe,EAAE,OAAO,CAAC;KAC1B,EAAE,CAAC;CACL,CAAC,CAAC;AAEH,MAAM,WAAW,WAAY,SAAQ,SAAS;IAC5C,eAAe,CAAC,EAAE,YAAY,CAAC,kBAAkB,CAAC,kBAAkB,CAAC,CAAC;CACvE;;AAED,wBAA0E"}
1
+ {"version":3,"file":"ReservedRegionsViewNativeComponent.d.ts","sourceRoot":"","sources":["../../../src/ReservedRegionsViewNativeComponent.ts"],"names":[],"mappings":"AAAA,OAAO,EAA0B,KAAK,YAAY,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzF,MAAM,MAAM,kBAAkB,GAAG,QAAQ,CAAC;IACxC,OAAO,EAAE;QACP,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,EAAE;YACL,CAAC,EAAE,YAAY,CAAC,MAAM,CAAC;YACvB,CAAC,EAAE,YAAY,CAAC,MAAM,CAAC;YACvB,KAAK,EAAE,YAAY,CAAC,MAAM,CAAC;YAC3B,MAAM,EAAE,YAAY,CAAC,MAAM,CAAC;SAC7B,CAAC;QACF,eAAe,EAAE,OAAO,CAAC;KAC1B,EAAE,CAAC;CACL,CAAC,CAAC;AAEH,MAAM,WAAW,WAAY,SAAQ,SAAS;IAC5C,eAAe,CAAC,EAAE,YAAY,CAAC,kBAAkB,CAAC,kBAAkB,CAAC,CAAC;CACvE"}
@@ -29,4 +29,9 @@ export type ReservedRegion = Readonly<{
29
29
  export declare function ReservedRegionsProvider({ children, ...props }: ViewProps): React.JSX.Element;
30
30
  /** Returns active regions reported by the nearest ReservedRegionsProvider. */
31
31
  export declare function useReservedRegions(): readonly ReservedRegion[];
32
+ /**
33
+ * Whether the nearest provider has reported its first measurement, including an empty
34
+ * result. Remains true for that provider's lifetime; does not imply platform support.
35
+ */
36
+ export declare function useReservedRegionsReady(): boolean;
32
37
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAG9C,qFAAqF;AACrF,MAAM,MAAM,mBAAmB,GAAG,QAAQ,CAAC;IACzC,8CAA8C;IAC9C,CAAC,EAAE,MAAM,CAAC;IACV,6CAA6C;IAC7C,CAAC,EAAE,MAAM,CAAC;IACV,8CAA8C;IAC9C,KAAK,EAAE,MAAM,CAAC;IACd,4CAA4C;IAC5C,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC,CAAC;AAEH,2DAA2D;AAC3D,MAAM,MAAM,cAAc,GACtB,QAAQ,CAAC;IACP,sDAAsD;IACtD,IAAI,EAAE,UAAU,CAAC;IACjB,gEAAgE;IAChE,KAAK,EAAE,mBAAmB,CAAC;IAC3B,yDAAyD;IACzD,eAAe,EAAE,OAAO,CAAC;CAC1B,CAAC,GACF,QAAQ,CAAC;IACP,gEAAgE;IAChE,IAAI,EAAE,WAAW,CAAC;IAClB,iEAAiE;IACjE,KAAK,EAAE,mBAAmB,CAAC;CAC5B,CAAC,CAAC;AAMP,sFAAsF;AACtF,wBAAgB,uBAAuB,CAAC,EACtC,QAAQ,EACR,GAAG,KAAK,EACT,EAAE,SAAS,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,CA+B/B;AAED,8EAA8E;AAC9E,wBAAgB,kBAAkB,IAAI,SAAS,cAAc,EAAE,CAQ9D"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAI9C,qFAAqF;AACrF,MAAM,MAAM,mBAAmB,GAAG,QAAQ,CAAC;IACzC,8CAA8C;IAC9C,CAAC,EAAE,MAAM,CAAC;IACV,6CAA6C;IAC7C,CAAC,EAAE,MAAM,CAAC;IACV,8CAA8C;IAC9C,KAAK,EAAE,MAAM,CAAC;IACd,4CAA4C;IAC5C,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC,CAAC;AAEH,2DAA2D;AAC3D,MAAM,MAAM,cAAc,GACtB,QAAQ,CAAC;IACP,sDAAsD;IACtD,IAAI,EAAE,UAAU,CAAC;IACjB,gEAAgE;IAChE,KAAK,EAAE,mBAAmB,CAAC;IAC3B,yDAAyD;IACzD,eAAe,EAAE,OAAO,CAAC;CAC1B,CAAC,GACF,QAAQ,CAAC;IACP,gEAAgE;IAChE,IAAI,EAAE,WAAW,CAAC;IAClB,iEAAiE;IACjE,KAAK,EAAE,mBAAmB,CAAC;CAC5B,CAAC,CAAC;AASP,sFAAsF;AACtF,wBAAgB,uBAAuB,CAAC,EAAE,QAAQ,EAAE,GAAG,KAAK,EAAE,EAAE,SAAS,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,CAsB5F;AAED,8EAA8E;AAC9E,wBAAgB,kBAAkB,IAAI,SAAS,cAAc,EAAE,CAM9D;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,IAAI,OAAO,CAMjD"}
package/package.json CHANGED
@@ -1,16 +1,19 @@
1
1
  {
2
2
  "name": "react-native-reserved-regions",
3
- "version": "0.1.0-alpha.0",
3
+ "version": "0.1.0-alpha.2",
4
4
  "description": "Provider-scoped reserved display regions for React Native",
5
- "main": "./lib/module/index.js",
6
- "types": "./lib/typescript/src/index.d.ts",
7
- "exports": {
8
- ".": {
9
- "react-native-reserved-regions-source": "./src/index.tsx",
10
- "types": "./lib/typescript/src/index.d.ts",
11
- "default": "./lib/module/index.js"
12
- },
13
- "./package.json": "./package.json"
5
+ "keywords": [
6
+ "android",
7
+ "ios",
8
+ "react-native"
9
+ ],
10
+ "homepage": "https://github.com/appandflow/react-native-reserved-regions#readme",
11
+ "bugs": "https://github.com/appandflow/react-native-reserved-regions/issues",
12
+ "license": "MIT",
13
+ "author": "Janic Duplessis <janicduplessis@gmail.com> (https://github.com/janicduplessis)",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/appandflow/react-native-reserved-regions.git"
14
17
  },
15
18
  "files": [
16
19
  "src",
@@ -31,80 +34,63 @@
31
34
  "!**/__mocks__",
32
35
  "!**/.*"
33
36
  ],
34
- "scripts": {
35
- "example": "yarn workspace react-native-reserved-regions-example",
36
- "clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib",
37
- "prepare": "bob build",
38
- "typecheck": "tsc",
39
- "lint": "eslint \"**/*.{js,ts,tsx}\"",
40
- "test": "jest"
37
+ "main": "./lib/module/index.js",
38
+ "types": "./lib/typescript/src/index.d.ts",
39
+ "exports": {
40
+ ".": {
41
+ "react-native-reserved-regions-source": "./src/index.tsx",
42
+ "types": "./lib/typescript/src/index.d.ts",
43
+ "default": "./lib/module/index.js"
44
+ },
45
+ "./package.json": "./package.json"
41
46
  },
42
- "keywords": [
43
- "react-native",
44
- "ios",
45
- "android"
46
- ],
47
- "author": "Janic Duplessis <janicduplessis@gmail.com> (https://github.com/janicduplessis)",
48
- "license": "MIT",
49
47
  "publishConfig": {
50
48
  "registry": "https://registry.npmjs.org/"
51
49
  },
52
50
  "devDependencies": {
53
- "@eslint/compat": "^2.1.0",
54
- "@eslint/eslintrc": "^3.3.6",
55
- "@eslint/js": "^9.39.5",
56
51
  "@jest/globals": "^29.7.0",
57
52
  "@react-native/babel-preset": "0.88.0-rc.1",
58
- "@react-native/eslint-config": "0.86.2",
59
53
  "@react-native/jest-preset": "0.86.2",
60
54
  "@types/react": "^19.2.0",
61
55
  "@types/react-test-renderer": "^19.1.0",
62
56
  "del-cli": "^7.0.0",
63
- "eslint": "^9.39.5",
64
- "eslint-config-prettier": "^10.1.8",
65
- "eslint-plugin-ft-flow": "^3.0.11",
66
- "eslint-plugin-prettier": "^5.5.6",
67
57
  "jest": "^29.7.0",
68
- "prettier": "^3.9.6",
58
+ "oxfmt": "0.65.0",
59
+ "oxlint": "1.80.0",
69
60
  "react": "19.3.0",
70
61
  "react-native": "0.88.0-rc.1",
71
62
  "react-native-builder-bob": "^0.43.1",
72
63
  "react-test-renderer": "19.3.0",
73
- "turbo": "^2.10.8",
74
- "typescript": "^6.0.3"
64
+ "typescript": "7.0.2"
75
65
  },
76
66
  "peerDependencies": {
77
67
  "react": "*",
78
68
  "react-native": "*"
79
69
  },
80
- "workspaces": [
81
- "example"
82
- ],
83
- "packageManager": "yarn@4.11.0",
84
- "react-native-builder-bob": {
85
- "source": "src",
86
- "output": "lib",
87
- "targets": [
88
- [
89
- "module",
90
- {
91
- "esm": true
92
- }
93
- ],
94
- [
95
- "typescript",
96
- {
97
- "project": "tsconfig.build.json"
98
- }
70
+ "jest": {
71
+ "modulePathIgnorePatterns": [
72
+ "<rootDir>/example/node_modules",
73
+ "<rootDir>/lib/",
74
+ "<rootDir>/website/"
75
+ ],
76
+ "preset": "@react-native/jest-preset",
77
+ "testEnvironmentOptions": {
78
+ "customExportConditions": [
79
+ "require",
80
+ "react-native",
81
+ "react-native-reserved-regions-source"
99
82
  ]
100
- ]
83
+ }
84
+ },
85
+ "engines": {
86
+ "node": ">=22.18.0"
101
87
  },
102
88
  "codegenConfig": {
103
89
  "name": "ReservedRegionsViewSpec",
104
90
  "type": "all",
105
91
  "jsSrcsDir": "src",
106
92
  "android": {
107
- "javaPackageName": "com.reservedregions"
93
+ "javaPackageName": "com.appandflow.reservedregions"
108
94
  },
109
95
  "ios": {
110
96
  "components": {
@@ -114,27 +100,6 @@
114
100
  }
115
101
  }
116
102
  },
117
- "prettier": {
118
- "quoteProps": "consistent",
119
- "singleQuote": true,
120
- "tabWidth": 2,
121
- "trailingComma": "es5",
122
- "useTabs": false
123
- },
124
- "jest": {
125
- "preset": "@react-native/jest-preset",
126
- "testEnvironmentOptions": {
127
- "customExportConditions": [
128
- "require",
129
- "react-native",
130
- "react-native-reserved-regions-source"
131
- ]
132
- },
133
- "modulePathIgnorePatterns": [
134
- "<rootDir>/example/node_modules",
135
- "<rootDir>/lib/"
136
- ]
137
- },
138
103
  "create-react-native-library": {
139
104
  "type": "fabric-view",
140
105
  "languages": "kotlin-objc",
@@ -143,5 +108,36 @@
143
108
  "jest"
144
109
  ],
145
110
  "version": "0.63.1"
111
+ },
112
+ "react-native-builder-bob": {
113
+ "source": "src",
114
+ "output": "lib",
115
+ "targets": [
116
+ [
117
+ "module",
118
+ {
119
+ "esm": true
120
+ }
121
+ ],
122
+ [
123
+ "typescript",
124
+ {
125
+ "project": "tsconfig.build.json"
126
+ }
127
+ ]
128
+ ]
129
+ },
130
+ "scripts": {
131
+ "example": "pnpm --filter react-native-reserved-regions-example",
132
+ "clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib",
133
+ "typecheck": "tsc --noEmit",
134
+ "lint": "oxlint .",
135
+ "test": "jest --runInBand --watchman=false",
136
+ "build": "bob build",
137
+ "lint:fix": "oxlint --fix .",
138
+ "format": "oxfmt .",
139
+ "format:check": "oxfmt --check .",
140
+ "docs:build": "pnpm --filter reserved-regions-docs build",
141
+ "docs:start": "pnpm --filter reserved-regions-docs start"
146
142
  }
147
- }
143
+ }
@@ -1,3 +1,4 @@
1
+ import { useEffect } from 'react';
1
2
  import { View, type ViewProps } from 'react-native';
2
3
  import type { RegionsChangeEvent } from './ReservedRegionsViewNativeComponent';
3
4
 
@@ -5,9 +6,9 @@ export type ReservedRegionsViewProps = ViewProps & {
5
6
  onRegionsChange?: (event: { nativeEvent: RegionsChangeEvent }) => void;
6
7
  };
7
8
 
8
- export function ReservedRegionsView({
9
- onRegionsChange: _onRegionsChange,
10
- ...props
11
- }: ReservedRegionsViewProps) {
9
+ export function ReservedRegionsView({ onRegionsChange, ...props }: ReservedRegionsViewProps) {
10
+ useEffect(() => {
11
+ onRegionsChange?.({ nativeEvent: { regions: [] } });
12
+ }, [onRegionsChange]);
12
13
  return <View {...props} />;
13
14
  }
@@ -1,8 +1,4 @@
1
- import {
2
- codegenNativeComponent,
3
- type CodegenTypes,
4
- type ViewProps,
5
- } from 'react-native';
1
+ import { codegenNativeComponent, type CodegenTypes, type ViewProps } from 'react-native';
6
2
 
7
3
  export type RegionsChangeEvent = Readonly<{
8
4
  regions: {
package/src/index.tsx CHANGED
@@ -1,6 +1,7 @@
1
1
  import * as React from 'react';
2
2
  import type { ViewProps } from 'react-native';
3
3
  import { ReservedRegionsView } from './ReservedRegionsView';
4
+ import type { RegionsChangeEvent } from './ReservedRegionsViewNativeComponent';
4
5
 
5
6
  /** A rectangle in logical points relative to the nearest ReservedRegionsProvider. */
6
7
  export type ReservedRegionFrame = Readonly<{
@@ -31,41 +32,32 @@ export type ReservedRegion =
31
32
  frame: ReservedRegionFrame;
32
33
  }>;
33
34
 
34
- const ReservedRegionsContext = React.createContext<
35
- readonly ReservedRegion[] | null
36
- >(null);
35
+ type ReservedRegionsSnapshot = Readonly<{
36
+ regions: readonly ReservedRegion[];
37
+ isReady: boolean;
38
+ }>;
39
+
40
+ const ReservedRegionsContext = React.createContext<ReservedRegionsSnapshot | null>(null);
37
41
 
38
42
  /** Makes active reserved regions available to descendants in provider coordinates. */
39
- export function ReservedRegionsProvider({
40
- children,
41
- ...props
42
- }: ViewProps): React.JSX.Element {
43
- const [regions, setRegions] = React.useState<readonly ReservedRegion[]>([]);
43
+ export function ReservedRegionsProvider({ children, ...props }: ViewProps): React.JSX.Element {
44
+ const [snapshot, setSnapshot] = React.useState<ReservedRegionsSnapshot>({ regions: [], isReady: false });
45
+ const onRegionsChange = React.useCallback((event: { nativeEvent: RegionsChangeEvent }) => {
46
+ const regions = event.nativeEvent.regions.flatMap((region): ReservedRegion[] => {
47
+ if (region.kind === 'division') {
48
+ return [{ kind: 'division', frame: region.frame, occludesContent: region.occludesContent }];
49
+ }
50
+ if (region.kind === 'occlusion') {
51
+ return [{ kind: 'occlusion', frame: region.frame }];
52
+ }
53
+ return [];
54
+ });
55
+ setSnapshot({ regions, isReady: true });
56
+ }, []);
44
57
 
45
58
  return (
46
- <ReservedRegionsContext.Provider value={regions}>
47
- <ReservedRegionsView
48
- {...props}
49
- onRegionsChange={(event) => {
50
- setRegions(
51
- event.nativeEvent.regions.flatMap((region): ReservedRegion[] => {
52
- if (region.kind === 'division') {
53
- return [
54
- {
55
- kind: 'division' as const,
56
- frame: region.frame,
57
- occludesContent: region.occludesContent,
58
- },
59
- ];
60
- }
61
- if (region.kind === 'occlusion') {
62
- return [{ kind: 'occlusion' as const, frame: region.frame }];
63
- }
64
- return [];
65
- })
66
- );
67
- }}
68
- >
59
+ <ReservedRegionsContext.Provider value={snapshot}>
60
+ <ReservedRegionsView {...props} onRegionsChange={onRegionsChange}>
69
61
  {children}
70
62
  </ReservedRegionsView>
71
63
  </ReservedRegionsContext.Provider>
@@ -74,11 +66,21 @@ export function ReservedRegionsProvider({
74
66
 
75
67
  /** Returns active regions reported by the nearest ReservedRegionsProvider. */
76
68
  export function useReservedRegions(): readonly ReservedRegion[] {
77
- const regions = React.useContext(ReservedRegionsContext);
78
- if (regions === null) {
79
- throw new Error(
80
- 'useReservedRegions must be used inside ReservedRegionsProvider'
81
- );
69
+ const snapshot = React.useContext(ReservedRegionsContext);
70
+ if (snapshot === null) {
71
+ throw new Error('useReservedRegions must be used inside ReservedRegionsProvider');
72
+ }
73
+ return snapshot.regions;
74
+ }
75
+
76
+ /**
77
+ * Whether the nearest provider has reported its first measurement, including an empty
78
+ * result. Remains true for that provider's lifetime; does not imply platform support.
79
+ */
80
+ export function useReservedRegionsReady(): boolean {
81
+ const snapshot = React.useContext(ReservedRegionsContext);
82
+ if (snapshot === null) {
83
+ throw new Error('useReservedRegionsReady must be used inside ReservedRegionsProvider');
82
84
  }
83
- return regions;
85
+ return snapshot.isReady;
84
86
  }