@react-navigation/stack 7.4.4 → 7.4.6

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 (32) hide show
  1. package/lib/module/views/Stack/Card.js +65 -81
  2. package/lib/module/views/Stack/Card.js.map +1 -1
  3. package/lib/module/views/Stack/CardA11yWrapper.js +44 -0
  4. package/lib/module/views/Stack/CardA11yWrapper.js.map +1 -0
  5. package/lib/module/views/Stack/CardContainer.js +71 -70
  6. package/lib/module/views/Stack/CardContainer.js.map +1 -1
  7. package/lib/module/views/Stack/{CardSheet.js → CardContent.js} +5 -13
  8. package/lib/module/views/Stack/CardContent.js.map +1 -0
  9. package/lib/module/views/Stack/CardStack.js +26 -21
  10. package/lib/module/views/Stack/CardStack.js.map +1 -1
  11. package/lib/module/views/Stack/StackView.js +2 -2
  12. package/lib/module/views/Stack/StackView.js.map +1 -1
  13. package/lib/typescript/src/views/Stack/Card.d.ts +5 -5
  14. package/lib/typescript/src/views/Stack/Card.d.ts.map +1 -1
  15. package/lib/typescript/src/views/Stack/CardA11yWrapper.d.ts +15 -0
  16. package/lib/typescript/src/views/Stack/CardA11yWrapper.d.ts.map +1 -0
  17. package/lib/typescript/src/views/Stack/CardContainer.d.ts.map +1 -1
  18. package/lib/typescript/src/views/Stack/CardContent.d.ts +13 -0
  19. package/lib/typescript/src/views/Stack/CardContent.d.ts.map +1 -0
  20. package/lib/typescript/src/views/Stack/CardStack.d.ts +2 -1
  21. package/lib/typescript/src/views/Stack/CardStack.d.ts.map +1 -1
  22. package/package.json +5 -5
  23. package/src/views/Stack/Card.tsx +96 -111
  24. package/src/views/Stack/CardA11yWrapper.tsx +65 -0
  25. package/src/views/Stack/CardContainer.tsx +92 -98
  26. package/src/views/Stack/CardContent.tsx +92 -0
  27. package/src/views/Stack/CardStack.tsx +51 -42
  28. package/src/views/Stack/StackView.tsx +2 -2
  29. package/lib/module/views/Stack/CardSheet.js.map +0 -1
  30. package/lib/typescript/src/views/Stack/CardSheet.d.ts +0 -14
  31. package/lib/typescript/src/views/Stack/CardSheet.d.ts.map +0 -1
  32. package/src/views/Stack/CardSheet.tsx +0 -106
@@ -8,7 +8,6 @@ import {
8
8
  type StyleProp,
9
9
  StyleSheet,
10
10
  View,
11
- type ViewProps,
12
11
  type ViewStyle,
13
12
  } from 'react-native';
14
13
  import type { EdgeInsets } from 'react-native-safe-area-context';
@@ -30,9 +29,10 @@ import {
30
29
  PanGestureHandler,
31
30
  type PanGestureHandlerGestureEvent,
32
31
  } from '../GestureHandler';
33
- import { CardSheet, type CardSheetRef } from './CardSheet';
32
+ import { CardContent } from './CardContent';
34
33
 
