-
-
Notifications
You must be signed in to change notification settings - Fork 2.1k
feat: added dropdown component #4102
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
import React, { useState } from 'react'; | ||
import { ScrollView, StyleSheet, View } from 'react-native'; | ||
|
||
import { Dropdown, List, TextInput, useTheme } from 'react-native-paper'; | ||
|
||
const options = [ | ||
{ | ||
id: '1', | ||
icon: 'dice-1', | ||
title: 'Dice 1', | ||
}, | ||
{ | ||
id: '2', | ||
icon: 'dice-2', | ||
title: 'Dice 2', | ||
}, | ||
{ | ||
id: '3', | ||
icon: 'dice-3', | ||
title: 'Dice 3', | ||
}, | ||
{ | ||
id: '4', | ||
icon: 'dice-4', | ||
title: 'Dice 4', | ||
}, | ||
{ | ||
id: '5', | ||
icon: 'dice-5', | ||
title: 'Dice 5', | ||
}, | ||
{ | ||
id: '6', | ||
icon: 'dice-6', | ||
title: 'Dice 6', | ||
}, | ||
]; | ||
|
||
const DropdownExample = () => { | ||
const theme = useTheme(); | ||
|
||
const [value, setValue] = useState<string | null>(null); | ||
|
||
return ( | ||
<ScrollView style={{ backgroundColor: theme.colors.background }}> | ||
<List.Section title="Uncontrolled dropdown"> | ||
<View style={styles.dropdown}> | ||
<Dropdown onChange={console.log} placeholder="Select an Option"> | ||
{options.map(({ id, title }) => ( | ||
<Dropdown.Item key={id} title={title} value={title} /> | ||
))} | ||
</Dropdown> | ||
</View> | ||
</List.Section> | ||
<List.Section title="Uncontrolled dropdown with icon"> | ||
<View style={styles.dropdown}> | ||
<Dropdown | ||
onChange={console.log} | ||
placeholder="Select an Option" | ||
left={<TextInput.Icon icon="magnify" />} | ||
> | ||
{options.map(({ id, title }) => ( | ||
<Dropdown.Item key={id} title={title} value={title} /> | ||
))} | ||
</Dropdown> | ||
</View> | ||
</List.Section> | ||
<List.Section title="Uncontrolled dropdown with required value"> | ||
<View style={styles.dropdown}> | ||
<Dropdown | ||
onChange={console.log} | ||
placeholder="Select an Option" | ||
defaultValue="Dice 1" | ||
required | ||
> | ||
{options.map(({ id, title }) => ( | ||
<Dropdown.Item key={id} title={title} value={title} /> | ||
))} | ||
</Dropdown> | ||
</View> | ||
</List.Section> | ||
<List.Section title="Controlled Dropdown"> | ||
<View style={styles.dropdown}> | ||
<Dropdown | ||
onChange={setValue} | ||
placeholder="Select an Option" | ||
value={value} | ||
valueText={options.filter(({ id }) => id === value)[0]?.title} | ||
> | ||
{options.map(({ id, title }) => ( | ||
<Dropdown.Item key={id} title={title} value={id} /> | ||
))} | ||
</Dropdown> | ||
</View> | ||
</List.Section> | ||
<List.Section title="Customized dropdown"> | ||
<View style={styles.dropdown}> | ||
<Dropdown | ||
mode="outlined" | ||
onChange={setValue} | ||
placeholder="Select an Option" | ||
value={value} | ||
valueText={options.filter(({ id }) => id === value)[0]?.title} | ||
left={ | ||
<TextInput.Icon | ||
icon={ | ||
options.filter(({ id }) => id === value)[0]?.icon ?? 'magnify' | ||
} | ||
/> | ||
} | ||
> | ||
{options.map(({ id, title, icon }) => ( | ||
<Dropdown.Item | ||
key={id} | ||
title={title} | ||
value={id} | ||
leadingIcon={icon} | ||
/> | ||
))} | ||
</Dropdown> | ||
</View> | ||
</List.Section> | ||
</ScrollView> | ||
); | ||
}; | ||
|
||
DropdownExample.title = 'Dropdown'; | ||
|
||
const styles = StyleSheet.create({ | ||
dropdown: { | ||
paddingHorizontal: 8, | ||
}, | ||
}); | ||
|
||
export default DropdownExample; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
import React, { ReactElement, useEffect, useState } from 'react'; | ||
import { View } from 'react-native'; | ||
|
||
import DropdownContext from './DropdownContext'; | ||
import { Props as DropdownItemProps } from './DropdownItem'; | ||
import Menu from '../Menu/Menu'; | ||
import TextInput, { Props as TextInputProps } from '../TextInput/TextInput'; | ||
import TouchableRipple from '../TouchableRipple/TouchableRipple'; | ||
|
||
export interface Props extends Omit<TextInputProps, 'value' | 'onChange'> { | ||
/** | ||
* List of underlying dropdown options. | ||
*/ | ||
children?: | ||
| ReactElement<DropdownItemProps> | ||
| Array<ReactElement<DropdownItemProps>>; | ||
/** | ||
* Callback called when the selected option changes. | ||
* @param value new selected value | ||
*/ | ||
onChange?: (value: string | null) => void; | ||
/** | ||
* Currently selected value in the dropdown. When undefined, the dropdown behaves as an uncontrolled input. | ||
*/ | ||
value?: string | null; | ||
/** | ||
* Text displayed in the underlying TextInput. When undefined, the text displayed is equal to the selected value. | ||
*/ | ||
valueText?: string; | ||
/** | ||
* Initial value for the dropdown. | ||
*/ | ||
defaultValue?: string; | ||
/** | ||
* The clear button will show by default to remove the current value. | ||
* If required is set to true, this button will not appear. | ||
*/ | ||
required?: boolean; | ||
} | ||
|
||
/** | ||
* Dropdowns present a list of options which a user can select from. | ||
* A selected option can represent a value in a form, or can be used as an action to filter or sort existing content. | ||
* | ||
* ## Usage | ||
* ```js | ||
* import * as React from 'react'; | ||
* import { Dropdown, Provider, Text, Title } from 'react-native-paper'; | ||
* | ||
* const MyComponent = () => { | ||
* const [selected, setSelected] = React.useState(null); | ||
* const options = [ | ||
* {id: 1, name: 'Cookie', calories: 502}, | ||
* {id: 2, name: 'Candy', calories: 535}, | ||
* ]; | ||
* | ||
* return ( | ||
* <Provider> | ||
* <Dropdown onChange={setSelected}> | ||
* {options.map(option => ( | ||
* <Dropdown.Item | ||
* value={option.name} | ||
* title={option.name} | ||
* key={value.id} | ||
* label={value.name} | ||
* /> | ||
* ))} | ||
* </Dropdown> | ||
* <Title>{selectedOption}</Title> | ||
* </Provider> | ||
* ); | ||
* }; | ||
* | ||
* export default MyComponent; | ||
* ``` | ||
*/ | ||
const Dropdown = ({ | ||
value: valueFromProps, | ||
valueText: valueTextFromProps, | ||
required, | ||
onChange, | ||
children, | ||
defaultValue, | ||
...textInputProps | ||
}: Props) => { | ||
const isControlled = typeof valueFromProps !== 'undefined'; | ||
|
||
const [menu, setMenu] = useState<View | null>(null); | ||
const [width, setWidth] = useState(0); | ||
const [open, setOpen] = useState(false); | ||
const [internalValue, setInternalValue] = useState<string | null>( | ||
defaultValue ?? null | ||
); | ||
|
||
useEffect(() => { | ||
menu?.measureInWindow((_x, _y, width, _height) => { | ||
setWidth(width); | ||
}); | ||
}, [open, menu]); | ||
|
||
useEffect(() => { | ||
if (typeof valueFromProps !== 'undefined') { | ||
setInternalValue(valueFromProps); | ||
} | ||
}, [valueFromProps]); | ||
|
||
const value = isControlled ? valueFromProps : internalValue; | ||
const valueText = | ||
typeof valueTextFromProps !== 'undefined' ? valueTextFromProps : value; | ||
|
||
return ( | ||
<View> | ||
<Menu | ||
anchor={ | ||
<View ref={setMenu}> | ||
<TouchableRipple onPress={() => setOpen(true)}> | ||
<TextInput | ||
editable={false} | ||
right={ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would preferer to have a prop called something like |
||
value && !required ? ( | ||
<TextInput.Icon | ||
icon="close-circle-outline" | ||
onPress={() => { | ||
onChange?.(null); | ||
setInternalValue(null); | ||
}} | ||
/> | ||
) : undefined | ||
} | ||
value={valueText ?? ''} | ||
{...textInputProps} | ||
/> | ||
</TouchableRipple> | ||
</View> | ||
} | ||
anchorPosition="bottom" | ||
contentStyle={{ width: width }} | ||
visible={open} | ||
onDismiss={() => setOpen(false)} | ||
> | ||
<DropdownContext.Provider | ||
value={{ | ||
onChange: (newValue: string) => { | ||
setInternalValue(newValue); | ||
onChange?.(newValue); | ||
setOpen(false); | ||
}, | ||
}} | ||
> | ||
{children} | ||
</DropdownContext.Provider> | ||
</Menu> | ||
</View> | ||
); | ||
}; | ||
|
||
export default Dropdown; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import { createContext } from 'react'; | ||
|
||
export interface DropdownContextData { | ||
onChange?: (value: string) => void; | ||
} | ||
|
||
const DropdownContext = createContext<DropdownContextData>({}); | ||
|
||
export default DropdownContext; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import React, { useContext } from 'react'; | ||
|
||
import DropdownContext from './DropdownContext'; | ||
import { Props as MenuItemProps } from '../Menu/MenuItem'; | ||
import MenuItem from '../Menu/MenuItem'; | ||
|
||
export interface Props extends Omit<MenuItemProps, 'onPress'> { | ||
value: string; | ||
} | ||
|
||
const DropdownItem = (props: Props) => { | ||
const dropdownContext = useContext(DropdownContext); | ||
|
||
return ( | ||
<MenuItem | ||
style={{ maxWidth: undefined }} | ||
onPress={() => dropdownContext.onChange?.(props.value)} | ||
{...props} | ||
/> | ||
); | ||
}; | ||
|
||
export default DropdownItem; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import DropdownComponent from './Dropdown'; | ||
import DropdownItem from './DropdownItem'; | ||
|
||
type DropdownExport = typeof DropdownComponent & { | ||
Item: typeof DropdownItem; | ||
}; | ||
|
||
const Dropdown: DropdownExport = Object.assign( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Isn't cleaner to write Dropdown.Item = DropdownItem; like do we need to use |
||
// @component ./Dropdown.tsx | ||
DropdownComponent, | ||
{ | ||
// @component ./DropdownItem.tsx | ||
Item: DropdownItem, | ||
} | ||
); | ||
|
||
export default Dropdown; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we have extra props for modifying UI components, like
ViewContainerProps
,MenuProps
, andTextInputProps
etc...