@react-native-ohos/elements 2.3.9-rc.1 → 2.4.0-rc.1

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.
Files changed (44) hide show
  1. package/README.OpenSource +1 -1
  2. package/lib/module/Header/Header.js +3 -2
  3. package/lib/module/Header/Header.js.map +1 -1
  4. package/lib/module/Header/HeaderButton.js +7 -3
  5. package/lib/module/Header/HeaderButton.js.map +1 -1
  6. package/lib/module/Header/HeaderSearchBar.js +2 -3
  7. package/lib/module/Header/HeaderSearchBar.js.map +1 -1
  8. package/lib/module/Header/HeaderTitle.js +1 -1
  9. package/lib/module/Header/HeaderTitle.js.map +1 -1
  10. package/lib/module/PlatformPressable.js +18 -11
  11. package/lib/module/PlatformPressable.js.map +1 -1
  12. package/lib/module/SafeAreaProviderCompat.js +19 -17
  13. package/lib/module/SafeAreaProviderCompat.js.map +1 -1
  14. package/lib/module/Screen.js +5 -5
  15. package/lib/module/Screen.js.map +1 -1
  16. package/lib/module/getNamedContext.js.map +1 -1
  17. package/lib/module/index.js +1 -0
  18. package/lib/module/index.js.map +1 -1
  19. package/lib/module/useFrameSize.js +174 -0
  20. package/lib/module/useFrameSize.js.map +1 -0
  21. package/lib/typescript/src/Header/Header.d.ts.map +1 -1
  22. package/lib/typescript/src/Header/HeaderButton.d.ts +2 -1
  23. package/lib/typescript/src/Header/HeaderButton.d.ts.map +1 -1
  24. package/lib/typescript/src/Header/HeaderSearchBar.d.ts.map +1 -1
  25. package/lib/typescript/src/PlatformPressable.d.ts +13 -7
  26. package/lib/typescript/src/PlatformPressable.d.ts.map +1 -1
  27. package/lib/typescript/src/SafeAreaProviderCompat.d.ts.map +1 -1
  28. package/lib/typescript/src/Screen.d.ts.map +1 -1
  29. package/lib/typescript/src/getNamedContext.d.ts.map +1 -1
  30. package/lib/typescript/src/index.d.ts +1 -0
  31. package/lib/typescript/src/index.d.ts.map +1 -1
  32. package/lib/typescript/src/useFrameSize.d.ts +15 -0
  33. package/lib/typescript/src/useFrameSize.d.ts.map +1 -0
  34. package/package.json +85 -83
  35. package/src/Header/Header.tsx +3 -5
  36. package/src/Header/HeaderButton.tsx +21 -12
  37. package/src/Header/HeaderSearchBar.tsx +2 -3
  38. package/src/Header/HeaderTitle.tsx +1 -1
  39. package/src/PlatformPressable.tsx +52 -29
  40. package/src/SafeAreaProviderCompat.tsx +23 -19
  41. package/src/Screen.tsx +8 -10
  42. package/src/getNamedContext.tsx +0 -1
  43. package/src/index.tsx +1 -0
  44. package/src/useFrameSize.tsx +254 -0
@@ -17,12 +17,15 @@ type HoverEffectProps = {
17
17
  activeOpacity?: number;
18
18
  };
19
19
 