35
- type Props = ViewProps & {
34
+ type Props = {
35
+ animated: boolean;
36
36
  interpolationIndex: number;
37
37
  opening: boolean;
38
38
  closing: boolean;
@@ -181,11 +181,11 @@ export class Card extends React.Component<Props> {
181
181
 
182
182
  private isSwiping = new Animated.Value(FALSE);
183
183
 
184
- private interactionHandle: number | undefined;
184
+ private lastToValue: number | undefined;
185
185
 
186
+ private interactionHandle: number | undefined;
186
187
  private pendingGestureCallback: number | undefined;
187
-
188
- private lastToValue: number | undefined;
188
+ private animationHandle: number | undefined;
189
189
 
190
190
  private animate = ({
191
191
  closing,
@@ -194,7 +194,7 @@ export class Card extends React.Component<Props> {
194
194
  closing: boolean;
195
195
  velocity?: number;
196
196
  }) => {
197
- const { transitionSpec, onOpen, onClose, onTransition, gesture } =
197
+ const { animated, transitionSpec, onOpen, onClose, onTransition, gesture } =
198
198
  this.props;
199
199
 
200
200
  const toValue = this.getAnimateToValue({
@@ -211,36 +211,50 @@ export class Card extends React.Component<Props> {
211
211
  const animation =
212
212
  spec.animation === 'spring' ? Animated.spring : Animated.timing;
213
213
 
214
- this.setPointerEventsEnabled(!closing);
215
- this.handleStartInteraction();
216
-
217
214
  clearTimeout(this.pendingGestureCallback);
218
215
 
216
+ if (this.animationHandle !== undefined) {
217
+ cancelAnimationFrame(this.animationHandle);
218
+ }
219
+
219
220
  onTransition?.({ closing, gesture: velocity !== undefined });
220
- animation(gesture, {
221
- ...spec.config,
222
- velocity,
223
- toValue,
224
- useNativeDriver,
225
- isInteraction: false,
226
- }).start(({ finished }) => {
227
- this.handleEndInteraction();
228
-
229
- clearTimeout(this.pendingGestureCallback);
230
-
231
- if (finished) {
232
- if (closing) {
233
- onClose();
234
- } else {
235
- onOpen();
236
- }
237
221
 
222
+ const onFinish = () => {
223
+ if (closing) {
224
+ onClose();
225
+ } else {
226
+ onOpen();
227
+ }
228
+
229
+ this.animationHandle = requestAnimationFrame(() => {
238
230
  if (this.isCurrentlyMounted) {
239
231
  // Make sure to re-open screen if it wasn't removed
240
232
  this.forceUpdate();
241
233
  }
242
- }
243
- });
234
+ });
235
+ };
236
+
237
+ if (animated) {
238
+ this.handleStartInteraction();
239
+
240
+ animation(gesture, {
241
+ ...spec.config,
242
+ velocity,
243
+ toValue,
244
+ useNativeDriver,
245
+ isInteraction: false,
246
+ }).start(({ finished }) => {
247
+ this.handleEndInteraction();
248
+
249
+ clearTimeout(this.pendingGestureCallback);
250
+
251
+ if (finished) {
252
+ onFinish();
253
+ }
254
+ });
255
+ } else {
256
+ onFinish();
257
+ }
244
258
  };
245
259
 
246
260
  private getAnimateToValue = ({
@@ -267,12 +281,6 @@ export class Card extends React.Component<Props> {
267
281
  );
268
282
  };
269
283
 
270
- private setPointerEventsEnabled = (enabled: boolean) => {
271
- const pointerEvents = enabled ? 'box-none' : 'none';
272
-
273
- this.ref.current?.setPointerEvents(pointerEvents);
274
- };
275
-
276
284
  private handleStartInteraction = () => {
277
285
  if (this.interactionHandle === undefined) {
278
286
  this.interactionHandle = InteractionManager.createInteractionHandle();
@@ -462,8 +470,6 @@ export class Card extends React.Component<Props> {
462
470
  }
463
471
  }
464
472
 
465
- private ref = React.createRef<CardSheetRef>();
466
-
467
473
  render() {
468
474
  const {
469
475
  styleInterpolator,
@@ -482,20 +488,6 @@ export class Card extends React.Component<Props> {
482
488
  children,
483
489
  containerStyle: customContainerStyle,
484
490
  contentStyle,
485
- /* eslint-disable @typescript-eslint/no-unused-vars */
486
- closing,
487
- direction,
488
- gestureResponseDistance,
489
- gestureVelocityImpact,
490
- onClose,
491
- onGestureBegin,
492
- onGestureCanceled,
493
- onGestureEnd,
494
- onOpen,
495
- onTransition,
496
- transitionSpec,
497
- /* eslint-enable @typescript-eslint/no-unused-vars */
498
- ...rest
499
491
  } = this.props;
500
492
 
501
493
  const interpolationProps = this.getCardAnimation(
@@ -540,72 +532,65 @@ export class Card extends React.Component<Props> {
540
532
 
541
533
  return (
542
534
  <CardAnimationContext.Provider value={interpolationProps}>
535
+ {Platform.OS !== 'web' ? (
536
+ <Animated.View
537
+ style={{
538
+ // This is a dummy style that doesn't actually change anything visually.
539
+ // Animated needs the animated value to be used somewhere, otherwise things don't update properly.
540
+ // If we disable animations and hide header, it could end up making the value unused.
541
+ // So we have this dummy style that will always be used regardless of what else changed.
542
+ opacity: current,
543
+ }}
544
+ // Make sure that this view isn't removed. If this view is removed, our style with animated value won't apply
545
+ collapsable={false}
546
+ />
547
+ ) : null}
548
+ {overlayEnabled ? (
549
+ <View pointerEvents="box-none" style={StyleSheet.absoluteFill}>
550
+ {overlay({ style: overlayStyle })}
551
+ </View>
552
+ ) : null}
543
553
  <Animated.View
544
- style={{
545
- // This is a dummy style that doesn't actually change anything visually.
546
- // Animated needs the animated value to be used somewhere, otherwise things don't update properly.
547
- // If we disable animations and hide header, it could end up making the value unused.
548
- // So we have this dummy style that will always be used regardless of what else changed.
549
- opacity: current,
550
- }}
551
- // Make sure that this view isn't removed. If this view is removed, our style with animated value won't apply
552
- collapsable={false}
553
- />
554
- <View
554
+ style={[styles.container, containerStyle, customContainerStyle]}
555
555
  pointerEvents="box-none"
556
- // Make sure this view is not removed on the new architecture, as it causes focus loss during navigation on Android.
557
- // This can happen when the view flattening results in different trees - due to `overflow` style changing in a parent.
558
- collapsable={false}
559
- {...rest}
560
556
  >
561
- {overlayEnabled ? (
562
- <View pointerEvents="box-none" style={StyleSheet.absoluteFill}>
563
- {overlay({ style: overlayStyle })}
564
- </View>
565
- ) : null}
566
- <Animated.View
567
- style={[styles.container, containerStyle, customContainerStyle]}
568
- pointerEvents="box-none"
557
+ <PanGestureHandler
558
+ enabled={layout.width !== 0 && gestureEnabled}
559
+ onGestureEvent={handleGestureEvent}
560
+ onHandlerStateChange={this.handleGestureStateChange}
561
+ {...this.gestureActivationCriteria()}
569
562
  >
570
- <PanGestureHandler
571
- enabled={layout.width !== 0 && gestureEnabled}
572
- onGestureEvent={handleGestureEvent}
573
- onHandlerStateChange={this.handleGestureStateChange}
574
- {...this.gestureActivationCriteria()}
563
+ <Animated.View
564
+ needsOffscreenAlphaCompositing={hasOpacityStyle(cardStyle)}
565
+ style={[styles.container, cardStyle]}
575
566
  >
576
- <Animated.View
577
- needsOffscreenAlphaCompositing={hasOpacityStyle(cardStyle)}
578
- style={[styles.container, cardStyle]}
567
+ {shadowEnabled && shadowStyle && !isTransparent ? (
568
+ <Animated.View
569
+ style={[
570
+ styles.shadow,
571
+ gestureDirection === 'horizontal'
572
+ ? [styles.shadowHorizontal, styles.shadowStart]
573
+ : gestureDirection === 'horizontal-inverted'
574
+ ? [styles.shadowHorizontal, styles.shadowEnd]
575
+ : gestureDirection === 'vertical'
576
+ ? [styles.shadowVertical, styles.shadowTop]
577
+ : [styles.shadowVertical, styles.shadowBottom],
578
+ { backgroundColor },
579
+ shadowStyle,
580
+ ]}
581
+ pointerEvents="none"
582
+ />
583
+ ) : null}
584
+ <CardContent
585
+ enabled={pageOverflowEnabled}
586
+ layout={layout}
587
+ style={contentStyle}
579
588
  >
580
- {shadowEnabled && shadowStyle && !isTransparent ? (
581
- <Animated.View
582
- style={[
583
- styles.shadow,
584
- gestureDirection === 'horizontal'
585
- ? [styles.shadowHorizontal, styles.shadowStart]
586
- : gestureDirection === 'horizontal-inverted'
587
- ? [styles.shadowHorizontal, styles.shadowEnd]
588
- : gestureDirection === 'vertical'
589
- ? [styles.shadowVertical, styles.shadowTop]
590
- : [styles.shadowVertical, styles.shadowBottom],
591
- { backgroundColor },
592
- shadowStyle,
593
- ]}
594
- pointerEvents="none"
595
- />
596
- ) : null}
597
- <CardSheet
598
- ref={this.ref}
599
- enabled={pageOverflowEnabled}
600
- layout={layout}
601
- style={contentStyle}
602
- >
603
- {children}
604
- </CardSheet>
605
- </Animated.View>
606
- </PanGestureHandler>
607
- </Animated.View>
608
- </View>
589
+ {children}
590
+ </CardContent>
591
+ </Animated.View>
592
+ </PanGestureHandler>
593
+ </Animated.View>
609
594
  </CardAnimationContext.Provider>
610
595
  );
611
596
  }
@@ -0,0 +1,65 @@
1
+ import * as React from 'react';
2
+ import { Platform, StyleSheet, View } from 'react-native';
3
+
4
+ type Props = {
5
+ focused: boolean;
6
+ active: boolean;
7
+ animated: boolean;
8
+ isNextScreenTransparent: boolean;
9
+ detachCurrentScreen: boolean;
10
+ children: React.ReactNode;
11
+ };
12
+
13
+ export type CardA11yWrapperRef = { setInert: (value: boolean) => void };
14
+
15
+ export const CardA11yWrapper = React.forwardRef(
16
+ (
17
+ {
18
+ focused,
19
+ active,
20
+ animated,
21
+ isNextScreenTransparent,
22
+ detachCurrentScreen,
23
+ children,
24
+ }: Props,
25
+ ref: React.Ref<CardA11yWrapperRef>
26
+ ) => {
27
+ // Manage this in separate component to avoid re-rendering card during gestures
28
+ // Otherwise the gesture animation will be interrupted as state hasn't updated yet
29
+ const [inert, setInert] = React.useState(false);
30
+
31
+ React.useImperativeHandle(ref, () => ({ setInert }), []);
32
+
33
+ const isHidden =
34
+ !animated &&
35
+ isNextScreenTransparent === false &&
36
+ detachCurrentScreen !== false &&
37
+ !focused;
38
+
39
+ return (
40
+ <View
41
+ aria-hidden={!focused}
42
+ pointerEvents={(animated ? inert : !focused) ? 'none' : 'box-none'}
43
+ style={{
44
+ ...StyleSheet.absoluteFillObject,
45
+ // This is necessary to avoid unfocused larger pages increasing scroll area
46
+ // The issue can be seen on the web when a smaller screen is pushed over a larger one
47
+ overflow: active ? undefined : 'hidden',
48
+ // We use visibility on web
49
+ display: Platform.OS !== 'web' && isHidden ? 'none' : 'flex',
50
+ // Hide unfocused screens when animation isn't enabled
51
+ // This is also necessary for a11y on web
52
+ // @ts-expect-error visibility is only available on web
53
+ visibility: isHidden ? 'hidden' : 'visible',
54
+ }}
55
+ // Make sure this view is not removed on the new architecture, as it causes focus loss during navigation on Android.
56
+ // This can happen when the view flattening results in different trees - due to `overflow` style changing in a parent.
57
+ // `Container` has `collapsable={false}` by default so we don't need to set it here.
58
+ >
59
+ {children}
60
+ </View>
61
+ );
62
+ }
63
+ );
64
+
65
+ CardA11yWrapper.displayName = 'CardA11yWrapper';
@@ -18,6 +18,7 @@ import { ModalPresentationContext } from '../../utils/ModalPresentationContext';
18
18
  import { useKeyboardManager } from '../../utils/useKeyboardManager';
19
19
  import type { Props as HeaderContainerProps } from '../Header/HeaderContainer';
20
20
  import { Card } from './Card';
21
+ import { CardA11yWrapper, type CardA11yWrapperRef } from './CardA11yWrapper';
21
22
 
22
23
  type Props = {
23
24
  interpolationIndex: number;
@@ -94,6 +95,8 @@ function CardContainerInner({
94
95
  safeAreaInsetTop,
95
96
  scene,
96
97
  }: Props) {
98
+ const wrapperRef = React.useRef<CardA11yWrapperRef>(null);
99
+
97
100
  const { direction } = useLocale();
98
101
 
99
102
  const parentHeaderHeight = React.useContext(HeaderHeightContext);
@@ -150,6 +153,8 @@ function CardContainerInner({
150
153
  closing: boolean;
151
154
  gesture: boolean;
152
155
  }) => {
156
+ wrapperRef.current?.setInert(closing);
157
+
153
158
  const { route } = scene.descriptor;
154
159
 
155
160
  if (!gesture) {
@@ -172,14 +177,10 @@ function CardContainerInner({
172
177
 
173
178
  const { colors } = useTheme();
174
179
 
175
- const [pointerEvents, setPointerEvents] = React.useState<'box-none' | 'none'>(
176
- 'box-none'
177
- );
178
-
179
180
  React.useEffect(() => {
180
181
  const listener = scene.progress.next?.addListener?.(
181
182
  ({ value }: { value: number }) => {
182
- setPointerEvents(value <= EPSILON ? 'box-none' : 'none');
183
+ wrapperRef.current?.setInert(value > EPSILON);
183
184
  }
184
185
  );
185
186
 
@@ -188,7 +189,7 @@ function CardContainerInner({
188
189
  scene.progress.next?.removeListener?.(listener);
189
190
  }
190
191
  };
191
- }, [pointerEvents, scene.progress.next]);
192
+ }, [scene.progress.next]);
192
193
 
193
194
  const {
194
195
  presentation,
@@ -232,101 +233,94 @@ function CardContainerInner({
232
233
  return undefined;
233
234
  }, [canGoBack, backTitle, href]);
234
235
 
236
+ const animated = animation !== 'none';
237
+
235
238
  return (
236
- <Card
237
- interpolationIndex={interpolationIndex}
238
- gestureDirection={gestureDirection}
239
- layout={layout}
240
- insets={insets}
241
- direction={direction}
242
- gesture={gesture}
243
- current={scene.progress.current}
244
- next={scene.progress.next}
245
- opening={opening}
246
- closing={closing}
247
- onOpen={handleOpen}
248
- onClose={handleClose}
249
- overlay={cardOverlay}
250
- overlayEnabled={cardOverlayEnabled}
251
- shadowEnabled={cardShadowEnabled}
252
- onTransition={handleTransition}
253
- onGestureBegin={handleGestureBegin}
254
- onGestureCanceled={handleGestureCanceled}
255
- onGestureEnd={handleGestureEnd}
256
- gestureEnabled={index === 0 ? false : gestureEnabled}
257
- gestureResponseDistance={gestureResponseDistance}
258
- gestureVelocityImpact={gestureVelocityImpact}
259
- transitionSpec={transitionSpec}
260
- styleInterpolator={cardStyleInterpolator}
261
- aria-hidden={!focused}
262
- pointerEvents={active ? 'box-none' : pointerEvents}
263
- pageOverflowEnabled={headerMode !== 'float' && presentation !== 'modal'}
264
- preloaded={preloaded}
265
- containerStyle={
266
- hasAbsoluteFloatHeader && headerMode !== 'screen'
267
- ? { marginTop: headerHeight }
268
- : null
269
- }
270
- contentStyle={[
271
- {
272
- backgroundColor:
273
- presentation === 'transparentModal'
274
- ? 'transparent'
275
- : colors.background,
276
- },
277
- cardStyle,
278
- ]}
279
- style={[
280
- {
281
- // This is necessary to avoid unfocused larger pages increasing scroll area
282
- // The issue can be seen on the web when a smaller screen is pushed over a larger one
283
- overflow: active ? undefined : 'hidden',
284
- display:
285
- // Hide unfocused screens when animation isn't enabled
286
- // This is also necessary for a11y on web
287
- animation === 'none' &&
288
- isNextScreenTransparent === false &&
289
- detachCurrentScreen !== false &&
290
- !focused
291
- ? 'none'
292
- : 'flex',
293
- },
294
- StyleSheet.absoluteFill,
295
- ]}
239
+ <CardA11yWrapper
240
+ ref={wrapperRef}
241
+ focused={focused}
242
+ active={active}
243
+ animated={animated}
244
+ isNextScreenTransparent={isNextScreenTransparent}
245
+ detachCurrentScreen={detachCurrentScreen}
296
246
  >
297
- <View style={styles.container}>
298
- <ModalPresentationContext.Provider value={modal}>
299
- {headerMode !== 'float'
300
- ? renderHeader({
301
- mode: 'screen',
302
- layout,
303
- scenes: [previousScene, scene],
304
- getPreviousScene,
305
- getFocusedRoute,
306
- onContentHeightChange: onHeaderHeightChange,
307
- style: styles.header,
308
- })
309
- : null}
310
- <View style={styles.scene}>
311
- <HeaderBackContext.Provider value={headerBack}>
312
- <HeaderShownContext.Provider
313
- value={isParentHeaderShown || headerShown !== false}
314
- >
315
- <HeaderHeightContext.Provider
316
- value={
317
- headerShown !== false
318
- ? headerHeight
319
- : (parentHeaderHeight ?? 0)
320
- }
247
+ <Card
248
+ animated={animated}
249
+ interpolationIndex={interpolationIndex}
250
+ gestureDirection={gestureDirection}
251
+ layout={layout}
252
+ insets={insets}
253
+ direction={direction}
254
+ gesture={gesture}
255
+ current={scene.progress.current}
256
+ next={scene.progress.next}
257
+ opening={opening}
258
+ closing={closing}
259
+ onOpen={handleOpen}
260
+ onClose={handleClose}
261
+ overlay={cardOverlay}
262
+ overlayEnabled={cardOverlayEnabled}
263
+ shadowEnabled={cardShadowEnabled}
264
+ onTransition={handleTransition}
265
+ onGestureBegin={handleGestureBegin}
266
+ onGestureCanceled={handleGestureCanceled}
267
+ onGestureEnd={handleGestureEnd}
268
+ gestureEnabled={index === 0 ? false : gestureEnabled}
269
+ gestureResponseDistance={gestureResponseDistance}
270
+ gestureVelocityImpact={gestureVelocityImpact}
271
+ transitionSpec={transitionSpec}
272
+ styleInterpolator={cardStyleInterpolator}
273
+ pageOverflowEnabled={headerMode !== 'float' && presentation !== 'modal'}
274
+ preloaded={preloaded}
275
+ containerStyle={
276
+ hasAbsoluteFloatHeader && headerMode !== 'screen'
277
+ ? { marginTop: headerHeight }
278
+ : null
279
+ }
280
+ contentStyle={[
281
+ {
282
+ backgroundColor:
283
+ presentation === 'transparentModal'
284
+ ? 'transparent'
285
+ : colors.background,
286
+ },
287
+ cardStyle,
288
+ ]}
289
+ >
290
+ <View style={styles.container}>
291
+ <ModalPresentationContext.Provider value={modal}>
292
+ {headerMode !== 'float'
293
+ ? renderHeader({
294
+ mode: 'screen',
295
+ layout,
296
+ scenes: [previousScene, scene],
297
+ getPreviousScene,
298
+ getFocusedRoute,
299
+ onContentHeightChange: onHeaderHeightChange,
300
+ style: styles.header,
301
+ })
302
+ : null}
303
+ <View style={styles.scene}>
304
+ <HeaderBackContext.Provider value={headerBack}>
305
+ <HeaderShownContext.Provider
306
+ value={isParentHeaderShown || headerShown !== false}
321
307
  >
322
- {scene.descriptor.render()}
323
- </HeaderHeightContext.Provider>
324
- </HeaderShownContext.Provider>
325
- </HeaderBackContext.Provider>
326
- </View>
327
- </ModalPresentationContext.Provider>
328
- </View>
329
- </Card>
308
+ <HeaderHeightContext.Provider
309
+ value={
310
+ headerShown !== false
311
+ ? headerHeight
312
+ : (parentHeaderHeight ?? 0)
313
+ }
314
+ >
315
+ {scene.descriptor.render()}
316
+ </HeaderHeightContext.Provider>
317
+ </HeaderShownContext.Provider>
318
+ </HeaderBackContext.Provider>
319
+ </View>
320
+ </ModalPresentationContext.Provider>
321
+ </View>
322
+ </Card>
323
+ </CardA11yWrapper>
330
324
  );
331
325
  }
332
326
 
@@ -0,0 +1,92 @@
1
+ import * as React from 'react';
2
+ import { StyleSheet, View, type ViewProps } from 'react-native';
3
+
4
+ type Props = ViewProps & {
5
+ enabled: boolean;
6
+ layout: { width: number; height: number };
7
+ children: React.ReactNode;
8
+ };
9
+
10
+ // This component will render a page which overflows the screen
11
+ // if the container fills the body by comparing the size
12
+ // This lets the document.body handle scrolling of the content
13
+ // It's necessary for mobile browsers to be able to hide address bar on scroll
14
+ export function CardContent({ enabled, layout, style, ...rest }: Props) {
15
+ const [fill, setFill] = React.useState(false);
16
+
17
+ React.useEffect(() => {
18
+ if (typeof document === 'undefined' || !document.body) {
19
+ // Only run when DOM is available
20
+ return;
21
+ }
22
+
23
+ const width = document.body.clientWidth;
24
+ const height = document.body.clientHeight;
25
+
26
+ // Workaround for mobile Chrome, necessary when a navigation happens
27
+ // when the address bar has already collapsed, which resulted in an
28
+ // empty space at the bottom of the page (matching the height of the
29
+ // address bar). To fix this, it's necessary to update the height of
30
+ // the DOM with the current height of the window.
31
+ // See https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
32
+ const isFullHeight = height === layout.height;
33
+ const id = '__react-navigation-stack-mobile-chrome-viewport-fix';
34
+
35
+ let unsubscribe: (() => void) | undefined;
36
+
37
+ if (isFullHeight && navigator.maxTouchPoints > 0) {
38
+ const style =
39
+ document.getElementById(id) ?? document.createElement('style');
40
+
41
+ style.id = id;
42
+
43
+ const updateStyle = () => {
44
+ const vh = window.innerHeight * 0.01;
45
+
46
+ style.textContent = [
47
+ `:root { --vh: ${vh}px; }`,
48
+ `body { height: calc(var(--vh, 1vh) * 100); }`,
49
+ ].join('\n');
50
+ };
51
+
52
+ updateStyle();
53
+
54
+ if (!document.head.contains(style)) {
55
+ document.head.appendChild(style);
56
+ }
57
+
58
+ window.addEventListener('resize', updateStyle);
59
+
60
+ unsubscribe = () => {
61
+ window.removeEventListener('resize', updateStyle);
62
+ };
63
+ } else {
64
+ // Remove the workaround if the stack does not occupy the whole
65
+ // height of the page
66
+ document.getElementById(id)?.remove();
67
+ }
68
+
69
+ // eslint-disable-next-line @eslint-react/hooks-extra/no-direct-set-state-in-use-effect
70
+ setFill(width === layout.width && height === layout.height);
71
+
72
+ return unsubscribe;
73
+ }, [layout.height, layout.width]);
74
+
75
+ return (
76
+ <View
77
+ {...rest}
78
+ pointerEvents="box-none"
79
+ style={[enabled && fill ? styles.page : styles.card, style]}
80
+ />
81
+ );
82
+ }
83
+
84
+ const styles = StyleSheet.create({
85
+ page: {
86
+ minHeight: '100%',
87
+ },
88
+ card: {
89
+ flex: 1,
90
+ overflow: 'hidden',
91
+ },
92
+ });