1
- import styled from '@emotion/native' ;
2
1
import { StoryIndex } from '@storybook/client-api' ;
2
+ import styled from '@emotion/native' ;
3
+ import { useTheme } from 'emotion-theming' ;
3
4
import React , { useState , useRef } from 'react' ;
4
5
import {
5
6
Animated ,
6
7
Dimensions ,
7
- FlexStyle ,
8
+ Easing ,
8
9
Keyboard ,
9
10
KeyboardAvoidingView ,
10
11
Platform ,
11
- SafeAreaView ,
12
12
TouchableOpacity ,
13
13
StatusBar ,
14
14
StyleSheet ,
15
15
View ,
16
+ ViewStyle ,
17
+ StyleProp ,
16
18
} from 'react-native' ;
17
19
import { useStoryContextParam } from '../../../hooks' ;
18
20
import StoryListView from '../StoryListView' ;
@@ -25,22 +27,23 @@ import Addons from './addons/Addons';
25
27
import {
26
28
getAddonPanelPosition ,
27
29
getNavigatorPanelPosition ,
28
- getPreviewPosition ,
29
- getPreviewScale ,
30
+ getPreviewShadowStyle ,
31
+ getPreviewStyle ,
30
32
} from './animation' ;
31
33
import Navigation from './navigation' ;
32
34
import { PREVIEW , ADDONS } from './navigation/constants' ;
33
35
import Panel from './Panel' ;
34
36
import { useWindowDimensions } from 'react-native' ;
35
37
import { useSafeAreaInsets } from 'react-native-safe-area-context' ;
36
38
37
- const ANIMATION_DURATION = 300 ;
39
+ const ANIMATION_DURATION = 400 ;
38
40
const IS_IOS = Platform . OS === 'ios' ;
39
41
// @ts -ignore: Property 'Expo' does not exist on type 'Global'
40
42
const getExpoRoot = ( ) => global . Expo || global . __expo || global . __exponent ;
41
43
export const IS_EXPO = getExpoRoot ( ) !== undefined ;
42
44
const IS_ANDROID = Platform . OS === 'android' ;
43
45
const BREAKPOINT = 1024 ;
46
+
44
47
interface OnDeviceUIProps {
45
48
storyIndex : StoryIndex ;
46
49
url ?: string ;
@@ -52,28 +55,38 @@ interface OnDeviceUIProps {
52
55
53
56
const flex = { flex : 1 } ;
54
57
55
- const Preview = styled . View < { disabled : boolean } > ( flex , ( { disabled, theme } ) => ( {
56
- borderLeftWidth : disabled ? 0 : 1 ,
57
- borderTopWidth : disabled ? 0 : 1 ,
58
- borderRightWidth : disabled ? 0 : 1 ,
59
- borderBottomWidth : disabled ? 0 : 1 ,
60
- borderColor : disabled ? 'transparent' : theme . previewBorderColor ,
61
- borderRadius : disabled ? 0 : 12 ,
62
- overflow : 'hidden' ,
63
- } ) ) ;
58
+ interface PreviewProps {
59
+ animatedValue : Animated . Value ;
60
+ style : StyleProp < ViewStyle > ;
61
+ children ?: React . ReactNode ;
62
+ }
64
63
65
- const absolutePosition : FlexStyle = {
66
- position : 'absolute' ,
67
- top : 0 ,
68
- bottom : 0 ,
69
- left : 0 ,
70
- right : 0 ,
71
- } ;
64
+ /**
65
+ * Story preview container.
66
+ */
67
+ function Preview ( { animatedValue, style, children } : PreviewProps ) {
68
+ const theme : any = useTheme ( ) ;
69
+ const containerStyle = {
70
+ backgroundColor : theme . backgroundColor ,
71
+ ...getPreviewShadowStyle ( animatedValue ) ,
72
+ } ;
73
+ return (
74
+ < Animated . View style = { [ flex , containerStyle ] } >
75
+ < View style = { [ flex , style ] } > { children } </ View >
76
+ </ Animated . View >
77
+ ) ;
78
+ }
72
79
73
80
const styles = StyleSheet . create ( {
74
81
expoAndroidContainer : { paddingTop : StatusBar . currentHeight } ,
75
82
} ) ;
76
83
84
+ const Container = styled . View ( ( { theme } ) => ( {
85
+ flex : 1 ,
86
+ backgroundColor : theme . backgroundColor ,
87
+ ...( IS_ANDROID && IS_EXPO ? styles . expoAndroidContainer : undefined ) ,
88
+ } ) ) ;
89
+
77
90
const OnDeviceUI = ( {
78
91
storyIndex,
79
92
isUIHidden,
@@ -82,55 +95,80 @@ const OnDeviceUI = ({
82
95
tabOpen : initialTabOpen ,
83
96
} : OnDeviceUIProps ) => {
84
97
const [ tabOpen , setTabOpen ] = useState ( initialTabOpen || PREVIEW ) ;
85
- const [ slideBetweenAnimation , setSlideBetweenAnimation ] = useState ( false ) ;
98
+ const lastTabOpen = React . useRef ( tabOpen ) ;
86
99
const [ previewDimensions , setPreviewDimensions ] = useState < PreviewDimens > ( ( ) => ( {
87
100
width : Dimensions . get ( 'window' ) . width ,
88
101
height : Dimensions . get ( 'window' ) . height ,
89
102
} ) ) ;
90
103
const animatedValue = useRef ( new Animated . Value ( tabOpen ) ) ;
91
104
const wide = useWindowDimensions ( ) . width >= BREAKPOINT ;
92
105
const insets = useSafeAreaInsets ( ) ;
106
+ const theme : any = useTheme ( ) ;
93
107
const [ isUIVisible , setIsUIVisible ] = useState ( isUIHidden !== undefined ? ! isUIHidden : true ) ;
94
108
95
- const handleToggleTab = React . useCallback ( ( newTabOpen : number ) => {
96
- if ( newTabOpen === tabOpen ) {
97
- return ;
98
- }
99
- Animated . timing ( animatedValue . current , {
100
- toValue : newTabOpen ,
101
- duration : ANIMATION_DURATION ,
102
- useNativeDriver : true ,
103
- } ) . start ( ) ;
104
- setTabOpen ( newTabOpen ) ;
105
- const isSwipingBetweenNavigatorAndAddons = tabOpen + newTabOpen === PREVIEW ;
106
- setSlideBetweenAnimation ( isSwipingBetweenNavigatorAndAddons ) ;
109
+ const handleToggleTab = React . useCallback (
110
+ ( newTabOpen : number ) => {
111
+ if ( newTabOpen === tabOpen ) {
112
+ return ;
113
+ }
114
+ lastTabOpen . current = tabOpen ;
115
+ Animated . timing ( animatedValue . current , {
116
+ toValue : newTabOpen ,
117
+ duration : ANIMATION_DURATION ,
118
+ easing : Easing . inOut ( Easing . cubic ) ,
119
+ useNativeDriver : true ,
120
+ } ) . start ( ) ;
121
+ setTabOpen ( newTabOpen ) ;
107
122
108
- // close the keyboard opened from a TextInput from story list or knobs
109
- if ( newTabOpen === PREVIEW ) {
110
- Keyboard . dismiss ( ) ;
111
- }
112
- } , [ tabOpen ] ) ;
123
+ // close the keyboard opened from a TextInput from story list or knobs
124
+ if ( newTabOpen === PREVIEW ) {
125
+ Keyboard . dismiss ( ) ;
126
+ }
127
+ } ,
128
+ [ tabOpen ]
129
+ ) ;
113
130
114
131
const noSafeArea = useStoryContextParam < boolean > ( 'noSafeArea' , false ) ;
115
132
const previewWrapperStyles = [
116
133
flex ,
117
- getPreviewPosition ( {
134
+ getPreviewStyle ( {
118
135
animatedValue : animatedValue . current ,
119
136
previewDimensions,
120
- slideBetweenAnimation,
121
137
wide,
122
- noSafeArea,
123
138
insets,
139
+ tabOpen,
140
+ lastTabOpen : lastTabOpen . current ,
124
141
} ) ,
125
142
] ;
126
143
127
- const previewStyles = [ flex , getPreviewScale ( animatedValue . current , slideBetweenAnimation , wide ) ] ;
144
+ // The initial value is just a guess until the layout calculation has been done.
145
+ const [ navBarHeight , setNavBarHeight ] = React . useState ( insets . bottom + 40 ) ;
146
+ const measureNavigation = React . useCallback (
147
+ ( { nativeEvent } ) => {
148
+ const inset = insets . bottom ;
149
+ setNavBarHeight ( isUIVisible ? nativeEvent . layout . height - inset : 0 ) ;
150
+ } ,
151
+ [ isUIVisible , insets ]
152
+ ) ;
128
153
129
- const WrapperView = noSafeArea ? View : SafeAreaView ;
130
- const wrapperMargin = { marginBottom : isUIVisible ? insets . bottom + 40 : 0 } ;
154
+ // There are 4 cases for the additional UI margin:
155
+ // 1. Storybook UI is visible, and `noSafeArea` is false: Include top and
156
+ // bottom safe area insets, and also include the navigation bar height.
157
+ //
158
+ // 2. Storybook UI is not visible, and `noSafeArea` is false: Include top
159
+ // and bottom safe area insets.
160
+ //
161
+ // 3. Storybook UI is visible, and `noSafeArea` is true: Include only the
162
+ // bottom safe area inset and the navigation bar height.
163
+ //
164
+ // 4. Storybook UI is not visible, and `noSafeArea` is true: No margin.
165
+ const safeAreaMargins = {
166
+ paddingBottom : isUIVisible ? insets . bottom + navBarHeight : noSafeArea ? 0 : insets . bottom ,
167
+ paddingTop : ! noSafeArea ? insets . top : 0 ,
168
+ } ;
131
169
return (
132
170
< >
133
- < View style = { [ flex , IS_ANDROID && IS_EXPO && styles . expoAndroidContainer ] } >
171
+ < Container >
134
172
< KeyboardAvoidingView
135
173
enabled = { ! shouldDisableKeyboardAvoidingView || tabOpen !== PREVIEW }
136
174
behavior = { IS_IOS ? 'padding' : null }
@@ -142,47 +180,44 @@ const OnDeviceUI = ({
142
180
previewDimensions = { previewDimensions }
143
181
>
144
182
< Animated . View style = { previewWrapperStyles } >
145
- < Animated . View style = { previewStyles } >
146
- < Preview disabled = { tabOpen === PREVIEW } >
147
- < WrapperView style = { [ flex , wrapperMargin ] } >
148
- < StoryView />
149
- </ WrapperView >
150
- </ Preview >
151
- { tabOpen !== PREVIEW ? (
152
- < TouchableOpacity
153
- style = { absolutePosition }
154
- onPress = { ( ) => handleToggleTab ( PREVIEW ) }
155
- />
156
- ) : null }
157
- </ Animated . View >
183
+ < Preview style = { safeAreaMargins } animatedValue = { animatedValue . current } >
184
+ < StoryView />
185
+ </ Preview >
186
+ { tabOpen !== PREVIEW ? (
187
+ < TouchableOpacity
188
+ style = { StyleSheet . absoluteFillObject }
189
+ onPress = { ( ) => handleToggleTab ( PREVIEW ) }
190
+ />
191
+ ) : null }
158
192
</ Animated . View >
159
193
< Panel
160
- style = { getNavigatorPanelPosition (
161
- animatedValue . current ,
162
- previewDimensions . width ,
163
- wide
164
- ) }
194
+ style = { [
195
+ getNavigatorPanelPosition ( animatedValue . current , previewDimensions . width , wide ) ,
196
+ safeAreaMargins ,
197
+ { backgroundColor : theme . storyListBackgroundColor } ,
198
+ ] }
165
199
>
166
200
< StoryListView storyIndex = { storyIndex } />
167
201
</ Panel >
168
202
169
203
< Panel
170
204
style = { [
171
205
getAddonPanelPosition ( animatedValue . current , previewDimensions . width , wide ) ,
172
- wrapperMargin ,
206
+ safeAreaMargins ,
173
207
] }
174
208
>
175
209
< Addons active = { tabOpen === ADDONS } />
176
210
</ Panel >
177
211
</ AbsolutePositionedKeyboardAwareView >
178
212
</ KeyboardAvoidingView >
179
213
< Navigation
214
+ onLayout = { measureNavigation }
180
215
tabOpen = { tabOpen }
181
216
onChangeTab = { handleToggleTab }
182
217
isUIVisible = { isUIVisible }
183
218
setIsUIVisible = { setIsUIVisible }
184
219
/>
185
- </ View >
220
+ </ Container >
186
221
</ >
187
222
) ;
188
223
} ;
0 commit comments