@granite-js/react-native 1.0.36 → 1.0.38

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/CHANGELOG.md CHANGED
@@ -1,5 +1,36 @@
1
1
  # @granite-js/react-native
2
2
 
3
+ ## 1.0.38
4
+
5
+ ### Patch Changes
6
+
7
+ - 757fa25: Deprecate `useBackEvent` in favor of `useBackHandler`. Handlers registered through `useBackHandler` receive a `BackEvent` object and can conditionally consume the back action by returning `true`. `useBackEvent` still works, but will be removed in a future release.
8
+ - @granite-js/blur-view@1.0.38
9
+ - @granite-js/brownfield-module@1.0.38
10
+ - @granite-js/cli@1.0.38
11
+ - @granite-js/jest@1.0.38
12
+ - @granite-js/mpack@1.0.38
13
+ - @granite-js/native@1.0.38
14
+ - @granite-js/plugin-core@1.0.38
15
+ - @granite-js/style-utils@1.0.38
16
+ - @granite-js/video@1.0.38
17
+
18
+ ## 1.0.37
19
+
20
+ ### Patch Changes
21
+
22
+ - e60a630: fix(react-native): stop forwarding InView API props to the host view
23
+ - Updated dependencies [2096242]
24
+ - @granite-js/mpack@1.0.37
25
+ - @granite-js/cli@1.0.37
26
+ - @granite-js/blur-view@1.0.37
27
+ - @granite-js/brownfield-module@1.0.37
28
+ - @granite-js/jest@1.0.37
29
+ - @granite-js/native@1.0.37
30
+ - @granite-js/plugin-core@1.0.37
31
+ - @granite-js/style-utils@1.0.37
32
+ - @granite-js/video@1.0.37
33
+
3
34
  ## 1.0.36
4
35
 
5
36
  ### Patch Changes
