Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
2e1698b
initial ContentOverlay with @gorhom/bottom-sheet
edison-cy-yang Nov 17, 2025
3c1be3f
backdrop component
edison-cy-yang Nov 17, 2025
d720a5a
handle styling
edison-cy-yang Nov 17, 2025
bbb6b99
header with dismiss button
edison-cy-yang Nov 18, 2025
46f90a1
close bottom sheet modal on Android hardware back button
edison-cy-yang Nov 18, 2025
8469b9e
useBottomSheetModalBackHandler test variables rename
edison-cy-yang Nov 18, 2025
5378443
Merge branch 'JOB-140604-install-react-native-bottom-sheet-and-replac…
edison-cy-yang Nov 18, 2025
37f2b9a
snapPoint, draggable, and fullScreen setup
edison-cy-yang Nov 18, 2025
5c279b1
add scroll view and sticky header
edison-cy-yang Nov 18, 2025
d16787d
fix scrollview issue where scrolled content is showing above the header
edison-cy-yang Nov 18, 2025
5c4b7d5
add top inset
edison-cy-yang Nov 18, 2025
8b904cb
there should always be a snap point of 100%
edison-cy-yang Nov 19, 2025
f233256
use index to determine if overlay is at the top
edison-cy-yang Nov 19, 2025
c6db5e6
use position from onChange to detect if the sheet is at the top
edison-cy-yang Nov 19, 2025
de63e76
implement onBeforeExit
edison-cy-yang Nov 19, 2025
9fe099c
remove extra return
edison-cy-yang Nov 19, 2025
79638b1
hide handle when draggable is false
edison-cy-yang Nov 19, 2025
e35d0dc
update useBottomSheetModalBackHandler
edison-cy-yang Nov 19, 2025
f67bf9e
Merge branch 'JOB-140604-install-react-native-bottom-sheet-and-replac…
jdeichert Nov 26, 2025
3b0c18f
Merge branch 'JOB-140604-install-react-native-bottom-sheet-and-replac…
jdeichert Nov 26, 2025
1ccfde7
Merge branch 'JOB-140604-install-react-native-bottom-sheet-and-replac…
jdeichert Nov 28, 2025
7668ef0
Fix type error
jdeichert Nov 28, 2025
2d179c7
Add TODOs
jdeichert Nov 28, 2025
660fa16
Remove some references to react-native-modalize
jdeichert Nov 28, 2025
7859ace
Delete v7 ContentOverlay story
jdeichert Nov 28, 2025
969e591
Remove ContentOverlayRebuiltProps
jdeichert Nov 28, 2025
2e3c550
Fix broken link
jdeichert Nov 28, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 0 additions & 53 deletions docs/components/ContentOverlay/Mobile.stories.tsx

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { buildThemedStyles } from "../AtlantisThemeContext";

