Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

- **Web**: Fixed detached/form sheet autopresent animation, scrollable content, footer click, and grabber drag interactions. ([#684](https://github.com/lodev09/react-native-true-sheet/pull/684) by [@lodev09](https://github.com/lodev09))

- **Web**: Clip parent to form-card box when child is a form sheet, skip cascade when form parent has page child, and track only immediate child for cascade target. ([#687](https://github.com/lodev09/react-native-true-sheet/pull/687) by [@lodev09](https://github.com/lodev09))

- **iOS**: Fixed Mac Catalyst build issue. ([#685](https://github.com/lodev09/react-native-true-sheet/pull/685) by [@theeket](https://github.com/theeket))

### ⚠️ Breaking
Expand Down
4 changes: 2 additions & 2 deletions example/bare/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2777,7 +2777,7 @@ PODS:
- ReactCommon/turbomodule/core
- SocketRocket
- Yoga
- RNTrueSheet (3.10.0):
- RNTrueSheet (3.11.0-beta.7):
- boost
- DoubleConversion
- fast_float
Expand Down Expand Up @@ -3248,7 +3248,7 @@ SPEC CHECKSUMS:
RNGestureHandler: b407c25a3f51d22dd7430e6b530f753f112b7e77
RNReanimated: 292cd58688552a22b3fc1cefcfbc49b336dfed68
RNScreens: afaf526a9c804c3b4503f950cf3e67ed81e29ada
RNTrueSheet: 819132cbb5713a5794cc0574c4d5fa7859a7c7de
RNTrueSheet: eca911005316e6b28240ea15132c8f768517a0c1
RNWorklets: 01efdd402d236a13651ea5ea5437ca85a44e7afa
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
Yoga: b669e79fa0f8d3f6f5e35372345f54b99e06b13c
Expand Down
82 changes: 72 additions & 10 deletions src/TrueSheet.web.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,

const { width: windowWidth, height: windowHeight } = useWindowDimensions();
const isLandscapeOrTablet = windowWidth >= 600 || windowWidth > windowHeight;
const isFormSheet = isLandscapeOrTablet && presentation === 'form';

// presentation='form' implies a floating/detached sheet on web — mirrors iOS
// form-sheet semantics where the sheet is never edge-attached.
Expand Down Expand Up @@ -482,7 +483,8 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
const { isNested, dismissAbove, descendants } = useSheetStack(
methodsRef,
drawerContentRef,
isOpen
isOpen,
isFormSheet
);
dismissAboveRef.current = dismissAbove;

Expand All @@ -492,28 +494,89 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
useEffect(() => {
const parent = drawerContentRef.current;
if (!parent) return;
const parentWrapper = parent.closest<HTMLElement>('[data-vaul-detached-wrapper]');

const transition = `transform ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`;
const wrapperTransition = `clip-path ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`;
const CLIP_NONE = 'inset(0px round 0px)';

// Animate clip-path on the wrapper. Dedupes via DOM read so repeat ticks
// (mutation observer) skip identical writes. Seeds CLIP_NONE before the
// first inset() so the browser interpolates between two inset() shapes —
// `none → inset()` interpolates inconsistently.
const setClip = (next: string) => {
if (!parentWrapper) return;
const current = parentWrapper.style.clipPath;
if (current === next) return;
if (next === CLIP_NONE && !current) return;
if (!current) {
parentWrapper.style.transition = '';
parentWrapper.style.clipPath = CLIP_NONE;
// eslint-disable-next-line no-void
void parentWrapper.offsetHeight;
}
parentWrapper.style.transition = wrapperTransition;
parentWrapper.style.clipPath = next;
};

if (descendants.length === 0) {
parent.style.transition = transition;
parent.style.transform = '';
setClip(CLIP_NONE);
return;
}

// Track only the immediate child's snap point. Walking deeper descendants
// would push this sheet further when a grandchild opens, even when our
// own child didn't move (e.g., child skipped its cascade for a page
// grandchild) — leaving a visible gap between this sheet and its child.
const computeTargetY = () => {
const parentSnap = parseFloat(parent.style.getPropertyValue('--snap-point-height')) || 0;
let targetY = parentSnap;
for (const d of descendants) {
const node = d.nodeRef.current;
if (!node) continue;
const snap = parseFloat(node.style.getPropertyValue('--snap-point-height')) || 0;
if (snap > targetY) targetY = snap;
const node = descendants[0]?.nodeRef.current;
if (!node) return parentSnap;
const childSnap = parseFloat(node.style.getPropertyValue('--snap-point-height')) || 0;
return Math.max(parentSnap, childSnap);
};

// When a form-sheet descendant opens, clip the parent to the child card's
// viewport box so it doesn't peek above/around. Geometry comes from the
// child's inline styles (not getBoundingClientRect) to read the at-rest
// box, unskewed by vaul's slide-in.
const applyFormClip = () => {
const form = descendants.find((d) => d.isFormSheetRef.current);
if (!form) {
setClip(CLIP_NONE);
return;
}
return targetY;
const childDrawer = form.nodeRef.current;
const childWrapper = childDrawer?.closest<HTMLElement>('[data-vaul-detached-wrapper]');
if (!parentWrapper || !childDrawer || !childWrapper) return;
const snapY = parseFloat(childDrawer.style.getPropertyValue('--snap-point-height')) || 0;
const childBottomGap = parseFloat(childWrapper.style.bottom) || 0;
const childMaxW = parseFloat(childWrapper.style.maxWidth) || window.innerWidth;
const formLeft = (window.innerWidth - childMaxW) / 2;
const formRight = (window.innerWidth + childMaxW) / 2;
const formBottom = window.innerHeight - childBottomGap;
const rect = parentWrapper.getBoundingClientRect();
const top = Math.max(0, snapY - rect.top);
const left = Math.max(0, formLeft - rect.left);
const right = Math.max(0, rect.right - formRight);
const bottom = Math.max(0, rect.bottom - formBottom);
const radius = cornerRadius ?? DEFAULT_CORNER_RADIUS;
setClip(`inset(${top}px ${right}px ${bottom}px ${left}px round ${radius}px)`);
};

const apply = () => {
applyFormClip();
// Mirror iOS: a page-sheet child fully covers a form-sheet parent, so the
// cascade push-down has no visible effect — and would briefly peek above
// the page during the present animation. Leave the parent put.
const child = descendants[0];
if (isFormSheet && child && !child.isFormSheetRef.current) {
parent.style.transition = transition;
parent.style.transform = '';
return;
}
const targetY = computeTargetY();
const match = parent.style.transform.match(/translate3d\([^,]*,\s*(-?\d*\.?\d+)px/);
const currentY = match ? parseFloat(match[1]!) : 0;
Expand All @@ -533,7 +596,7 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
cancelAnimationFrame(raf);
observer.disconnect();
};
}, [descendants, activeSnapPoint]);
}, [descendants, activeSnapPoint, cornerRadius, isFormSheet]);

// Focus/blur events fire when a descendant sheet appears on top of this one
// (blur) or when all descendants are dismissed (focus). will-events fire
Expand Down Expand Up @@ -662,7 +725,6 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
// with a computed offset that centers it vertically. `presentation` is
// absolute: when 'form', `maxContentWidth` is ignored and the card uses
// DEFAULT_FORM_SHEET_WIDTH.
const isFormSheet = isLandscapeOrTablet && presentation === 'form';

// Vaul measures the auto-size wrapper's offsetHeight (always, post fork).
// Track it here so the form sheet can size its card to fit content,
Expand Down
13 changes: 11 additions & 2 deletions src/TrueSheetProvider.web.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type NodeRef = RefObject<HTMLDivElement | null>;
interface StackEntry {
ref: SheetRef;
nodeRef: NodeRef;
isFormSheetRef: RefObject<boolean>;
}

interface SheetContextValue {
Expand Down Expand Up @@ -166,10 +167,18 @@ export function useRegisterSheet(name: string | undefined, ref: SheetRef): void
* Registers the sheet in the open stack while `isOpen` is true and returns
* live data used by each sheet to render stacked visuals and dismiss children.
*/
export function useSheetStack(ref: SheetRef, nodeRef: NodeRef, isOpen: boolean) {
export function useSheetStack(
ref: SheetRef,
nodeRef: NodeRef,
isOpen: boolean,
isFormSheet: boolean
) {
const ctx = useContext(SheetContext);

const entry = useMemo<StackEntry>(() => ({ ref, nodeRef }), [ref, nodeRef]);
const isFormSheetRef = useRef(isFormSheet);
isFormSheetRef.current = isFormSheet;

const entry = useMemo<StackEntry>(() => ({ ref, nodeRef, isFormSheetRef }), [ref, nodeRef]);

useEffect(() => {
if (!ctx || !isOpen) return;
Expand Down
Loading