@@ -63,6 +63,11 @@ export declare function BackEventProvider({ children }: {
63
63
  */
64
64
  export declare function useBackEventState(): PrivateBackEventControls;
65
65
  /**
66
+ * @deprecated Use `useBackHandler` instead. This will be removed in a future release.
67
+ * Handlers registered through `useBackHandler` receive a `BackEvent` object and can conditionally
68
+ * consume the back action by returning `true`. To keep the behavior of `useBackEvent`
69
+ * (always blocking default back navigation while registered), return `true` from the handler.
70
+ *
66
71
  * @public
67
72
  * @category Screen Control
68
73
  * @name useBackEvent
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@granite-js/react-native",
3
- "version": "1.0.36",
3
+ "version": "1.0.38",
4
4
  "description": "The Granite Framework",
5
5
  "bin": {
6
6
  "granite": "./bin/cli.js"
@@ -91,8 +91,8 @@
91
91
  "@babel/core": "7.28.5",
92
92
  "@babel/preset-env": "7.28.5",
93
93
  "@babel/preset-typescript": "7.28.5",
94
- "@granite-js/native": "1.0.36",
95
- "@granite-js/vitest": "1.0.36",
94
+ "@granite-js/native": "1.0.38",
95
+ "@granite-js/vitest": "1.0.38",
96
96
  "@testing-library/dom": "^10.4.0",
97
97
  "@testing-library/react": "^16.1.0",
98
98
  "@types/babel__core": "^7",
@@ -114,21 +114,21 @@
114
114
  "zod": "^4.1.12"
115
115
  },
116
116
  "peerDependencies": {
117
- "@granite-js/native": "1.0.36",
117
+ "@granite-js/native": "1.0.38",
118
118
  "@types/react": "*",
119
119
  "brick-module": "*",
120
120
  "react": "*",
121
121
  "react-native": "*"
122
122
  },
123
123
  "dependencies": {
124
- "@granite-js/blur-view": "1.0.36",
125
- "@granite-js/brownfield-module": "1.0.36",
126
- "@granite-js/cli": "1.0.36",
127
- "@granite-js/jest": "1.0.36",
128
- "@granite-js/mpack": "1.0.36",
129
- "@granite-js/plugin-core": "1.0.36",
130
- "@granite-js/style-utils": "1.0.36",
131
- "@granite-js/video": "1.0.36",
124
+ "@granite-js/blur-view": "1.0.38",
125
+ "@granite-js/brownfield-module": "1.0.38",
126
+ "@granite-js/cli": "1.0.38",
127
+ "@granite-js/jest": "1.0.38",
128
+ "@granite-js/mpack": "1.0.38",
129
+ "@granite-js/plugin-core": "1.0.38",
130
+ "@granite-js/style-utils": "1.0.38",
131
+ "@granite-js/video": "1.0.38",
132
132
  "@standard-schema/spec": "^1.0.0",
133
133
  "es-toolkit": "^1.39.8",
134
134
  "react-native-url-polyfill": "3.0.0"
@@ -330,6 +330,12 @@ function ImpressionAreaImpl({
330
330
  return (
331
331
  <InView
332
332
  onChange={(inView, currentThreshold) => {
333
+ // Guard against non-observer invocations (e.g. a bubbling native event reaching
334
+ // the callback): a wrong `false` here sticks forever because the observer only
335
+ // re-notifies when its own `inView` value changes.
336
+ if (typeof inView !== 'boolean' || typeof currentThreshold !== 'number') {
337
+ return;
338
+ }
333
339
  const isVisible = inView && currentThreshold >= areaThreshold;
334
340
  setInviewImpressed(isVisible);
335
341
  }}
@@ -1,4 +1,5 @@
1
1
  import { render } from '@testing-library/react';
2
+ import { forwardRef } from 'react';
2
3
  import { View } from 'react-native';
3
4
  import { describe, expect, it, vi } from 'vitest';
4
5
  import IOContext, { IOContextValue } from './IOContext';
@@ -147,6 +148,33 @@ describe('InView', () => {
147
148
  expect(outer.unobserve).toHaveBeenCalledTimes(1);
148
149
  });
149
150
 
151
+ it('does not forward its own API props (onChange, triggerOnce) to the rendered view', () => {
152
+ const manager = new FakeIntersectionObserverRegistry();
153
+ const onChange = vi.fn();
154
+ const received: Record<string, unknown>[] = [];
155
+ const CaptureView = forwardRef<View, Record<string, unknown>>((props, ref) => {
156
+ received.push(props);
157
+ return <View ref={ref} />;
158
+ });
159
+
160
+ render(
161
+ <IOContext.Provider value={{ manager: manager.asManager, parent: { manager: null } }}>
162
+ <InView as={CaptureView} triggerOnce onChange={onChange} testID="target">
163
+ <View />
164
+ </InView>
165
+ </IOContext.Provider>
166
+ );
167
+
168
+ const props = received.at(-1);
169
+ // API props must not leak: `onChange` on a host View is a bubbling event handler,
170
+ // so change events from descendants (e.g. TextInput) would call the observer
171
+ // callback with a SyntheticEvent.
172
+ expect(props).not.toHaveProperty('onChange');
173
+ expect(props).not.toHaveProperty('triggerOnce');
174
+ // Regular ViewProps still pass through.
175
+ expect(props).toHaveProperty('testID', 'target');
176
+ });
177
+
150
178
  it('with triggerOnce, stops observing all ancestors once visible', () => {
151
179
  const inner = new FakeIntersectionObserverRegistry();
152
180
  const outer = new FakeIntersectionObserverRegistry();
@@ -257,7 +257,12 @@ class InView<T = ViewProps> extends PureComponent<InViewProps<T>> {
257
257
  };
258
258
 
259
259
  render() {
260
- const { as, children, ...props } = this.props;
260
+ // `onChange` and `triggerOnce` are InView's own API props and must not reach the
261
+ // rendered view. In React Native `onChange` is a bubbling event (`topChange`), so
262
+ // passing it through would make change events from descendants (e.g. a typing
263
+ // `TextInput`) invoke the observer callback with a SyntheticEvent instead of
264
+ // `(inView, intersectionRatio)`.
265
+ const { as, children, triggerOnce, onChange, ...props } = this.props;
261
266
  if (typeof children === 'function') {
262
267
  return null;
263
268
  }
@@ -1,14 +1,24 @@
1
1
  import { render } from '@testing-library/react';
2
- import { BackHandler } from 'react-native';
3
- import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { BackHandler, Platform } from 'react-native';
3
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4
4
  import { CanGoBackGuard } from './CanGoBackGuard';
5
5
 
6
+ function mockPlatformOS(os: typeof Platform.OS) {
7
+ vi.spyOn(Platform, 'OS', 'get').mockReturnValue(os);
8
+ }
9
+
6
10
  describe('CanGoBackGuard', () => {
7
11
  beforeEach(() => {
8
12
  vi.mocked(BackHandler.addEventListener).mockClear();
9
13
  });
10
14
 
15
+ afterEach(() => {
16
+ vi.restoreAllMocks();
17
+ });
18
+
11
19
  it('passes Android hardware back events to onBack and consumes the native event', () => {
20
+ mockPlatformOS('android');
21
+
12
22
  const onBack = vi.fn();
13
23
 
14
24
  render(
@@ -25,6 +35,8 @@ describe('CanGoBackGuard', () => {
25
35
  });
26
36
 
27
37
  it('does not register Android hardware back when no back event exists', () => {
38
+ mockPlatformOS('android');
39
+
28
40
  const onBack = vi.fn();
29
41
 
30
42
  render(
@@ -37,6 +49,8 @@ describe('CanGoBackGuard', () => {
37
49
  });
38
50
 
39
51
  it('registers the iOS swipe back handler while back events exist', () => {
52
+ mockPlatformOS('ios');
53
+
40
54
  const onBack = vi.fn();
41
55
  const setiOSBackPressHandler = vi.fn();
42
56
 
@@ -63,6 +77,8 @@ describe('CanGoBackGuard', () => {
63
77
  });
64
78
 
65
79
  it('disables iOS swipe when the current state should block default back navigation', () => {
80
+ mockPlatformOS('ios');
81
+
66
82
  const setIosSwipeGestureEnabled = vi.fn();
67
83
 
68
84
  const { unmount } = render(
@@ -1,5 +1,5 @@
1
1
  import { ReactNode, useEffect } from 'react';
2
- import { BackHandler } from 'react-native';
2
+ import { BackHandler, Platform } from 'react-native';
3
3
  import type { BackEvent } from '../../use-back-event';
4
4
 
5
5
  type SetIosSwipeGestureEnabled = ({ isEnabled }: { isEnabled: boolean }) => Promise<void> | void;
@@ -23,6 +23,10 @@ export function CanGoBackGuard({
23
23
  setiOSBackPressHandler?: SetIOSBackPressHandler;
24
24
  }) {
25
25
  useEffect(() => {
26
+ if (Platform.OS !== 'ios') {
27
+ return;
28
+ }
29
+
26
30
  if (!isInitialScreen || !canGoBack) {
27
31
  setIosSwipeGestureEnabled?.({ isEnabled: false });
28
32
 
@@ -35,6 +39,10 @@ export function CanGoBackGuard({
35
39
  }, [canGoBack, isInitialScreen, setIosSwipeGestureEnabled]);
36
40
 
37
41
  useEffect(() => {
42
+ if (Platform.OS !== 'android') {
43
+ return;
44
+ }
45
+
38
46
  if (!hasBackEvent) {
39
47
  return;
40
48
  }
@@ -51,6 +59,10 @@ export function CanGoBackGuard({
51
59
  }, [hasBackEvent, onBack]);
52
60
 
53
61
  useEffect(() => {
62
+ if (Platform.OS !== 'ios') {
63
+ return;
64
+ }
65
+
54
66
  if (!hasBackEvent || setiOSBackPressHandler == null) {
55
67
  return;
56
68
  }
@@ -218,6 +218,11 @@ export function useBackEventState() {
218
218
  }
219
219
 
220
220
  /**
221
+ * @deprecated Use `useBackHandler` instead. This will be removed in a future release.
222
+ * Handlers registered through `useBackHandler` receive a `BackEvent` object and can conditionally
223
+ * consume the back action by returning `true`. To keep the behavior of `useBackEvent`
224
+ * (always blocking default back navigation while registered), return `true` from the handler.
225
+ *
221
226
  * @public
222
227
  * @category Screen Control
223
228
  * @name useBackEvent