11import { animated , useTransition } from "@twilio-paste/animation-library" ;
2- import { Box , safelySpreadBoxProps } from "@twilio-paste/box" ;
2+ import { Box , getCustomElementStyles , safelySpreadBoxProps } from "@twilio-paste/box" ;
33import type { BoxProps } from "@twilio-paste/box" ;
4+ import { ModalDialogPrimitiveContent , ModalDialogPrimitiveOverlay } from "@twilio-paste/modal-dialog-primitive" ;
5+ import { css , styled } from "@twilio-paste/styling-library" ;
6+ import { pasteBaseStyles , useTheme } from "@twilio-paste/theme" ;
47import { useMergeRefs , useWindowSize } from "@twilio-paste/utils" ;
58import * as React from "react" ;
69
710import { SidePanelContext } from "./SidePanelContext" ;
811import type { SidePanelProps } from "./types" ;
912
13+ const SidePanelMobileOverlay = animated (
14+ // @ts -expect-error the styled div color prop from emotion is clashing with our color style prop in BoxProps
15+ styled ( ModalDialogPrimitiveOverlay ) (
16+ css ( {
17+ backgroundColor : "colorBackgroundOverlay" ,
18+ position : "fixed" ,
19+ top : 0 ,
20+ right : 0 ,
21+ bottom : 0 ,
22+ left : 0 ,
23+ width : "100%" ,
24+ zIndex : "zIndex80" ,
25+ } ) ,
26+ /*
27+ * import Paste Theme Based Styles due to portal positioning.
28+ * reach portal is a sibling to the main app, so you are now
29+ * no longer a child of the theme provider. We need to re-set
30+ * some of the base styles that we rely on inheriting from
31+ * such as font-family and line-height so that compositions
32+ * of paste components in the side panel are styled correctly.
33+ */
34+ pasteBaseStyles ,
35+ getCustomElementStyles ,
36+ ) ,
37+ ) ;
38+
1039const StyledSidePanelWrapper = React . forwardRef < HTMLDivElement , BoxProps > ( ( props , ref ) => (
1140 < Box
1241 { ...props }
@@ -19,6 +48,7 @@ const StyledSidePanelWrapper = React.forwardRef<HTMLDivElement, BoxProps>((props
1948 paddingRight = { [ "space0" , "space40" , "space40" ] }
2049 width = { [ "100%" , "size40" , "size40" ] }
2150 height = { props . height }
51+ boxSizing = "content-box"
2252 />
2353) ) ;
2454
@@ -31,87 +61,148 @@ const config = {
3161 friction : 20 ,
3262} ;
3363
34- const transitionStyles = {
35- from : { opacity : 0 , transform : "translateX(100%)" } ,
36- enter : { opacity : 1 , transform : "translateX(0%)" } ,
37- leave : { opacity : 0 , transform : "translateX(100%)" } ,
38- config,
39- } ;
40-
41- const mobileTransitionStyles = {
42- from : { opacity : 0 , transform : "translateY(100%)" } ,
43- enter : { opacity : 1 , transform : "translateY(0%)" } ,
44- leave : { opacity : 0 , transform : "translateY(100%)" } ,
45- config,
46- } ;
47-
48- const SidePanel = React . forwardRef < HTMLDivElement , SidePanelProps > (
49- ( { element = "SIDE_PANEL" , label, children, ...props } , ref ) => {
50- const { sidePanelId, isOpen } = React . useContext ( SidePanelContext ) ;
51-
52- const { breakpointIndex } = useWindowSize ( ) ;
53-
54- const transitions =
55- breakpointIndex === 0 ? useTransition ( isOpen , mobileTransitionStyles ) : useTransition ( isOpen , transitionStyles ) ;
56-
57- const screenSize = window . innerHeight ;
64+ interface SidePanelContentsProps extends SidePanelProps {
65+ sidePanelId : string ;
66+ styles : any ;
67+ isMobile : boolean ;
68+ }
5869
70+ const SidePanelContents = React . forwardRef < HTMLDivElement , SidePanelContentsProps > (
71+ ( { label, element, sidePanelId, styles, isMobile, children, ...props } , ref ) => {
72+ // Get the offset of the side panel from the top of the viewport
5973 const sidePanelRef = React . useRef < HTMLDivElement > ( null ) ;
6074 const mergedSidePanelRef = useMergeRefs ( sidePanelRef , ref ) as React . RefObject < HTMLDivElement > ;
61-
75+ const screenSize = window . innerHeight ;
6276 const [ offsetY , setOffsetY ] = React . useState ( 0 ) ;
63-
64- // Get the offset of the side panel from the top of the viewport
6577 React . useEffect ( ( ) => {
6678 const boundingClientRect = sidePanelRef ?. current ?. getBoundingClientRect ( ) ;
6779 setOffsetY ( boundingClientRect ?. y || 0 ) ;
6880 } , [ ] ) ;
6981
82+ return (
83+ < Box
84+ { ...safelySpreadBoxProps ( props ) }
85+ position = "absolute"
86+ role = "dialog"
87+ as = { isMobile ? ( ModalDialogPrimitiveContent as any ) : "div" }
88+ aria-label = { label }
89+ top = { 0 }
90+ right = { 0 }
91+ width = { [ "100%" , "auto" , "auto" ] }
92+ height = "100%"
93+ element = { element }
94+ id = { sidePanelId }
95+ >
96+ < AnimatedStyledSidePanelWrapper
97+ ref = { mergedSidePanelRef }
98+ element = { `ANIMATED_${ element } _WRAPPER` }
99+ style = { styles }
100+ height = { [ "100%" , screenSize - offsetY , screenSize - offsetY ] }
101+ top = { [ 0 , offsetY , offsetY ] }
102+ position = "relative"
103+ overflow = "hidden"
104+ >
105+ < Box
106+ display = "flex"
107+ flexDirection = "column"
108+ width = { [ "100%" , "388px" , "388px" ] } // 400px - 12px
109+ position = "absolute"
110+ top = { 0 }
111+ left = { [ 0 , 12 , 12 ] }
112+ bottom = { 0 }
113+ borderStyle = "solid"
114+ borderBottomLeftRadius = { [ "borderRadius0" , "borderRadius70" , "borderRadius70" ] }
115+ borderBottomRightRadius = { [ "borderRadius0" , "borderRadius70" , "borderRadius70" ] }
116+ borderTopRightRadius = { [ "borderRadius60" , "borderRadius70" , "borderRadius70" ] }
117+ borderTopLeftRadius = { [ "borderRadius60" , "borderRadius70" , "borderRadius70" ] }
118+ borderWidth = "borderWidth10"
119+ borderColor = "colorBorderWeaker"
120+ backgroundColor = "colorBackgroundBody"
121+ marginTop = { [ "space100" , "space40" , "space40" ] }
122+ marginBottom = { [ "space0" , "space40" , "space40" ] }
123+ paddingBottom = "space70"
124+ element = { `INNER_${ element } ` }
125+ >
126+ { children }
127+ </ Box >
128+ </ AnimatedStyledSidePanelWrapper >
129+ </ Box >
130+ ) ;
131+ } ,
132+ ) ;
133+ SidePanelContents . displayName = "SidePanelContents" ;
134+
135+ const SidePanel = React . forwardRef < HTMLDivElement , SidePanelProps > (
136+ ( { element = "SIDE_PANEL" , label, children, ...props } , ref ) => {
137+ const theme = useTheme ( ) ;
138+ const { sidePanelId, isOpen } = React . useContext ( SidePanelContext ) ;
139+ // Determine whether this is the initial render in order to block enter animations
140+ const [ isFirstRender , setIsFirstRender ] = React . useState ( true ) ;
141+ React . useEffect ( ( ) => {
142+ if ( isFirstRender ) {
143+ setIsFirstRender ( false ) ;
144+ }
145+ } , [ isFirstRender ] ) ;
146+
147+ // Define transition styles for both breakpoints
148+ const transitionStyles = {
149+ from : isFirstRender ? undefined : { opacity : 0 , width : "0px" } ,
150+ enter : { opacity : 1 , width : "400px" } ,
151+ leave : { opacity : 0 , width : "0px" } ,
152+ config,
153+ } ;
154+ const mobileTransitionStyles = {
155+ from : isFirstRender ? undefined : { opacity : 0 , transform : "translateY(100%)" } ,
156+ enter : { opacity : 1 , transform : "translateY(0%)" } ,
157+ leave : { opacity : 0 , transform : "translateY(100%)" } ,
158+ config,
159+ } ;
160+
161+ // Set mobile or desktop transitions based on breakpointIndex
162+ const { breakpointIndex } = useWindowSize ( ) ;
163+ const desktopTransitions = useTransition ( isOpen , transitionStyles ) ;
164+ const mobileTransitions = useTransition ( isOpen , mobileTransitionStyles ) ;
165+ const transitions = React . useMemo ( ( ) => {
166+ if ( breakpointIndex === 0 ) return mobileTransitions ;
167+ return desktopTransitions ;
168+ } , [ breakpointIndex , desktopTransitions , mobileTransitions ] ) ;
169+
70170 return (
71171 < >
72172 { transitions (
73173 ( styles , item ) =>
74- item && (
75- < Box
76- { ...safelySpreadBoxProps ( props ) } // moved this from animated wrapper... might cause something
77- position = "absolute"
78- role = "dialog"
79- aria-label = { label }
80- top = { 0 }
81- right = { 0 }
82- width = { [ "100%" , "auto" , "auto" ] }
83- height = "100%"
84- element = { element }
85- id = { sidePanelId }
174+ item &&
175+ ( breakpointIndex === 0 ? (
176+ < SidePanelMobileOverlay
177+ theme = { theme }
178+ data-paste-element = { `${ element } _OVERLAY` }
179+ style = { { opacity : styles . opacity } }
86180 >
87- < AnimatedStyledSidePanelWrapper
88- ref = { mergedSidePanelRef }
89- element = { `ANIMATED_${ element } _WRAPPER` }
90- style = { styles }
91- height = { screenSize - offsetY }
92- top = { offsetY }
181+ < SidePanelContents
182+ { ...props }
183+ element = { element }
184+ sidePanelId = { sidePanelId }
185+ styles = { styles }
186+ label = { label }
187+ isMobile
188+ ref = { ref }
93189 >
94- < Box
95- display = "flex"
96- maxHeight = "100%"
97- flexDirection = "column"
98- width = { [ "100%" , "size40" , "size40" ] }
99- borderStyle = "solid"
100- borderRadius = { [ "borderRadius0" , "borderRadius70" , "borderRadius70" ] }
101- borderWidth = "borderWidth10"
102- borderColor = "colorBorderWeaker"
103- backgroundColor = "colorBackgroundBody"
104- marginTop = "space40"
105- marginBottom = { [ "space0" , "space40" , "space40" ] }
106- paddingBottom = "space70"
107- overflowY = "hidden"
108- element = { `INNER_${ element } ` }
109- >
110- { children }
111- </ Box >
112- </ AnimatedStyledSidePanelWrapper >
113- </ Box >
114- ) ,
190+ { children }
191+ </ SidePanelContents >
192+ </ SidePanelMobileOverlay >
193+ ) : (
194+ < SidePanelContents
195+ { ...props }
196+ element = { element }
197+ sidePanelId = { sidePanelId }
198+ styles = { styles }
199+ label = { label }
200+ isMobile = { false }
201+ ref = { ref }
202+ >
203+ { children }
204+ </ SidePanelContents >
205+ ) ) ,
115206 ) }
116207 </ >
117208 ) ;
0 commit comments