1
1
import { animated , useTransition } from "@twilio-paste/animation-library" ;
2
- import { Box , safelySpreadBoxProps } from "@twilio-paste/box" ;
2
+ import { Box , getCustomElementStyles , safelySpreadBoxProps } from "@twilio-paste/box" ;
3
3
import 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" ;
4
7
import { useMergeRefs , useWindowSize } from "@twilio-paste/utils" ;
5
8
import * as React from "react" ;
6
9
7
10
import { SidePanelContext } from "./SidePanelContext" ;
8
11
import type { SidePanelProps } from "./types" ;
9
12
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
+
10
39
const StyledSidePanelWrapper = React . forwardRef < HTMLDivElement , BoxProps > ( ( props , ref ) => (
11
40
< Box
12
41
{ ...props }
@@ -19,6 +48,7 @@ const StyledSidePanelWrapper = React.forwardRef<HTMLDivElement, BoxProps>((props
19
48
paddingRight = { [ "space0" , "space40" , "space40" ] }
20
49
width = { [ "100%" , "size40" , "size40" ] }
21
50
height = { props . height }
51
+ boxSizing = "content-box"
22
52
/>
23
53
) ) ;
24
54
@@ -31,87 +61,148 @@ const config = {
31
61
friction : 20 ,
32
62
} ;
33
63
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
+ }
58
69
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
59
73
const sidePanelRef = React . useRef < HTMLDivElement > ( null ) ;
60
74
const mergedSidePanelRef = useMergeRefs ( sidePanelRef , ref ) as React . RefObject < HTMLDivElement > ;
61
-
75
+ const screenSize = window . innerHeight ;
62
76
const [ offsetY , setOffsetY ] = React . useState ( 0 ) ;
63
-
64
- // Get the offset of the side panel from the top of the viewport
65
77
React . useEffect ( ( ) => {
66
78
const boundingClientRect = sidePanelRef ?. current ?. getBoundingClientRect ( ) ;
67
79
setOffsetY ( boundingClientRect ?. y || 0 ) ;
68
80
} , [ ] ) ;
69
81
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
+
70
170
return (
71
171
< >
72
172
{ transitions (
73
173
( 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 } }
86
180
>
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 }
93
189
>
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
+ ) ) ,
115
206
) }
116
207
</ >
117
208
) ;
0 commit comments