1
1
import * as React from 'react' ;
2
- import { Platform , StyleSheet , View } from 'react-native' ;
2
+ import { Dimensions , Platform , StyleSheet , View } from 'react-native' ;
3
3
import Surface from '../Surface' ;
4
4
import TouchableRipple from '../TouchableRipple/TouchableRipple' ;
5
5
import IconButton from '../IconButton' ;
@@ -9,8 +9,16 @@ import DropdownContent from './DropdownContent';
9
9
import HelperText from '../HelperText' ;
10
10
import * as List from '../List/List' ;
11
11
import Portal from '../Portal/Portal' ;
12
+ import { APPROX_STATUSBAR_HEIGHT } from '../../constants' ;
12
13
13
14
type Props < T > = {
15
+ /**
16
+ * Extra padding to add at the top of header to account for translucent status bar.
17
+ * This is automatically handled on iOS >= 11 including iPhone X using `SafeAreaView`.
18
+ * If you are using Expo, we assume translucent status bar and set a height for status bar automatically.
19
+ * Pass `0` or a custom value to disable the default behaviour, and customize the height.
20
+ */
21
+ statusBarHeight ?: number ;
14
22
/**
15
23
* Placeholder label when there is no selected value
16
24
*/
@@ -49,6 +57,11 @@ type Props<T> = {
49
57
style : { color : string } ;
50
58
key : React . Key ;
51
59
} ) => React . ReactNode ;
60
+ /**
61
+ * A dropdown can be either be rendered inside a modal or inside a floating menu
62
+ * The default value is 'floating' if the platform is web and 'modal' otherwise
63
+ */
64
+ mode ?: 'modal' | 'floating' ;
52
65
} ;
53
66
54
67
type OptionProps < T > = {
@@ -60,19 +73,22 @@ type OptionProps<T> = {
60
73
const DEFAULT_EMPTY_DROPDOWN_LABEL = 'No available options' ;
61
74
const DEFAULT_PLACEHOLDER_LABEL = 'Select an option' ;
62
75
const DROPDOWN_NULL_OPTION_KEY = 'DROPDOWN_NULL_OPTION_KEY' ;
76
+ const DEFAULT_MAX_HEIGHT = 350 ;
63
77
64
78
export type DropdownContextProps < T > = {
65
79
closeMenu ( ) : void ;
66
80
selectOption ( option : OptionProps < T > | null ) : void ;
67
- maxHeight ?: number ;
68
81
required : boolean ;
69
82
emptyDropdownLabel : string ;
70
83
selectedValue ?: T ;
71
84
dropdownCoordinates : {
72
- top : number ;
85
+ top ?: number ;
86
+ bottom ?: number ;
73
87
left : number ;
74
88
width : number ;
89
+ maxHeight ?: number ;
75
90
} ;
91
+ mode : 'modal' | 'floating' ;
76
92
} ;
77
93
78
94
export type DropdownRefAttributes < T > = {
@@ -140,27 +156,40 @@ const Dropdown = React.forwardRef(function <T>(
140
156
{
141
157
children,
142
158
emptyDropdownLabel = DEFAULT_EMPTY_DROPDOWN_LABEL ,
143
- maxHeight,
159
+ maxHeight = DEFAULT_MAX_HEIGHT ,
144
160
onSelect,
145
161
placeholder = DEFAULT_PLACEHOLDER_LABEL ,
146
162
renderNoneOption,
147
163
required = false ,
148
164
selectedValue,
165
+ mode = Platform . select ( { web : 'floating' , default : 'modal' } ) ,
166
+ statusBarHeight = APPROX_STATUSBAR_HEIGHT ,
149
167
} : Props < T > ,
150
168
ref :
151
169
| ( ( instance : DropdownRefAttributes < T > | null ) => void )
152
170
| React . MutableRefObject < DropdownRefAttributes < T > | null >
153
171
| null
154
172
) {
173
+ const computedStatusBarHeight = Platform . select ( {
174
+ android : statusBarHeight ,
175
+ default : 0 ,
176
+ } ) ;
155
177
const theme = useTheme ( ) ;
156
- const rootViewRef = React . useRef < View > ( null ) ;
178
+ const anchorRef = React . useRef < View > ( null ) ;
179
+ const menuRef = React . useRef < View > ( null ) ;
157
180
const [ isMenuOpen , setMenuOpen ] = React . useState ( false ) ;
158
- const [ dropdownCoordinates , setCoordinates ] = React . useState ( {
159
- top : 0 ,
181
+ const [ dropdownCoordinates , setMenuPosition ] = React . useState < {
182
+ top ?: number ;
183
+ bottom ?: number ;
184
+ left : number ;
185
+ width : number ;
186
+ maxHeight ?: number ;
187
+ } > ( {
160
188
left : 0 ,
161
189
width : 0 ,
162
190
} ) ;
163
191
const [ selected , setSelected ] = React . useState < OptionProps < T > | null > ( null ) ;
192
+ const windowHeight = Dimensions . get ( 'window' ) . height ;
164
193
165
194
const toggleMenuOpen = ( ) => {
166
195
if ( isMenuOpen ) {
@@ -173,16 +202,19 @@ const Dropdown = React.forwardRef(function <T>(
173
202
const closeMenu = ( ) => setMenuOpen ( false ) ;
174
203
175
204
const openMenu = ( ) => {
176
- if ( Platform . OS === 'web' ) {
177
- rootViewRef . current ?. measure ( ( _x , _y , width , height , pageX , pageY ) =>
178
- setCoordinates ( {
205
+ if ( mode === 'floating' ) {
206
+ anchorRef . current ?. measureInWindow ( ( x , y , width , height ) => {
207
+ let top = y + height + computedStatusBarHeight ;
208
+ setMenuPosition ( {
209
+ left : x ,
179
210
width : width ,
180
- left : pageX ,
181
- top : pageY + height ,
182
- } )
183
- ) ;
211
+ top : top ,
212
+ bottom : 48 ,
213
+ maxHeight : Math . min ( maxHeight , windowHeight - top ) ,
214
+ } ) ;
215
+ } ) ;
184
216
}
185
- setMenuOpen ( true ) ;
217
+ requestAnimationFrame ( ( ) => setMenuOpen ( true ) ) ;
186
218
} ;
187
219
188
220
const renderLabel = ( ) => {
@@ -258,7 +290,7 @@ const Dropdown = React.forwardRef(function <T>(
258
290
} ) ) ;
259
291
260
292
return (
261
- < View ref = { rootViewRef } >
293
+ < View ref = { anchorRef } collapsable = { false } >
262
294
< TouchableRipple borderless onPress = { toggleMenuOpen } >
263
295
< Surface
264
296
style = { [
@@ -274,28 +306,28 @@ const Dropdown = React.forwardRef(function <T>(
274
306
< View style = { styles . label } > { renderLabel ( ) } </ View >
275
307
< IconButton
276
308
style = { styles . icon }
277
- icon = " menu-down"
309
+ icon = { isMenuOpen && mode === 'floating' ? ' menu-up' : 'menu- down' }
278
310
onPress = { toggleMenuOpen }
279
311
/>
280
312
</ Surface >
281
313
</ TouchableRipple >
282
- { isMenuOpen && (
283
- < Portal >
284
- < DropdownContext . Provider
285
- value = { {
286
- selectedValue : selected ?. value ,
287
- maxHeight ,
288
- dropdownCoordinates ,
289
- emptyDropdownLabel ,
290
- required ,
291
- closeMenu ,
292
- selectOption ,
293
- } }
294
- >
295
- < DropdownContent > { renderOptions ( ) } </ DropdownContent >
296
- </ DropdownContext . Provider >
297
- </ Portal >
298
- ) }
314
+ < Portal >
315
+ < DropdownContext . Provider
316
+ value = { {
317
+ selectedValue : selected ?. value ,
318
+ dropdownCoordinates ,
319
+ emptyDropdownLabel ,
320
+ required ,
321
+ closeMenu ,
322
+ selectOption ,
323
+ mode ,
324
+ } }
325
+ >
326
+ < DropdownContent visible = { isMenuOpen } >
327
+ < View ref = { menuRef } > { renderOptions ( ) } </ View >
328
+ </ DropdownContent >
329
+ </ DropdownContext . Provider >
330
+ </ Portal >
299
331
</ View >
300
332
) ;
301
333
} ) as ( < T = any > (
@@ -306,7 +338,7 @@ const Dropdown = React.forwardRef(function <T>(
306
338
const styles = StyleSheet . create ( {
307
339
container : {
308
340
flexDirection : 'row' ,
309
- borderWidth : 1 ,
341
+ borderWidth : 1.2 ,
310
342
alignItems : 'center' ,
311
343
elevation : 1 ,
312
344
} ,
0 commit comments