diff options
4 files changed, 116 insertions, 58 deletions
diff --git a/modules/bottom-sheet/android/src/main/java/expo/modules/bottomsheet/BottomSheetView.kt b/modules/bottom-sheet/android/src/main/java/expo/modules/bottomsheet/BottomSheetView.kt index 56b5b3f05..5df163b64 100644 --- a/modules/bottom-sheet/android/src/main/java/expo/modules/bottomsheet/BottomSheetView.kt +++ b/modules/bottom-sheet/android/src/main/java/expo/modules/bottomsheet/BottomSheetView.kt @@ -31,9 +31,13 @@ class BottomSheetView( private lateinit var dialogRootViewGroup: DialogRootViewGroup private var eventDispatcher: EventDispatcher? = null - private val screenHeight = - context.resources.displayMetrics.heightPixels - .toFloat() + private val rawScreenHeight = context.resources.displayMetrics.heightPixels.toFloat() + private val safeScreenHeight = (rawScreenHeight - getNavigationBarHeight()).toFloat() + + private fun getNavigationBarHeight(): Int { + val resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android") + return if (resourceId > 0) resources.getDimensionPixelSize(resourceId) else 0 + } private val onAttemptDismiss by EventDispatcher() private val onSnapPointChange by EventDispatcher() @@ -63,12 +67,12 @@ class BottomSheetView( } } - var maxHeight = this.screenHeight + var maxHeight = this.safeScreenHeight set(value) { val px = dpToPx(value) field = - if (px > this.screenHeight) { - this.screenHeight + if (px > this.safeScreenHeight) { + this.safeScreenHeight } else { px } @@ -153,6 +157,19 @@ class BottomSheetView( // Presentation + private fun getHalfExpandedRatio(contentHeight: Float): Float { + return when { + // Full height sheets + contentHeight >= safeScreenHeight -> 0.99f + // Medium height sheets (>50% but <100%) + contentHeight >= safeScreenHeight / 2 -> + this.clampRatio(this.getTargetHeight() / safeScreenHeight) + // Small height sheets (<50%) + else -> + this.clampRatio(this.getTargetHeight() / rawScreenHeight) + } + } + private fun present() { if (this.isOpen || this.isOpening || this.isClosing) return @@ -172,12 +189,12 @@ class BottomSheetView( val behavior = BottomSheetBehavior.from(it) behavior.state = BottomSheetBehavior.STATE_HIDDEN behavior.isFitToContents = true - behavior.halfExpandedRatio = this.clampRatio(this.getTargetHeight() / this.screenHeight) + behavior.halfExpandedRatio = getHalfExpandedRatio(contentHeight) behavior.skipCollapsed = true behavior.isDraggable = true behavior.isHideable = true - if (contentHeight >= this.screenHeight || this.minHeight >= this.screenHeight) { + if (contentHeight >= this.safeScreenHeight || this.minHeight >= this.safeScreenHeight) { behavior.state = BottomSheetBehavior.STATE_EXPANDED this.selectedSnapPoint = 2 } else { @@ -227,11 +244,11 @@ class BottomSheetView( bottomSheet?.let { val behavior = BottomSheetBehavior.from(it) - behavior.halfExpandedRatio = this.clampRatio(this.getTargetHeight() / this.screenHeight) + behavior.halfExpandedRatio = getHalfExpandedRatio(contentHeight) - if (contentHeight > this.screenHeight && behavior.state != BottomSheetBehavior.STATE_EXPANDED) { + if (contentHeight > this.safeScreenHeight && behavior.state != BottomSheetBehavior.STATE_EXPANDED) { behavior.state = BottomSheetBehavior.STATE_EXPANDED - } else if (contentHeight < this.screenHeight && behavior.state != BottomSheetBehavior.STATE_HALF_EXPANDED) { + } else if (contentHeight < this.safeScreenHeight && behavior.state != BottomSheetBehavior.STATE_HALF_EXPANDED) { behavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED } } diff --git a/modules/bottom-sheet/src/BottomSheetNativeComponent.tsx b/modules/bottom-sheet/src/BottomSheetNativeComponent.tsx index acd46ce01..1fe592aa2 100644 --- a/modules/bottom-sheet/src/BottomSheetNativeComponent.tsx +++ b/modules/bottom-sheet/src/BottomSheetNativeComponent.tsx @@ -1,14 +1,17 @@ import * as React from 'react' import { Dimensions, + LayoutChangeEvent, NativeSyntheticEvent, Platform, StyleProp, View, ViewStyle, } from 'react-native' +import {useSafeAreaInsets} from 'react-native-safe-area-context' import {requireNativeModule, requireNativeViewManager} from 'expo-modules-core' +import {isIOS} from '#/platform/detection' import {BottomSheetState, BottomSheetViewProps} from './BottomSheet.types' import {BottomSheetPortalProvider} from './BottomSheetPortal' import {Context as PortalContext} from './BottomSheetPortal' @@ -81,12 +84,10 @@ export class BottomSheetNativeComponent extends React.Component< return null } - const {children, backgroundColor, ...rest} = this.props - const cornerRadius = rest.cornerRadius ?? 0 - let extraStyles if (isIOS15 && this.state.viewHeight) { const {viewHeight} = this.state + const cornerRadius = this.props.cornerRadius ?? 0 if (viewHeight < screenHeight / 2) { extraStyles = { height: viewHeight, @@ -99,39 +100,70 @@ export class BottomSheetNativeComponent extends React.Component< return ( <Portal> - <NativeView - {...rest} + <BottomSheetNativeComponentInner + {...this.props} + nativeViewRef={this.ref} onStateChange={this.onStateChange} - ref={this.ref} - style={{ - position: 'absolute', - height: screenHeight, - width: '100%', + extraStyles={extraStyles} + onLayout={e => { + const {height} = e.nativeEvent.layout + this.setState({viewHeight: height}) + this.updateLayout() }} - containerBackgroundColor={backgroundColor}> - <View - style={[ - { - flex: 1, - backgroundColor, - }, - Platform.OS === 'android' && { - borderTopLeftRadius: cornerRadius, - borderTopRightRadius: cornerRadius, - }, - extraStyles, - ]}> - <View - onLayout={e => { - const {height} = e.nativeEvent.layout - this.setState({viewHeight: height}) - this.updateLayout() - }}> - <BottomSheetPortalProvider>{children}</BottomSheetPortalProvider> - </View> - </View> - </NativeView> + /> </Portal> ) } } + +function BottomSheetNativeComponentInner({ + children, + backgroundColor, + onLayout, + onStateChange, + nativeViewRef, + extraStyles, + ...rest +}: BottomSheetViewProps & { + extraStyles?: StyleProp<ViewStyle> + onStateChange: ( + event: NativeSyntheticEvent<{state: BottomSheetState}>, + ) => void + nativeViewRef: React.RefObject<View> + onLayout: (event: LayoutChangeEvent) => void +}) { + const insets = useSafeAreaInsets() + const cornerRadius = rest.cornerRadius ?? 0 + + const sheetHeight = isIOS ? screenHeight - insets.top : screenHeight + + return ( + <NativeView + {...rest} + onStateChange={onStateChange} + ref={nativeViewRef} + style={{ + position: 'absolute', + height: sheetHeight, + width: '100%', + }} + containerBackgroundColor={backgroundColor}> + <View + style={[ + { + flex: 1, + backgroundColor, + }, + Platform.OS === 'android' && { + borderTopLeftRadius: cornerRadius, + borderTopRightRadius: cornerRadius, + }, + extraStyles, + ]}> + <View onLayout={onLayout}> + <BottomSheetPortalProvider>{children}</BottomSheetPortalProvider> + </View> + </View> + </NativeView> + ) +} diff --git a/src/components/Dialog/index.tsx b/src/components/Dialog/index.tsx index 597964e29..e70e4aef4 100644 --- a/src/components/Dialog/index.tsx +++ b/src/components/Dialog/index.tsx @@ -26,7 +26,7 @@ import {isAndroid, isIOS} from '#/platform/detection' import {useA11y} from '#/state/a11y' import {useDialogStateControlContext} from '#/state/dialogs' import {List, ListMethods, ListProps} from '#/view/com/util/List' -import {atoms as a, useTheme} from '#/alf' +import {atoms as a, tokens, useTheme} from '#/alf' import {useThemeName} from '#/alf/util/useColorModeTheme' import {Context, useDialogContext} from '#/components/Dialog/context' import { @@ -46,7 +46,7 @@ export {useDialogContext, useDialogControl} from '#/components/Dialog/context' export * from '#/components/Dialog/shared' export * from '#/components/Dialog/types' export * from '#/components/Dialog/utils' -// @ts-ignore + export const Input = createInput(TextInput) export function Outer({ @@ -168,7 +168,9 @@ export function Outer({ onStateChange={onStateChange} disableDrag={disableDrag}> <Context.Provider value={context}> - <View testID={testID}>{children}</View> + <View testID={testID} style={[a.relative]}> + {children} + </View> </Context.Provider> </BottomSheet> ) @@ -196,7 +198,7 @@ export function Inner({children, style, header}: DialogInnerProps) { export const ScrollableInner = React.forwardRef<ScrollView, DialogInnerProps>( function ScrollableInner( - {children, style, contentContainerStyle, header, ...props}, + {children, contentContainerStyle, header, ...props}, ref, ) { const {nativeSnapPoint, disableDrag, setDisableDrag} = useDialogContext() @@ -216,13 +218,21 @@ export const ScrollableInner = React.forwardRef<ScrollView, DialogInnerProps>( [], ) - const basePading = - (isIOS ? 30 : 50) + (isIOS ? keyboardHeight / 4 : keyboardHeight) - const fullPaddingBase = insets.bottom + insets.top + basePading - const fullPadding = isIOS ? fullPaddingBase : fullPaddingBase + 50 - - const paddingBottom = - nativeSnapPoint === BottomSheetSnapPoint.Full ? fullPadding : basePading + let paddingBottom = 0 + if (isIOS) { + paddingBottom += keyboardHeight / 4 + if (nativeSnapPoint === BottomSheetSnapPoint.Full) { + paddingBottom += insets.bottom + tokens.space.md + } + paddingBottom = Math.max(paddingBottom, tokens.space._2xl) + } else { + paddingBottom += keyboardHeight + if (nativeSnapPoint === BottomSheetSnapPoint.Full) { + paddingBottom += insets.top + } + paddingBottom += + Math.max(insets.bottom, tokens.space._5xl) + tokens.space._2xl + } const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => { if (!isAndroid) { @@ -238,7 +248,6 @@ export const ScrollableInner = React.forwardRef<ScrollView, DialogInnerProps>( return ( <KeyboardAwareScrollView - style={[style]} contentContainerStyle={[ a.pt_2xl, a.px_xl, @@ -316,7 +325,7 @@ export function Handle() { style={[ a.rounded_sm, { - top: 10, + top: tokens.space._2xl / 2 - 2.5, width: 35, height: 5, alignSelf: 'center', diff --git a/src/components/Menu/index.tsx b/src/components/Menu/index.tsx index 9c970b051..06b9e7e55 100644 --- a/src/components/Menu/index.tsx +++ b/src/components/Menu/index.tsx @@ -100,7 +100,7 @@ export function Outer({ <Dialog.Handle /> {/* Re-wrap with context since Dialogs are portal-ed to root */} <Context.Provider value={context}> - <Dialog.ScrollableInner label={_(msg`Menu`)} style={[a.py_sm]}> + <Dialog.ScrollableInner label={_(msg`Menu`)}> <View style={[a.gap_lg]}> {children} {isNative && showCancel && <Cancel />} |