@@ -482,7 +482,8 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
482482 const { isNested, dismissAbove, descendants } = useSheetStack (
483483 methodsRef ,
484484 drawerContentRef ,
485- isOpen
485+ isOpen ,
486+ isLandscapeOrTablet && presentation === 'form'
486487 ) ;
487488 dismissAboveRef . current = dismissAbove ;
488489
@@ -492,12 +493,18 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
492493 useEffect ( ( ) => {
493494 const parent = drawerContentRef . current ;
494495 if ( ! parent ) return ;
496+ const parentWrapper = parent . closest < HTMLElement > ( '[data-vaul-detached-wrapper]' ) ;
495497
496498 const transition = `transform ${ TRANSITIONS . DURATION } s cubic-bezier(${ TRANSITIONS . EASE . join ( ',' ) } )` ;
499+ const wrapperTransition = `clip-path ${ TRANSITIONS . DURATION } s cubic-bezier(${ TRANSITIONS . EASE . join ( ',' ) } )` ;
497500
498501 if ( descendants . length === 0 ) {
499502 parent . style . transition = transition ;
500503 parent . style . transform = '' ;
504+ if ( parentWrapper && parentWrapper . style . clipPath ) {
505+ parentWrapper . style . transition = wrapperTransition ;
506+ parentWrapper . style . clipPath = 'inset(0px round 0px)' ;
507+ }
501508 return ;
502509 }
503510
@@ -513,13 +520,67 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
513520 return targetY ;
514521 } ;
515522
523+ // When a form-sheet descendant is present, clip the parent to the form
524+ // card's viewport box so the parts that would peek above/around the card
525+ // are hidden. Form-card box is derived from the child's inline styles
526+ // (vs. getBoundingClientRect) so it's the at-rest box, not skewed by
527+ // vaul's wrapper slide-in. Insets are relative to the parent wrapper's
528+ // rect — the parent wrapper isn't necessarily viewport-sized (e.g., it
529+ // has DEFAULT_MAX_WIDTH on tablet/landscape).
530+ // Clip-path transitions with the same duration/easing as the cascade
531+ // transform so the visible box and the drawer's content slide together;
532+ // otherwise the content animates against a static wrapper boundary.
533+ // We seed clip-path from `inset(0)` (= no clip) before setting the target
534+ // inset so the browser interpolates between two inset() shapes — going
535+ // from `none` directly to inset() is interpolated inconsistently.
536+ const applyFormClip = ( ) => {
537+ if ( ! parentWrapper ) return ;
538+ const form = descendants . find ( ( d ) => d . isFormSheetRef . current ) ;
539+ if ( ! form ) {
540+ if ( parentWrapper . style . clipPath ) {
541+ parentWrapper . style . transition = wrapperTransition ;
542+ parentWrapper . style . clipPath = 'inset(0px round 0px)' ;
543+ }
544+ return ;
545+ }
546+ const childDrawer = form . nodeRef . current ;
547+ const childWrapper = childDrawer ?. closest < HTMLElement > ( '[data-vaul-detached-wrapper]' ) ;
548+ if ( ! childDrawer || ! childWrapper ) return ;
549+ const snapY = parseFloat ( childDrawer . style . getPropertyValue ( '--snap-point-height' ) ) || 0 ;
550+ const childBottomGap = parseFloat ( childWrapper . style . bottom ) || 0 ;
551+ const childMaxW =
552+ parseFloat ( childWrapper . style . maxWidth ) || window . innerWidth ;
553+ const formLeft = ( window . innerWidth - childMaxW ) / 2 ;
554+ const formRight = ( window . innerWidth + childMaxW ) / 2 ;
555+ const formBottom = window . innerHeight - childBottomGap ;
556+ const rect = parentWrapper . getBoundingClientRect ( ) ;
557+ const top = Math . max ( 0 , snapY - rect . top ) ;
558+ const left = Math . max ( 0 , formLeft - rect . left ) ;
559+ const right = Math . max ( 0 , rect . right - formRight ) ;
560+ const bottom = Math . max ( 0 , rect . bottom - formBottom ) ;
561+ // Seed inset(0 round 0) so the first transition interpolates between
562+ // two inset() shapes with matching `round` values (a missing `round`
563+ // would interpolate inconsistently).
564+ if ( ! parentWrapper . style . clipPath ) {
565+ parentWrapper . style . transition = '' ;
566+ parentWrapper . style . clipPath = 'inset(0px round 0px)' ;
567+ // eslint-disable-next-line no-void
568+ void parentWrapper . offsetHeight ;
569+ }
570+ parentWrapper . style . transition = wrapperTransition ;
571+ const radius = cornerRadius ?? DEFAULT_CORNER_RADIUS ;
572+ parentWrapper . style . clipPath = `inset(${ top } px ${ right } px ${ bottom } px ${ left } px round ${ radius } px)` ;
573+ } ;
574+
516575 const apply = ( ) => {
517576 const targetY = computeTargetY ( ) ;
518577 const match = parent . style . transform . match ( / t r a n s l a t e 3 d \( [ ^ , ] * , \s * ( - ? \d * \. ? \d + ) p x / ) ;
519578 const currentY = match ? parseFloat ( match [ 1 ] ! ) : 0 ;
520- if ( Math . abs ( currentY - targetY ) < 0.5 ) return ;
521- parent . style . transition = transition ;
522- parent . style . transform = `translate3d(0, ${ targetY } px, 0)` ;
579+ if ( Math . abs ( currentY - targetY ) >= 0.5 ) {
580+ parent . style . transition = transition ;
581+ parent . style . transform = `translate3d(0, ${ targetY } px, 0)` ;
582+ }
583+ applyFormClip ( ) ;
523584 } ;
524585
525586 const raf = requestAnimationFrame ( apply ) ;
0 commit comments