20
- export type Props = Omit<PressableProps, 'style'> & {
20
+ export type Props = Omit<PressableProps, 'style' | 'onPress'> & {
21
+ href?: string;
21
22
  pressColor?: string;
22
23
  pressOpacity?: number;
23
24
  hoverEffect?: HoverEffectProps;
24
25
  style?: Animated.WithAnimatedValue<StyleProp<ViewStyle>>;
25
- href?: string;
26
+ onPress?: (
27
+ e: React.MouseEvent<HTMLAnchorElement, MouseEvent> | GestureResponderEvent
28
+ ) => void;
26
29
  children: React.ReactNode;
27
30
  };
28
31
 
@@ -37,19 +40,22 @@ const useNativeDriver = Platform.OS !== 'web';
37
40
  /**
38
41
  * PlatformPressable provides an abstraction on top of Pressable to handle platform differences.
39
42
  */
40
- export function PlatformPressable({
41
- disabled,
42
- onPress,
43
- onPressIn,
44
- onPressOut,
45
- android_ripple,
46
- pressColor,
47
- pressOpacity = 0.3,
48
- hoverEffect,
49
- style,
50
- children,
51
- ...rest
52
- }: Props) {
43
+ function PlatformPressableInternal(
44
+ {
45
+ disabled,
46
+ onPress,
47
+ onPressIn,
48
+ onPressOut,
49
+ android_ripple,
50
+ pressColor,
51
+ pressOpacity = 0.3,
52
+ hoverEffect,
53
+ style,
54
+ children,
55
+ ...rest
56
+ }: Props,
57
+ ref: React.Ref<React.ComponentRef<typeof AnimatedPressable>>
58
+ ) {
53
59
  const { dark } = useTheme();
54
60
  const [opacity] = React.useState(() => new Animated.Value(1));
55
61
 
@@ -66,18 +72,31 @@ export function PlatformPressable({
66
72
  }).start();
67
73
  };
68
74
 
69
- const handlePress = (e: GestureResponderEvent) => {
70
- if (Platform.OS === 'web' && rest.href != null) {
71
- // @ts-expect-error: these properties exist on web, but not in React Native
72
- const hasModifierKey = e.metaKey || e.altKey || e.ctrlKey || e.shiftKey; // ignore clicks with modifier keys
73
- // @ts-expect-error: these properties exist on web, but not in React Native
74
- const isLeftClick = e.button == null || e.button === 0; // only handle left clicks
75
- const isSelfTarget = [undefined, null, '', 'self'].includes(
76
- // @ts-expect-error: these properties exist on web, but not in React Native
77
- e.currentTarget?.target
78
- ); // let browser handle "target=_blank" etc.
75
+ const handlePress = (
76
+ e: React.MouseEvent<HTMLAnchorElement, MouseEvent> | GestureResponderEvent
77
+ ) => {
78
+ if (Platform.OS === 'web' && rest.href !== null) {
79
+ // ignore clicks with modifier keys
80
+ const hasModifierKey =
81
+ ('metaKey' in e && e.metaKey) ||
82
+ ('altKey' in e && e.altKey) ||
83
+ ('ctrlKey' in e && e.ctrlKey) ||
84
+ ('shiftKey' in e && e.shiftKey);
85
+
86
+ // only handle left clicks
87
+ const isLeftClick =
88
+ 'button' in e ? e.button == null || e.button === 0 : true;
89
+
90
+ // let browser handle "target=_blank" etc.
91
+ const isSelfTarget =
92
+ e.currentTarget && 'target' in e.currentTarget
93
+ ? [undefined, null, '', 'self'].includes(e.currentTarget.target)
94
+ : true;
95
+
79
96
  if (!hasModifierKey && isLeftClick && isSelfTarget) {
80
97
  e.preventDefault();
98
+ // call `onPress` only when browser default is prevented
99
+ // this prevents app from handling the click when a link is being opened
81
100
  onPress?.(e);
82
101
  }
83
102
  } else {
@@ -97,10 +116,9 @@ export function PlatformPressable({
97
116
 
98
117
  return (
99
118
  <AnimatedPressable
119
+ ref={ref}
100
120
  accessible
101
- accessibilityRole={
102
- Platform.OS === 'web' && rest.href != null ? 'link' : 'button'
103
- }
121
+ role={Platform.OS === 'web' && rest.href != null ? 'link' : 'button'}
104
122
  onPress={disabled ? undefined : handlePress}
105
123
  onPressIn={handlePressIn}
106
124
  onPressOut={handlePressOut}
@@ -137,6 +155,10 @@ export function PlatformPressable({
137
155
  );
138
156
  }
139
157
 
158
+ export const PlatformPressable = React.forwardRef(PlatformPressableInternal);
159
+
160
+ PlatformPressable.displayName = 'PlatformPressable';
161
+
140
162
  const css = String.raw;
141
163
 
142
164
  const CLASS_NAME = `__react-navigation_elements_Pressable_hover`;
@@ -152,6 +174,7 @@ const CSS_TEXT = css`
152
174
  background-color: var(--overlay-color);
153
175
  opacity: 0;
154
176
  transition: opacity 0.15s;
177
+ pointer-events: none;
155
178
  }
156
179
 
157
180
  a:hover > .${CLASS_NAME}, button:hover > .${CLASS_NAME} {
@@ -174,7 +197,7 @@ const HoverEffect = ({
174
197
 
175
198
  return (
176
199
  <>
177
- <style
200
+ <style
178
201
  // @ts-expect-error: href and precedence are only available on React 19
179
202
  href={CLASS_NAME}
180
203
  // eslint-disable-next-line @eslint-react/dom/no-unknown-property
@@ -13,6 +13,8 @@ import {
13
13
  SafeAreaProvider,
14
14
  } from 'react-native-safe-area-context';
15
15
 
16
+ import { FrameSizeProvider } from './useFrameSize';
17
+
16
18
  type Props = {
17
19
  children: React.ReactNode;
18
20
  style?: StyleProp<ViewStyle>;
@@ -26,29 +28,31 @@ const { width = 0, height = 0 } = Dimensions.get('window');
26
28
  const initialMetrics =
27
29
  Platform.OS === 'web' || initialWindowMetrics == null
28
30
  ? {
29
- frame: { x: 0, y: 0, width, height },
30
- insets: { top: 0, left: 0, right: 0, bottom: 0 },
31
- }
31
+ frame: { x: 0, y: 0, width, height },
32
+ insets: { top: 0, left: 0, right: 0, bottom: 0 },
33
+ }
32
34
  : initialWindowMetrics;
33
35
 
34
36
  export function SafeAreaProviderCompat({ children, style }: Props) {
37
+ const insets = React.useContext(SafeAreaInsetsContext);
38
+
39
+ children = (
40
+ <FrameSizeProvider initialFrame={initialMetrics.frame}>
41
+ {children}
42
+ </FrameSizeProvider>
43
+ );
44
+
45
+ if (insets) {
46
+ // If we already have insets, don't wrap the stack in another safe area provider
47
+ // This avoids an issue with updates at the cost of potentially incorrect values
48
+ // https://github.com/react-navigation/react-navigation/issues/174
49
+ return <View style={[styles.container, style]}>{children}</View>;
50
+ }
51
+
35
52
  return (
36
- <SafeAreaInsetsContext.Consumer>
37
- {(insets) => {
38
- if (insets) {
39
- // If we already have insets, don't wrap the stack in another safe area provider
40
- // This avoids an issue with updates at the cost of potentially incorrect values
41
- // https://github.com/react-navigation/react-navigation/issues/174
42
- return <View style={[styles.container, style]}>{children}</View>;
43
- }
44
-
45
- return (
46
- <SafeAreaProvider initialMetrics={initialMetrics} style={style}>
47
- {children}
48
- </SafeAreaProvider>
49
- );
50
- }}
51
- </SafeAreaInsetsContext.Consumer>
53
+ <SafeAreaProvider initialMetrics={initialMetrics} style={style}>
54
+ {children}
55
+ </SafeAreaProvider>
52
56
  );
53
57
  }
54
58
 
package/src/Screen.tsx CHANGED
@@ -13,15 +13,13 @@ import {
13
13
  View,
14
14
  type ViewStyle,
15
15
  } from 'react-native';
16
- import {
17
- useSafeAreaFrame,
18
- useSafeAreaInsets,
19
- } from 'react-native-safe-area-context';
16
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
20
17
 
21
18
  import { Background } from './Background';
22
19
  import { getDefaultHeaderHeight } from './Header/getDefaultHeaderHeight';
23
20
  import { HeaderHeightContext } from './Header/HeaderHeightContext';
24
21
  import { HeaderShownContext } from './Header/HeaderShownContext';
22
+ import { useFrameSize } from './useFrameSize';
25
23
 
26
24
  type Props = {
27
25
  focused: boolean;
@@ -37,7 +35,6 @@ type Props = {
37
35
  };
38
36
 
39
37
  export function Screen(props: Props) {
40
- const dimensions = useSafeAreaFrame();
41
38
  const insets = useSafeAreaInsets();
42
39
 
43
40
  const isParentHeaderShown = React.useContext(HeaderShownContext);
@@ -56,14 +53,15 @@ export function Screen(props: Props) {
56
53
  style,
57
54
  } = props;
58
55
 
59
- const [headerHeight, setHeaderHeight] = React.useState(() =>
60
- getDefaultHeaderHeight(dimensions, modal, headerStatusBarHeight)
56
+ const defaultHeaderHeight = useFrameSize((size) =>
57
+ getDefaultHeaderHeight(size, modal, headerStatusBarHeight)
61
58
  );
62
59
 
60
+ const [headerHeight, setHeaderHeight] = React.useState(defaultHeaderHeight);
61
+
63
62
  return (
64
63
  <Background
65
- accessibilityElementsHidden={!focused}
66
- importantForAccessibility={focused ? 'auto' : 'no-hide-descendants'}
64
+ aria-hidden={!focused}
67
65
  style={[styles.container, style]}
68
66
  // On Fabric we need to disable collapsing for the background to ensure
69
67
  // that we won't render unnecessary views due to the view flattening.
@@ -94,7 +92,7 @@ export function Screen(props: Props) {
94
92
  value={isParentHeaderShown || headerShown !== false}
95
93
  >
96
94
  <HeaderHeightContext.Provider
97
- value={headerShown ? headerHeight : parentHeaderHeight ?? 0}
95
+ value={headerShown ? headerHeight : (parentHeaderHeight ?? 0)}
98
96
  >
99
97
  {children}
100
98
  </HeaderHeightContext.Provider>
@@ -3,7 +3,6 @@ import * as React from 'react';
3
3
  const contexts = '__react_navigation__elements_contexts';
4
4
 
5
5
  declare global {
6
- // eslint-disable-next-line no-var
7
6
  var __react_navigation__elements_contexts: Map<string, React.Context<any>>;
8
7
  }
9
8
 
package/src/index.tsx CHANGED
@@ -26,6 +26,7 @@ export { ResourceSavingView } from './ResourceSavingView';
26
26
  export { SafeAreaProviderCompat } from './SafeAreaProviderCompat';
27
27
  export { Screen } from './Screen';
28
28
  export { Text } from './Text';
29
+ export { useFrameSize } from './useFrameSize';
29
30
 
30
31
  export const Assets = [
31
32
  backIcon,
@@ -0,0 +1,254 @@
1
+ import * as React from 'react';
2
+ import {
3
+ Platform,
4
+ type StyleProp,
5
+ StyleSheet,
6
+ type ViewStyle,
7
+ } from 'react-native';
8
+ import {
9
+ // eslint-disable-next-line no-restricted-imports
10
+ useSafeAreaFrame,
11
+ } from 'react-native-safe-area-context';
12
+ import useLatestCallback from 'use-latest-callback';
13
+ import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/with-selector';
14
+
15
+ // Load with require to avoid error from webpack due to missing export in older versions
16
+ // eslint-disable-next-line import-x/no-commonjs
17
+ type SafeAreaListenerProps = {
18
+ onChange: (event: { frame: Frame }) => void;
19
+ style?: StyleProp<ViewStyle>;
20
+ };
21
+
22
+ const SafeAreaListener = (require('react-native-safe-area-context') as {
23
+ SafeAreaListener?: React.ComponentType<SafeAreaListenerProps>;
24
+ }).SafeAreaListener;
25
+
26
+ type Frame = {
27
+ width: number;
28
+ height: number;
29
+ };
30
+
31
+ type Listener = () => void;
32
+
33
+ type RemoveListener = () => void;
34
+
35
+ type FrameContextType = {
36
+ getCurrent: () => Frame;
37
+ subscribe: (listener: Listener) => RemoveListener;
38
+ subscribeThrottled: (listener: Listener) => RemoveListener;
39
+ };
40
+
41
+ const FrameContext = React.createContext<FrameContextType | undefined>(
42
+ undefined
43
+ );
44
+
45
+ export function useFrameSize<T>(
46
+ selector: (frame: Frame) => T,
47
+ throttle?: boolean
48
+ ): T {
49
+ const context = React.useContext(FrameContext);
50
+
51
+ if (context == null) {
52
+ throw new Error('useFrameSize must be used within a FrameSizeProvider');
53
+ }
54
+
55
+ const value = useSyncExternalStoreWithSelector(
56
+ throttle ? context.subscribeThrottled : context.subscribe,
57
+ context.getCurrent,
58
+ context.getCurrent,
59
+ selector
60
+ );
61
+
62
+ return value;
63
+ }
64
+
65
+ type FrameSizeProviderProps = {
66
+ initialFrame: Frame;
67
+ children: React.ReactNode;
68
+ style?: StyleProp<ViewStyle>;
69
+ };
70
+
71
+ export function FrameSizeProvider({
72
+ initialFrame,
73
+ children,
74
+ }: FrameSizeProviderProps) {
75
+ const context = React.useContext(FrameContext);
76
+
77
+ if (context != null) {
78
+ // If the context is already present, don't wrap again
79
+ return children;
80
+ }
81
+
82
+ return (
83
+ <FrameSizeProviderInner initialFrame={initialFrame}>
84
+ {children}
85
+ </FrameSizeProviderInner>
86
+ );
87
+ }
88
+
89
+ function FrameSizeProviderInner({
90
+ initialFrame,
91
+ children,
92
+ }: FrameSizeProviderProps) {
93
+ const frameRef = React.useRef<Frame>({
94
+ width: initialFrame.width,
95
+ height: initialFrame.height,
96
+ });
97
+
98
+ const listeners = React.useRef<Set<Listener>>(new Set());
99
+
100
+ const getCurrent = useLatestCallback(() => frameRef.current);
101
+
102
+ const subscribe = useLatestCallback((listener: Listener): RemoveListener => {
103
+ listeners.current.add(listener);
104
+
105
+ return () => {
106
+ listeners.current.delete(listener);
107
+ };
108
+ });
109
+
110
+ const subscribeThrottled = useLatestCallback(
111
+ (listener: Listener): RemoveListener => {
112
+ const delay = 100; // Throttle delay in milliseconds
113
+
114
+ let timer: ReturnType<typeof setTimeout>;
115
+ let updated = false;
116
+ let waiting = false;
117
+
118
+ const throttledListener = () => {
119
+ clearTimeout(timer);
120
+
121
+ updated = true;
122
+
123
+ if (waiting) {
124
+ // Schedule a timer to call the listener at the end
125
+ timer = setTimeout(() => {
126
+ if (updated) {
127
+ updated = false;
128
+ listener();
129
+ }
130
+ }, delay);
131
+ } else {
132
+ waiting = true;
133
+ setTimeout(function () {
134
+ waiting = false;
135
+ }, delay);
136
+
137
+ // Call the listener immediately at start
138
+ updated = false;
139
+ listener();
140
+ }
141
+ };
142
+
143
+ const unsubscribe = subscribe(throttledListener);
144
+
145
+ return () => {
146
+ unsubscribe();
147
+ clearTimeout(timer);
148
+ };
149
+ }
150
+ );
151
+
152
+ const context = React.useMemo<FrameContextType>(
153
+ () => ({
154
+ getCurrent,
155
+ subscribe,
156
+ subscribeThrottled,
157
+ }),
158
+ [subscribe, subscribeThrottled, getCurrent]
159
+ );
160
+
161
+ const onChange = useLatestCallback((frame: Frame) => {
162
+ if (
163
+ frameRef.current.height === frame.height &&
164
+ frameRef.current.width === frame.width
165
+ ) {
166
+ return;
167
+ }
168
+
169
+ frameRef.current = { width: frame.width, height: frame.height };
170
+ listeners.current.forEach((listener) => listener());
171
+ });
172
+
173
+ return (
174
+ <>
175
+ {Platform.OS === 'web' ? (
176
+ <FrameSizeListenerWeb onChange={onChange} />
177
+ ) : typeof SafeAreaListener === 'undefined' ? (
178
+ <FrameSizeListenerNativeFallback onChange={onChange} />
179
+ ) : (
180
+ <SafeAreaListener
181
+ onChange={({ frame }) => onChange(frame)}
182
+ style={StyleSheet.absoluteFill}
183
+ />
184
+ )}
185
+ <FrameContext.Provider value={context}>{children}</FrameContext.Provider>
186
+ </>
187
+ );
188
+ }
189
+
190
+ // SafeAreaListener is available only on newer versions
191
+ // Fallback to an effect-based shim for older versions
192
+ function FrameSizeListenerNativeFallback({
193
+ onChange,
194
+ }: {
195
+ onChange: (frame: Frame) => void;
196
+ }) {
197
+ const frame = useSafeAreaFrame();
198
+
199
+ React.useLayoutEffect(() => {
200
+ onChange(frame);
201
+ }, [frame, onChange]);
202
+
203
+ return null;
204
+ }
205
+
206
+ // FIXME: On the Web, the safe area frame value doesn't update on resize
207
+ // So we workaround this by measuring the frame on resize
208
+ function FrameSizeListenerWeb({
209
+ onChange,
210
+ }: {
211
+ onChange: (frame: Frame) => void;
212
+ }) {
213
+ const elementRef = React.useRef<HTMLDivElement>(null);
214
+
215
+ React.useEffect(() => {
216
+ if (elementRef.current == null) {
217
+ return;
218
+ }
219
+
220
+ const rect = elementRef.current.getBoundingClientRect();
221
+
222
+ onChange({
223
+ width: rect.width,
224
+ height: rect.height,
225
+ });
226
+
227
+ const observer = new ResizeObserver((entries) => {
228
+ const entry = entries[0];
229
+
230
+ if (entry) {
231
+ const { width, height } = entry.contentRect;
232
+
233
+ onChange({ width, height });
234
+ }
235
+ });
236
+
237
+ observer.observe(elementRef.current);
238
+
239
+ return () => {
240
+ observer.disconnect();
241
+ };
242
+ }, [onChange]);
243
+
244
+ return (
245
+ <div
246
+ ref={elementRef}
247
+ style={{
248
+ ...StyleSheet.absoluteFillObject,
249
+ pointerEvents: 'none',
250
+ visibility: 'hidden',
251
+ }}
252
+ />
253
+ );
254
+ }