@granite-js/react-native 1.0.17 → 1.0.19

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,34 @@
1
1
  # @granite-js/react-native
2
2
 
3
+ ## 1.0.19
4
+
5
+ ### Patch Changes
6
+
7
+ - adfe616: perf(react-native): without new URL optimize
8
+ - b084323: feat: add useKeyboardHeight
9
+ - @granite-js/native@1.0.19
10
+ - @granite-js/blur-view@1.0.19
11
+ - @granite-js/jest@1.0.19
12
+ - @granite-js/style-utils@1.0.19
13
+ - @granite-js/cli@1.0.19
14
+ - @granite-js/mpack@1.0.19
15
+ - @granite-js/plugin-core@1.0.19
16
+ - @granite-js/video@1.0.19
17
+
18
+ ## 1.0.18
19
+
20
+ ### Patch Changes
21
+
22
+ - 96fe7c1: Use Android keyboard show/hide events in New Architecture for `useKeyboardAnimatedHeight`.
23
+ - @granite-js/blur-view@1.0.18
24
+ - @granite-js/cli@1.0.18
25
+ - @granite-js/jest@1.0.18
26
+ - @granite-js/mpack@1.0.18
27
+ - @granite-js/native@1.0.18
28
+ - @granite-js/plugin-core@1.0.18
29
+ - @granite-js/style-utils@1.0.18
30
+ - @granite-js/video@1.0.18
31
+
3
32
  ## 1.0.17
4
33
 
5
34
  ### Patch Changes
@@ -0,0 +1 @@
1
+ export declare function getSearchParamsFromUrl(url: string): Record<string, string>;
@@ -18,6 +18,4 @@
18
18
  * }