export const useStyles = buildThemedStyles(tokens => {
const modalBorderRadius = tokens["radius-larger"];

return {
handle: {
width: tokens["space-largest"],
height: tokens["space-smaller"] + tokens["space-smallest"],
backgroundColor: tokens["color-border"],
borderRadius: tokens["radius-circle"],
},

backdrop: {
backgroundColor: tokens["color-overlay"],
},

background: {
borderTopLeftRadius: modalBorderRadius,
borderTopRightRadius: modalBorderRadius,
},

modalForLargeScreens: {
width: 640,
alignSelf: "center",
},

header: {
flexDirection: "row",
backgroundColor: tokens["color-surface"],
zIndex: tokens["elevation-base"],
minHeight: tokens["space-extravagant"],
},

headerShadow: {
...tokens["shadow-base"],
},

childrenStyle: {
// We need to explicity lower the zIndex because otherwise, the modal content slides over the header shadow.
zIndex: -1,
},

dismissButton: {
alignItems: "center",
},

hiddenContent: {
opacity: 0,
},

title: {
flex: 1,
justifyContent: "center",
paddingLeft: tokens["space-base"],
},

titleWithoutDismiss: {
paddingRight: tokens["space-base"],
},

titleWithDismiss: {
paddingRight: tokens["space-smaller"],
},
};
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
import React, { useImperativeHandle, useMemo, useRef, useState } from "react";
import { AccessibilityInfo, View, findNodeHandle } from "react-native";
import type { NativeScrollEvent, NativeSyntheticEvent } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import {
BottomSheetBackdrop,
BottomSheetModal,
BottomSheetScrollView,
BottomSheetView,
} from "@gorhom/bottom-sheet";
import type {
BottomSheetBackdropProps,
BottomSheetModal as BottomSheetModalType,
} from "@gorhom/bottom-sheet";
import type { ContentOverlayProps, ModalBackgroundColor } from "./types";
import { useStyles } from "./ContentOverlay.rebuilt.style";
import { useBottomSheetModalBackHandler } from "./hooks/useBottomSheetModalBackHandler";
import { useIsScreenReaderEnabled } from "../hooks";
import { IconButton } from "../IconButton";
import { Heading } from "../Heading";
import { useAtlantisI18n } from "../hooks/useAtlantisI18n";
import { useAtlantisTheme } from "../AtlantisThemeContext";

function getModalBackgroundColor(
variation: ModalBackgroundColor,
tokens: ReturnType<typeof useAtlantisTheme>["tokens"],
) {
switch (variation) {
case "surface":
return tokens["color-surface"];
case "background":
return tokens["color-surface--background"];
}
}

// eslint-disable-next-line max-statements
export function ContentOverlayRebuilt({
children,
title,
accessibilityLabel,
fullScreen = false,
showDismiss = false,
isDraggable = true,
adjustToContentHeight = false,
keyboardShouldPersistTaps = false,
scrollEnabled = false,
modalBackgroundColor = "surface",
onClose,
onOpen,
onBeforeExit,
loading = false,
ref,
}: ContentOverlayProps) {
const insets = useSafeAreaInsets();
const bottomSheetModalRef = useRef<BottomSheetModalType>(null);
const previousIndexRef = useRef(-1);
const [currentPosition, setCurrentPosition] = useState<number>(-1);

const styles = useStyles();
const { t } = useAtlantisI18n();
const { tokens } = useAtlantisTheme();
const isScreenReaderEnabled = useIsScreenReaderEnabled();

const isFullScreenOrTopPosition =
fullScreen || (!adjustToContentHeight && currentPosition === 0);
const shouldShowDismiss =
showDismiss || isScreenReaderEnabled || isFullScreenOrTopPosition;

const draggable = onBeforeExit ? false : isDraggable;

const [showHeaderShadow, setShowHeaderShadow] = useState<boolean>(false);
const overlayHeader = useRef<View>(null);

// If isDraggable is true, we always want to have a snap point at 100%
// enableDynamicSizing will add another snap point of the content height
const snapPoints = useMemo(() => ["100%"], []);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The old implementation had a lot of logic here, but it's significantly simplified here due to how the library handles snap points. From what I can tell, we want to always provide the ability to snap to 100% when isDraggable is true.
So we will always have 100% in snapPoints.

When enableDynamicSizing is true, it automatically adds another snap point of the content height to snapPoints. For example, [1000] becomes [400, 1000].


const onCloseController = () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Existing implementations of onBeforeExit is used for things like resetting forms that are in the overlay when you leave.
When onBeforeExit is supplied:

  • Click on backdrop to close is disabled
  • Pan down to close is disabled

You would have to exit through the dismiss button, or the Android back button.

if (!onBeforeExit) {
bottomSheetModalRef.current?.dismiss();
} else {
onBeforeExit();
}
};

const { handleSheetPositionChange } =
useBottomSheetModalBackHandler(onCloseController);

useImperativeHandle(ref, () => ({
open: () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Map to the existing API to open and close

bottomSheetModalRef.current?.present();
},
close: () => {
bottomSheetModalRef.current?.dismiss();
},
}));

const handleChange = (index: number, position: number) => {
const previousIndex = previousIndexRef.current;

setCurrentPosition(position);
handleSheetPositionChange(index);

if (previousIndex === -1 && index >= 0) {
// Transitioned from closed to open
onOpen?.();

// Set accessibility focus on header when opened
if (overlayHeader.current) {
const reactTag = findNodeHandle(overlayHeader.current);

if (reactTag) {
AccessibilityInfo.setAccessibilityFocus(reactTag);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't actually had the chance to test if this is working the way we intend it to. This was brought over from the existing implementation

}
}
} else if (previousIndex >= 0 && index === -1) {
// Transitioned from open to closed
onClose?.();
}

previousIndexRef.current = index;
};

const handleOnScroll = ({
nativeEvent,
}: NativeSyntheticEvent<NativeScrollEvent>) => {
setShowHeaderShadow(nativeEvent.contentOffset.y > 0);
};

const renderHeader = () => {
const closeOverlayA11YLabel = t("ContentOverlay.close", {
title: title,
});

const headerStyles = [
styles.header,
showHeaderShadow && styles.headerShadow,
{
backgroundColor: getModalBackgroundColor(modalBackgroundColor, tokens),
},
];

return (
<View testID="ATL-Overlay-Header">
<View style={headerStyles}>
<View
style={[
styles.title,
shouldShowDismiss
? styles.titleWithDismiss
: styles.titleWithoutDismiss,
]}
>
<Heading
level="subtitle"
variation={loading ? "subdued" : "heading"}
align={"start"}
>
{title}
</Heading>
</View>

{shouldShowDismiss && (
<View
style={styles.dismissButton}
ref={overlayHeader}
accessibilityLabel={accessibilityLabel || closeOverlayA11YLabel}
accessible={true}
>
<IconButton
name="cross"
customColor={
loading ? tokens["color-disabled"] : tokens["color-heading"]
}
onPress={() => onCloseController()}
accessibilityLabel={closeOverlayA11YLabel}
testID="ATL-Overlay-CloseButton"
/>
</View>
)}
</View>
</View>
);
};

return (
<BottomSheetModal
ref={bottomSheetModalRef}
onChange={handleChange}
backgroundStyle={styles.background}
handleIndicatorStyle={styles.handle}
handleComponent={draggable ? undefined : null}
backdropComponent={props => (
<Backdrop {...props} pressBehavior={onBeforeExit ? "none" : "close"} />
)}
name="content-overlay-rebuilt"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Official documentation states that the name prop is "Modal name to help identify the modal for later on." I am not sure how it could be used or if it even is applicable to how we use ContentOverlay

snapPoints={snapPoints}
enablePanDownToClose={draggable}
enableContentPanningGesture={draggable}
enableDynamicSizing={!fullScreen}
topInset={insets.top}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This avoids the bottom sheet modal to open all the way up into the top inset area. It looks different from the existing implementation:
Screenshot 2025-11-19 at 1 18 32 PM

With @gorhom/bottom-sheet:
Screenshot 2025-11-19 at 1 18 42 PM

>
{scrollEnabled ? (
<BottomSheetScrollView
style={styles.background}
contentContainerStyle={{ paddingBottom: insets.bottom }}
keyboardShouldPersistTaps={
keyboardShouldPersistTaps ? "handled" : "never"
}
showsVerticalScrollIndicator={false}
onScroll={handleOnScroll}
stickyHeaderIndices={[0]}
>
{renderHeader()}
{children}
</BottomSheetScrollView>
) : (
<BottomSheetView style={styles.background}>
{renderHeader()}
<View style={{ paddingBottom: insets.bottom }}>{children}</View>
</BottomSheetView>
)}
</BottomSheetModal>
);
}

function Backdrop(
bottomSheetBackdropProps: BottomSheetBackdropProps & {
pressBehavior: "none" | "close";
},
) {
const styles = useStyles();
const { pressBehavior, ...props } = bottomSheetBackdropProps;

return (
<BottomSheetBackdrop
{...props}
appearsOnIndex={0}
disappearsOnIndex={-1}
style={styles.backdrop}
opacity={1}
pressBehavior={pressBehavior}
/>
);
}
Loading