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

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,18 @@ 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.3` is an alpha release; its API may still change.
17
+
15
18
  ```sh
16
- npm install react-native-reserved-regions
19
+ npm install react-native-reserved-regions@0.1.0-alpha.3
17
20
  ```
18
21
 
19
22
  On iOS, install pods in your app's `ios` directory. Android links automatically through React Native autolinking.
@@ -21,13 +24,12 @@ On iOS, install pods in your app's `ios` directory. Android links automatically
21
24
  ## Usage
22
25
 
23
26
  ```tsx
24
- import {
25
- ReservedRegionsProvider,
26
- useReservedRegions,
27
- } from 'react-native-reserved-regions';
27
+ import { ReservedRegionsProvider, useReservedRegions, useReservedRegionsReady } from 'react-native-reserved-regions';
28
28
 
29
29
  function Screen() {
30
30
  const regions = useReservedRegions();
31
+ const isReady = useReservedRegionsReady();
32
+ if (!isReady) return null;
31
33
  return regions.map((region, index) => {
32
34
  if (region.kind === 'division') {
33
35
  console.log(region.frame, region.occludesContent);
@@ -47,7 +49,7 @@ export default function App() {
47
49
  }
48
50
  ```
49
51
 
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.
52
+ `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
53
 
52
54
  The public type is a tagged union:
53
55
 
@@ -66,24 +68,69 @@ type ReservedRegion =
66
68
 
67
69
  A division splits the available display. `occludesContent` tells you whether content under that division is hidden. An occlusion is an area where content is hidden. On iOS, division regions currently report `occludesContent: false`. The TypeScript declarations in `src/index.tsx` document every field.
68
70
 
71
+ ## Why a provider and a hook?
72
+
73
+ A reserved region needs a coordinate space: the same fold has different local
74
+ coordinates in a full-screen view and an inset panel. `ReservedRegionsProvider`
75
+ renders the native view that defines those bounds and shares its measurements
76
+ with descendants. `useReservedRegions()` reads the nearest provider, so several
77
+ consumers can use the same measurements without each adding a native view. Place
78
+ a provider around each area that needs its own coordinate space; it can replace
79
+ an existing container `View`.
80
+
81
+ [react-native-hinges](https://appandflow.github.io/react-native-hinges/docs/usage)
82
+ exposes a hook without a provider because posture and angle do not depend on a
83
+ child view's position or size. It observes the existing React root's hierarchy/window.
84
+ Use both libraries when layout and animation need those separate inputs.
85
+
69
86
  ## Native sources
70
87
 
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/)
88
+ - 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
89
  - 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
90
 
74
91
  The library reports geometry and does not reposition content. Normal safe area insets, system bars, and keyboard insets remain separate concerns.
75
92
 
93
+ ## Measurement timing
94
+
95
+ Native events request synchronous delivery. Android measures after Fabric mounting
96
+ and also observes pre-draw changes. The iOS example tests same-frame delivery with
97
+ an [upstream React Native event-beat patch](docs/workflow.md#react-native-event-beat-test-patch).
98
+ That example patch is not installed into consuming apps. Readiness indicates a
99
+ completed measurement, not a universal first-frame guarantee.
100
+
101
+ ### Readiness-gated animated content
102
+
103
+ On the tested RN `0.88.0-rc.1` Android stack, conditionally mounting Reanimated
104
+ `4.7.0` content when `useReservedRegionsReady()` becomes true can throw
105
+ `__requestMapperRunFinalizer` is undefined: Worklets `0.13.0` can run synchronous
106
+ UI work ahead of queued mapper initialization. The combined hinges example uses
107
+ an [example-only Worklets FIFO patch](https://github.com/appandflow/react-native-hinges/blob/main/patches/react-native-worklets%400.13.0.patch).
108
+ Installing either library does not patch a consuming app's Worklets dependency.
109
+ Keeping the animated subtree mounted avoided this failure in the tested case;
110
+ apps that gate its mount need to apply the patch, rebuild the native app, and
111
+ validate it themselves.
112
+
113
+ The [performance guide](https://appandflow.github.io/react-native-reserved-regions/docs/performance)
114
+ explains provider placement and optional readiness gating. `ReservedRegionsGate`
115
+ is included starting with `0.1.0-alpha.3`.
116
+
76
117
  ## Development
77
118
 
78
119
  ```sh
79
- yarn install
80
- yarn typecheck
81
- yarn lint
82
- yarn test --watch=false
83
- yarn prepare
120
+ pnpm install --frozen-lockfile
121
+ pnpm run format:check
122
+ pnpm run lint
123
+ pnpm run typecheck
124
+ pnpm test
125
+ pnpm run build
126
+ pnpm run docs:build
84
127
  ```
85
128
 
86
- To run the example, use `yarn example start`, then `yarn example ios` or `yarn example android`.
129
+ The example includes full-screen, inset and content-box providers with
130
+ `react-native-safe-area-context` values and overlays for comparison.
131
+ See [the development workflow](docs/workflow.md) for native runs,
132
+ [the docs website](website/README.md) for local previews, and
133
+ [the release process](RELEASE.md) for publishing.
87
134
 
88
135
  ## License
89
136
 
@@ -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,51 +1,96 @@
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
37
+ private var awaitingFirstMount = true
29
38
  private var onRegionsChange: RegionsChangeHandler? = null
30
39
 
40
+ override fun willDispatchViewUpdates(uiManager: UIManager) {}
41
+
42
+ override fun willMountItems(uiManager: UIManager) {}
43
+
44
+ override fun didScheduleMountItems(uiManager: UIManager) {}
45
+
46
+ override fun didDispatchMountItems(uiManager: UIManager) {}
47
+
48
+ override fun didMountItems(uiManager: UIManager) {
49
+ if (awaitingFirstMount) updateRegions()
50
+ }
51
+
31
52
  override fun onAttachedToWindow() {
32
53
  super.onAttachedToWindow()
54
+ uiManager = UIManagerHelper.getUIManagerForReactTag(UIManagerHelper.getReactContext(this), id)
55
+ awaitingFirstMount = true
56
+ // React Native's FabricMountingManager.cpp applies layout before installing a view's event
57
+ // emitter inside one mount batch, so onLayout cannot dispatch a synchronous event on first
58
+ // mount; didMountItems is the earliest point after the emitter exists and before the event
59
+ // beat. It runs for every mount batch in the app, so it is removed after the first delivery.
60
+ uiManager?.addUIManagerEventListener(this)
33
61
  viewTreeObserver.addOnPreDrawListener(this)
62
+ foldingFeatures = emptyList()
63
+ foldingFeaturesReady = false
64
+ lastRegions = null
34
65
  val activity = (context as? ThemedReactContext)?.currentActivity
35
66
  if (activity != null) {
36
- tracker = WindowInfoTrackerCallbackAdapter(WindowInfoTracker.getOrCreate(context))
67
+ val windowTracker = WindowInfoTracker.getOrCreate(context)
68
+ if (WindowSdkExtensions.getInstance().extensionVersion >= 9) {
69
+ foldingFeatures = windowTracker.getCurrentWindowLayoutInfo(activity)
70
+ .displayFeatures.filterIsInstance<FoldingFeature>()
71
+ foldingFeaturesReady = true
72
+ }
73
+ tracker = WindowInfoTrackerCallbackAdapter(windowTracker)
37
74
  tracker?.addWindowLayoutInfoListener(activity, ContextCompat.getMainExecutor(context), layoutInfoConsumer)
75
+ } else {
76
+ foldingFeaturesReady = true
38
77
  }
39
- updateRegions()
40
78
  }
41
79
 
42
80
  override fun onDetachedFromWindow() {
81
+ uiManager?.removeUIManagerEventListener(this)
82
+ uiManager = null
43
83
  tracker?.removeWindowLayoutInfoListener(layoutInfoConsumer)
44
84
  tracker = null
45
85
  viewTreeObserver.removeOnPreDrawListener(this)
46
86
  super.onDetachedFromWindow()
47
87
  }
48
88
 
89
+ override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
90
+ super.onLayout(changed, left, top, right, bottom)
91
+ if (!awaitingFirstMount) updateRegions()
92
+ }
93
+
49
94
  override fun onPreDraw(): Boolean {
50
95
  updateRegions()
51
96
  return true
@@ -53,12 +98,13 @@ class ReservedRegionsView(context: Context) : ReactViewGroup(context), ViewTreeO
53
98
 
54
99
  internal fun setOnRegionsChangeHandler(handler: RegionsChangeHandler) {
55
100
  onRegionsChange = handler
56
- updateRegions()
101
+ lastRegions = null
102
+ invalidate()
57
103
  }
58
104
 
59
- private fun updateRegions() {
60
- val handler = onRegionsChange ?: return
61
- if (!isAttachedToWindow || width == 0 || height == 0) return
105
+ private fun updateRegions(): Boolean {
106
+ val handler = onRegionsChange ?: return false
107
+ if (!isAttachedToWindow || !foldingFeaturesReady || width == 0 || height == 0) return false
62
108
 
63
109
  val windowLocation = IntArray(2)
64
110
  getLocationInWindow(windowLocation)
@@ -70,13 +116,7 @@ class ReservedRegionsView(context: Context) : ReactViewGroup(context), ViewTreeO
70
116
  regions.add(ReservedRegion("division", frame, feature.occlusionType == FoldingFeature.OcclusionType.FULL))
71
117
  }
72
118
 
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) {
119
+ if (Build.VERSION.SDK_INT >= 28) {
80
120
  val screenLocation = IntArray(2)
81
121
  getLocationOnScreen(screenLocation)
82
122
  rootWindowInsets?.displayCutout?.boundingRects?.forEach { bounds ->
@@ -86,10 +126,14 @@ class ReservedRegionsView(context: Context) : ReactViewGroup(context), ViewTreeO
86
126
  }
87
127
  }
88
128
 
89
- if (regions != lastRegions) {
90
- lastRegions = regions
91
- handler(this, regions)
129
+ if (regions == lastRegions) return false
130
+ lastRegions = regions
131
+ handler(this, regions)
132
+ if (awaitingFirstMount) {
133
+ awaitingFirstMount = false
134
+ uiManager?.removeUIManagerEventListener(this)
92
135
  }
136
+ return true
93
137
  }
94
138
 
95
139
  private fun clippedFrame(bounds: Rect, originX: Int, originY: Int): Rect? {
@@ -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
 
@@ -25,6 +22,17 @@ using namespace facebook::react;
25
22
  if (self = [super initWithFrame:frame]) {
26
23
  static const auto defaultProps = std::make_shared<const ReservedRegionsViewProps>();
27
24
  _props = defaultProps;
25
+ #if defined(__IPHONE_27_1) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_27_1
26
+ if (@available(iOS 27.1, *)) {
27
+ // UIKit posts no reserved-region change notification, so hinge updates schedule the re-query.
28
+ __weak __typeof(self) weakSelf = self;
29
+ UIHingeInteraction *hingeInteraction =
30
+ [[UIHingeInteraction alloc] initWithUpdateHandler:^(UIHingeInteraction *, UIHingeInteractionUpdate *) {
31
+ [weakSelf setNeedsLayout];
32
+ }];
33
+ [self addInteraction:hingeInteraction];
34
+ }
35
+ #endif
28
36
  }
29
37
  return self;
30
38
  }
@@ -38,62 +46,62 @@ using namespace facebook::react;
38
46
  - (void)didMoveToWindow
39
47
  {
40
48
  [super didMoveToWindow];
41
- [self updateRegions];
49
+ [self setNeedsLayout];
50
+ }
51
+
52
+ - (void)updateEventEmitter:(EventEmitter::Shared const &)eventEmitter
53
+ {
54
+ [super updateEventEmitter:eventEmitter];
55
+ [self setNeedsLayout];
42
56
  }
43
57
 
44
58
  - (void)updateRegions
45
59
  {
46
- if (self.window == nil || !_eventEmitter) {
60
+ if (self.window == nil || !_eventEmitter || CGSizeEqualToSize(self.bounds.size, CGSizeZero)) {
47
61
  return;
48
62
  }
49
63
 
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
64
  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"},
65
+ #if defined(__IPHONE_27_1) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_27_1
66
+ if (@available(iOS 27.1, *)) {
67
+ NSArray<UIViewReservedRegionKind *> *kinds = @[
68
+ UIViewReservedRegionKind.divisionRegionKind,
69
+ UIViewReservedRegionKind.occlusionRegionKind,
59
70
  ];
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
- }];
71
+ NSArray<NSString *> *names = @[@"division", @"occlusion"];
72
+ for (NSUInteger index = 0; index < kinds.count; index++) {
73
+ for (UIViewReservedRegion *region in [self reservedRegionsOfKind:kinds[index]
74
+ options:UIViewReservedRegionQueryOptionsNone]) {
75
+ [regions addObject:@{@"kind": names[index], @"frame": [NSValue valueWithCGRect:region.frame]}];
78
76
  }
79
77
  }
80
78
  }
79
+ #endif
81
80
 
82
81
  if ([_currentRegions isEqualToArray:regions]) {
83
82
  return;
84
83
  }
85
84
  _currentRegions = [regions copy];
86
85
 
87
- ReservedRegionsViewEventEmitter::OnRegionsChange event;
86
+ auto regionPayloads = folly::dynamic::array();
88
87
  for (NSDictionary *region in regions) {
89
88
  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
- });
89
+ regionPayloads.push_back(folly::dynamic::object
90
+ ("kind", [region[@"kind"] UTF8String])
91
+ ("frame", folly::dynamic::object
92
+ ("x", frame.origin.x)
93
+ ("y", frame.origin.y)
94
+ ("width", frame.size.width)
95
+ ("height", frame.size.height))
96
+ ("occludesContent", false));
95
97
  }
96
- std::static_pointer_cast<ReservedRegionsViewEventEmitter const>(_eventEmitter)->onRegionsChange(event);
98
+ auto eventEmitter = _eventEmitter;
99
+ eventEmitter->experimental_flushSync([eventEmitter, regions = std::move(regionPayloads)]() mutable {
100
+ eventEmitter->dispatchEvent(
101
+ "regionsChange",
102
+ folly::dynamic::object("regions", std::move(regions)),
103
+ RawEvent::Category::Discrete);
104
+ });
97
105
  }
98
106
 
99
107
  - (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,29 @@ 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;
72
+ }
73
+
74
+ /** Renders children after the nearest provider reports its first measurement. */
75
+ export function ReservedRegionsGate({
76
+ children
77
+ }) {
78
+ return useReservedRegionsReady() ? children : null;
52
79
  }
53
80
  //# 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","ReservedRegionsGate"],"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;;AAEA;AACA,OAAO,SAASgB,mBAAmBA,CAAC;EAAEtB;AAAkC,CAAC,EAAmB;EAC1F,OAAOqB,uBAAuB,CAAC,CAAC,GAAGrB,QAAQ,GAAG,IAAI;AACpD","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,11 @@ 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;
37
+ /** Renders children after the nearest provider reports its first measurement. */
38
+ export declare function ReservedRegionsGate({ children }: React.PropsWithChildren): React.ReactNode;
32
39
  //# 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;AAED,iFAAiF;AACjF,wBAAgB,mBAAmB,CAAC,EAAE,QAAQ,EAAE,EAAE,KAAK,CAAC,iBAAiB,GAAG,KAAK,CAAC,SAAS,CAE1F"}
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.3",
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,37 @@
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
+ "e2e:android": "node e2e/android.mjs",
141
+ "docs:build": "pnpm --filter reserved-regions-docs build",
142
+ "docs:start": "pnpm --filter reserved-regions-docs start"
146
143
  }
147
- }
144
+ }
@@ -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,26 @@ 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');
82
72
  }
83
- return regions;
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');
84
+ }
85
+ return snapshot.isReady;
86
+ }
87
+
88
+ /** Renders children after the nearest provider reports its first measurement. */
89
+ export function ReservedRegionsGate({ children }: React.PropsWithChildren): React.ReactNode {
90
+ return useReservedRegionsReady() ? children : null;
84
91
  }