19
19
  * ```
20
20
  */
21
- export declare function useInitialSearchParams(): {
22
- [k: string]: string;
23
- };
21
+ export declare function useInitialSearchParams(): Record<string, string>;
@@ -0,0 +1 @@
1
+ export declare function getInitialKeyboardHeight(): number;
@@ -0,0 +1,7 @@
1
+ export declare function getKeyboardEventNames(): {
2
+ readonly show: "keyboardWillShow";
3
+ readonly hide: "keyboardWillHide";
4
+ } | {
5
+ readonly show: "keyboardDidShow";
6
+ readonly hide: "keyboardDidHide";
7
+ } | null;
@@ -1,2 +1,3 @@
1
1
  export { KeyboardAboveView } from './KeyboardAboveView';
2
2
  export { useKeyboardAnimatedHeight } from './useKeyboardAnimatedHeight';
3
+ export { useKeyboardHeight } from './useKeyboardHeight';
@@ -5,7 +5,7 @@ import { Animated } from 'react-native';
5
5
  * @description
6
6
  * A Hook that returns an animatable value (`Animated.Value`) representing the keyboard height changes when the keyboard appears or disappears. You can smoothly animate UI elements according to the keyboard height as it rises or falls.
7
7
  *
8
- * This Hook is primarily used on iOS. On Android, it does not detect keyboard height changes and always returns an `Animated.Value` with an initial value of `0`. In other words, animations are not applied in the Android environment.
8
+ * This Hook uses `keyboardWillShow`/`keyboardWillHide` on iOS, and `keyboardDidShow`/`keyboardDidHide` on Android when React Native New Architecture is enabled. On Android Old Architecture, it always returns an `Animated.Value` with an initial value of `0`.
9
9
  *
10
10
  * @returns {Animated.Value} - An animation value representing the keyboard height.
11
11
  * @example
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @category Hooks
3
+ * @name useKeyboardHeight
4
+ * @description
5
+ * A Hook that returns the current keyboard height as a number when the keyboard appears or disappears. Unlike `useKeyboardAnimatedHeight`, this Hook does not animate the value — it simply reflects the latest keyboard height.
6
+ *
7
+ * This Hook uses `keyboardWillShow`/`keyboardWillHide` on iOS, and `keyboardDidShow`/`keyboardDidHide` on Android when React Native New Architecture is enabled. On Android Old Architecture, it always returns `0`.
8
+ *
9
+ * @returns {number} - The current keyboard height.
10
+ * @example
11
+ * ```typescript
12
+ * const keyboardHeight = useKeyboardHeight();
13
+ *
14
+ * <View style={{ marginBottom: keyboardHeight }}>
15
+ * {children}
16
+ * </View>
17
+ * ```
18
+ */
19
+ export declare function useKeyboardHeight(): number;
@@ -0,0 +1 @@
1
+ export declare function isNewArchEnabled(): boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@granite-js/react-native",
3
- "version": "1.0.17",
3
+ "version": "1.0.19",
4
4
  "description": "The Granite Framework",
5
5
  "bin": {
6
6
  "granite": "./bin/cli.js"
@@ -91,7 +91,7 @@
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.17",
94
+ "@granite-js/native": "1.0.19",
95
95
  "@testing-library/dom": "^10.4.0",
96
96
  "@testing-library/react": "^16.1.0",
97
97
  "@types/babel__core": "^7",
@@ -113,20 +113,20 @@
113
113
  "zod": "^4.1.12"
114
114
  },
115
115
  "peerDependencies": {
116
- "@granite-js/native": "1.0.17",
116
+ "@granite-js/native": "1.0.19",
117
117
  "@types/react": "*",
118
118
  "brick-module": "*",
119
119
  "react": "*",
120
120
  "react-native": "*"
121
121
  },
122
122
  "dependencies": {
123
- "@granite-js/blur-view": "1.0.17",
124
- "@granite-js/cli": "1.0.17",
125
- "@granite-js/jest": "1.0.17",
126
- "@granite-js/mpack": "1.0.17",
127
- "@granite-js/plugin-core": "1.0.17",
128
- "@granite-js/style-utils": "1.0.17",
129
- "@granite-js/video": "1.0.17",
123
+ "@granite-js/blur-view": "1.0.19",
124
+ "@granite-js/cli": "1.0.19",
125
+ "@granite-js/jest": "1.0.19",
126
+ "@granite-js/mpack": "1.0.19",
127
+ "@granite-js/plugin-core": "1.0.19",
128
+ "@granite-js/style-utils": "1.0.19",
129
+ "@granite-js/video": "1.0.19",
130
130
  "@standard-schema/spec": "^1.0.0",
131
131
  "es-toolkit": "^1.39.8",
132
132
  "react-native-url-polyfill": "3.0.0"
@@ -0,0 +1,32 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { getSearchParamsFromUrl } from './getSearchParamsFromUrl';
3
+
4
+ describe('getSearchParamsFromUrl', () => {
5
+ it('returns query parameters from a custom scheme URL', () => {
6
+ expect(getSearchParamsFromUrl('myapp://home?userId=42&theme=dark')).toEqual({
7
+ userId: '42',
8
+ theme: 'dark',
9
+ });
10
+ });
11
+
12
+ it('decodes query parameters the same way as URLSearchParams', () => {
13
+ expect(getSearchParamsFromUrl('myapp://home?name=foo+bar&message=hello%20world')).toEqual({
14
+ name: 'foo bar',
15
+ message: 'hello world',
16
+ });
17
+ });
18
+
19
+ it('ignores the hash fragment and keeps the last duplicated key', () => {
20
+ expect(getSearchParamsFromUrl('myapp://home?a=1&a=2#section')).toEqual({
21
+ a: '2',
22
+ });
23
+ });
24
+
25
+ it('returns an empty object for URLs without a query string', () => {
26
+ expect(getSearchParamsFromUrl('myapp://home')).toEqual({});
27
+ });
28
+
29
+ it('returns an empty object for invalid URLs', () => {
30
+ expect(getSearchParamsFromUrl('not a url')).toEqual({});
31
+ });
32
+ });
@@ -0,0 +1,26 @@
1
+ export function getSearchParamsFromUrl(url: string): Record<string, string> {
2
+ if (typeof URLSearchParams === 'undefined') {
3
+ return {};
4
+ }
5
+
6
+ const queryString = getQueryString(url);
7
+
8
+ if (queryString == null || queryString.length === 0) {
9
+ return {};
10
+ }
11
+
12
+ return Object.fromEntries(new URLSearchParams(queryString));
13
+ }
14
+
15
+ function getQueryString(url: string) {
16
+ const questionMarkIndex = url.indexOf('?');
17
+
18
+ if (questionMarkIndex === -1) {
19
+ return null;
20
+ }
21
+
22
+ const fragmentIndex = url.indexOf('#', questionMarkIndex + 1);
23
+ const queryEndIndex = fragmentIndex === -1 ? url.length : fragmentIndex;
24
+
25
+ return url.slice(questionMarkIndex + 1, queryEndIndex);
26
+ }
@@ -1,4 +1,6 @@
1
+ import { useMemo } from 'react';
1
2
  import { useInitialProps } from './InitialPropsContext';
3
+ import { getSearchParamsFromUrl } from './getSearchParamsFromUrl';
2
4
  import { getSchemeUri } from '../../native-modules';
3
5
 
4
6
  /**
@@ -24,9 +26,5 @@ import { getSchemeUri } from '../../native-modules';
24
26
  export function useInitialSearchParams() {
25
27
  const scheme = useInitialProps().scheme ?? getSchemeUri();
26
28
 
27
- try {
28
- return Object.fromEntries(new URL(scheme).searchParams);
29
- } catch {
30
- return {};
31
- }
29
+ return useMemo(() => getSearchParamsFromUrl(scheme), [scheme]);
32
30
  }
@@ -0,0 +1,21 @@
1
+ import { Platform, Keyboard } from "react-native";
2
+ import { isNewArchEnabled } from "../utils/isNewArchEnabled";
3
+
4
+ export function getInitialKeyboardHeight() {
5
+ if (Platform.OS === 'android' && !isNewArchEnabled()) {
6
+ return 0;
7
+ }
8
+
9
+ /**
10
+ * Branch handling for React Native 0.68.0 version where `metrics()` does not exist
11
+ */
12
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
13
+ // @ts-ignore
14
+ if (typeof Keyboard?.metrics === 'function') {
15
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
16
+ // @ts-ignore
17
+ return Keyboard.metrics()?.height ?? 0;
18
+ } else {
19
+ return 0;
20
+ }
21
+ }
@@ -0,0 +1,20 @@
1
+ import { Platform } from "react-native";
2
+ import { isNewArchEnabled } from "../utils/isNewArchEnabled";
3
+
4
+ export function getKeyboardEventNames() {
5
+ if (Platform.OS === 'ios') {
6
+ return {
7
+ show: 'keyboardWillShow',
8
+ hide: 'keyboardWillHide',
9
+ } as const;
10
+ }
11
+
12
+ if (Platform.OS === 'android' && isNewArchEnabled()) {
13
+ return {
14
+ show: 'keyboardDidShow',
15
+ hide: 'keyboardDidHide',
16
+ } as const;
17
+ }
18
+
19
+ return null;
20
+ }
@@ -1,2 +1,3 @@
1
1
  export { KeyboardAboveView } from './KeyboardAboveView';
2
2
  export { useKeyboardAnimatedHeight } from './useKeyboardAnimatedHeight';
3
+ export { useKeyboardHeight } from './useKeyboardHeight';
@@ -1,24 +1,7 @@
1
1
  import { useEffect, useRef } from 'react';
2
- import { Animated, Keyboard, Platform } from 'react-native';
3
-
4
- function getInitialKeyboardHeight() {
5
- if (Platform.OS !== 'ios') {
6
- return 0;
7
- }
8
-
9
- /**
10
- * Branch handling for React Native 0.68.0 version where `metrics()` does not exist
11
- */
12
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
13
- // @ts-ignore
14
- if (typeof Keyboard?.metrics === 'function') {
15
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
16
- // @ts-ignore
17
- return Keyboard.metrics()?.height ?? 0;
18
- } else {
19
- return 0;
20
- }
21
- }
2
+ import { Animated, Keyboard } from 'react-native';
3
+ import { getInitialKeyboardHeight } from './getInitialKeyboardHeight';
4
+ import { getKeyboardEventNames } from './getKeyboardEventNames';
22
5
 
23
6
  /**
24
7
  * @category Hooks
@@ -26,7 +9,7 @@ function getInitialKeyboardHeight() {
26
9
  * @description
27
10
  * A Hook that returns an animatable value (`Animated.Value`) representing the keyboard height changes when the keyboard appears or disappears. You can smoothly animate UI elements according to the keyboard height as it rises or falls.
28
11
  *
29
- * This Hook is primarily used on iOS. On Android, it does not detect keyboard height changes and always returns an `Animated.Value` with an initial value of `0`. In other words, animations are not applied in the Android environment.
12
+ * This Hook uses `keyboardWillShow`/`keyboardWillHide` on iOS, and `keyboardDidShow`/`keyboardDidHide` on Android when React Native New Architecture is enabled. On Android Old Architecture, it always returns an `Animated.Value` with an initial value of `0`.
30
13
  *
31
14
  * @returns {Animated.Value} - An animation value representing the keyboard height.
32
15
  * @example
@@ -42,31 +25,34 @@ export function useKeyboardAnimatedHeight(): Animated.Value {
42
25
  const keyboardHeight = useRef(new Animated.Value(getInitialKeyboardHeight())).current;
43
26
 
44
27
  useEffect(() => {
45
- if (Platform.OS === 'ios') {
46
- const willShowSubscription = Keyboard.addListener('keyboardWillShow', (event) => {
47
- const height = event.endCoordinates.height;
48
-
49
- Animated.spring(keyboardHeight, {
50
- toValue: height,
51
- useNativeDriver: true,
52
- ...spring.quick,
53
- }).start();
54
- });
28
+ const keyboardEventNames = getKeyboardEventNames();
55
29
 
56
- const willHideSubscription = Keyboard.addListener('keyboardWillHide', () => {
57
- Animated.spring(keyboardHeight, {
58
- toValue: 0,
59
- useNativeDriver: true,
60
- ...spring.quick,
61
- }).start();
62
- });
63
- return () => {
64
- willShowSubscription.remove();
65
- willHideSubscription.remove();
66
- };
67
- } else {
30
+ if (keyboardEventNames == null) {
68
31
  return;
69
32
  }
33
+
34
+ const showSubscription = Keyboard.addListener(keyboardEventNames.show, (event) => {
35
+ const height = event.endCoordinates.height;
36
+
37
+ Animated.spring(keyboardHeight, {
38
+ toValue: height,
39
+ useNativeDriver: true,
40
+ ...spring.quick,
41
+ }).start();
42
+ });
43
+
44
+ const hideSubscription = Keyboard.addListener(keyboardEventNames.hide, () => {
45
+ Animated.spring(keyboardHeight, {
46
+ toValue: 0,
47
+ useNativeDriver: true,
48
+ ...spring.quick,
49
+ }).start();
50
+ });
51
+
52
+ return () => {
53
+ showSubscription.remove();
54
+ hideSubscription.remove();
55
+ };
70
56
  }, [keyboardHeight]);
71
57
 
72
58
  return keyboardHeight;
@@ -0,0 +1,49 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { Keyboard } from 'react-native';
3
+ import { getInitialKeyboardHeight } from './getInitialKeyboardHeight';
4
+ import { getKeyboardEventNames } from './getKeyboardEventNames';
5
+
6
+ /**
7
+ * @category Hooks
8
+ * @name useKeyboardHeight
9
+ * @description
10
+ * A Hook that returns the current keyboard height as a number when the keyboard appears or disappears. Unlike `useKeyboardAnimatedHeight`, this Hook does not animate the value — it simply reflects the latest keyboard height.
11
+ *
12
+ * This Hook uses `keyboardWillShow`/`keyboardWillHide` on iOS, and `keyboardDidShow`/`keyboardDidHide` on Android when React Native New Architecture is enabled. On Android Old Architecture, it always returns `0`.
13
+ *
14
+ * @returns {number} - The current keyboard height.
15
+ * @example
16
+ * ```typescript
17
+ * const keyboardHeight = useKeyboardHeight();
18
+ *
19
+ * <View style={{ marginBottom: keyboardHeight }}>
20
+ * {children}
21
+ * </View>
22
+ * ```
23
+ */
24
+ export function useKeyboardHeight(): number {
25
+ const [keyboardHeight, setKeyboardHeight] = useState<number>(getInitialKeyboardHeight);
26
+
27
+ useEffect(() => {
28
+ const keyboardEventNames = getKeyboardEventNames();
29
+
30
+ if (keyboardEventNames == null) {
31
+ return;
32
+ }
33
+
34
+ const showSubscription = Keyboard.addListener(keyboardEventNames.show, (event) => {
35
+ setKeyboardHeight(event.endCoordinates.height);
36
+ });
37
+
38
+ const hideSubscription = Keyboard.addListener(keyboardEventNames.hide, () => {
39
+ setKeyboardHeight(0);
40
+ });
41
+
42
+ return () => {
43
+ showSubscription.remove();
44
+ hideSubscription.remove();
45
+ };
46
+ }, []);
47
+
48
+ return keyboardHeight;
49
+ }
@@ -1,5 +1,3 @@
1
1
  export function getSchemePrefix({ scheme, appName, host }: { scheme: string; appName: string; host: string }) {
2
- const base = host.length > 0 ? `${scheme}://${host}/` : `${scheme}://`;
3
-
4
- return new URL(`${base}${appName}`).toString();
2
+ return host.length > 0 ? `${scheme}://${host}/${appName}` : `${scheme}://${appName}`;
5
3
  }
@@ -0,0 +1,13 @@
1
+ export function isNewArchEnabled() {
2
+ const globalObject = globalThis as typeof globalThis & {
3
+ RN$Bridgeless?: boolean;
4
+ nativeFabricUIManager?: unknown;
5
+ __turboModuleProxy?: unknown;
6
+ };
7
+
8
+ return (
9
+ globalObject.RN$Bridgeless === true ||
10
+ globalObject.nativeFabricUIManager != null ||
11
+ globalObject.__turboModuleProxy != null
12
+ );
13
+ }