Skip to content

Commit 93cffb0

Browse files
committed
feat: added read me and avoidBottom
1 parent 8cf72b8 commit 93cffb0

File tree

7 files changed

+132
-31
lines changed

7 files changed

+132
-31
lines changed

README.md

Lines changed: 89 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,106 @@
1-
# rn-select
1+
# React Native Select
2+
3+
React Native Select is a select component for React Native that is compatible with both mobile and web platforms. It is entirely written in TypeScript and comes with features such as being searchable, multiselect, and creatable.
4+
5+
> Why another select component? </br>
6+
> I wanted something approachable, highly customizable and that felt just a wee bit native on all platforms. Those wants led me to create this and soon after share it with the community.
7+
8+
9+
## Demo
10+
11+
| Mobile | Web |
12+
|--------|-----|
13+
| <img src="https://github.com/peter-olom/zero/blob/a6a40a37939ce33ad76a1b68920e20fa02a32662/rn-select/rnsm.gif?raw=true" width="1200" alt="Mobile Demo" /> | ![Web Demo](https://github.com/peter-olom/zero/blob/a6a40a37939ce33ad76a1b68920e20fa02a32662/rn-select/rnsw.gif?raw=true) |
214

3-
Custom typescript only select component for react native
415

516
## Installation
617

18+
```sh
19+
yarn add rn-select
20+
```
21+
or
722
```sh
823
npm install rn-select
924
```
1025

1126
## Usage
1227

1328
```js
14-
import { multiply } from 'rn-select';
29+
import { Select } from 'rn-select';
1530

16-
// ...
17-
18-
const result = await multiply(3, 7);
31+
export function App() {
32+
const pets = [
33+
['cat', 'Cat'],
34+
['dog', 'Dog'],
35+
['horse', 'Horse'],
36+
['fish', 'Fish'],
37+
]
38+
return <Selelect options={pets} />
39+
}
1940
```
2041

42+
## Props
43+
44+
### Single Select Props
45+
| Prop | Description | Default |
46+
|----------------|---------------------------------------|---------|
47+
| value | Currently selected value `string` | None |
48+
| onChangeValue | Callback when value changes. Receives `string`| None |
49+
50+
### Multi Select Props
51+
| Prop | Description | Default |
52+
|----------------|---------------------------------------|---------|
53+
| multi | Flag indicating multiselect mode | `true` |
54+
| value | Currently selected values `Array<string>`| None |
55+
| onChangeValue | Callback when values change. Receives `Array<string>`| None |
56+
57+
58+
### Common Props
59+
| Prop | Description | Default |
60+
|-------------------------------|---------------------------------------------|---------------|
61+
| placeholder | Placeholder text when no option is selected | None |
62+
| listTitle | Title for the options list | None |
63+
| searchPlaceholder | Placeholder text for the search input | None |
64+
| searchPlaceholderTextColor | Text color for the search input placeholder | None |
65+
| showSelectionCount | Flag to show the count of selected items | `true` |
66+
| options | Array of options (`Array<[key, value]>`) | Required |
67+
| reverse | Flag to reverse option and check mark positon| `false` |
68+
| selectionEffectColor | Color for selection effect | None |
69+
| optionsScrollIndicator | Flag to show scroll indicator for options | `true` |
70+
| emptyOptionsPlaceholder | Placeholder text when options is empty | `true` |
71+
| emptySearchMsg | Message when search results are empty | "No options" |
72+
| value | Currently selected value(s) `string` or `Array<string>`| None|
73+
| clearable | Flag to enable clearing of selection | `true` |
74+
| disabled | Flag to disable the component | `false` |
75+
| searchable | Flag to enable searching | `true` |
76+
| createable | Flag to enable creating new items | `false` |
77+
| avoidBottom | Avoid options from being hidden by browser window `height` or `position` | None |
78+
| onCreateItem | Callback when a new item is created | None |
79+
| onChangeInput | Callback when input value changes | None |
80+
| renderAnchor | Custom rendering for anchor component | None |
81+
| renderSearch | Custom rendering for search component | None |
82+
| renderOption | Custom rendering for option component | None |
83+
| optionDivider | Custom component for option divider | None |
84+
| selectStyle | Styles for the select container | None |
85+
| selectPlaceholderTextStyle | Styles for the placeholder text | None |
86+
| selectTextStyle | Styles for the select text | None |
87+
| selectPillTextStyle | Styles for the select pill text | None |
88+
| selectPillRemoveContainerStyle| Styles for the select pill remove container| None |
89+
| selectPillRemoveIconStyle | Styles for the select pill remove icon | None |
90+
| selectIconStyle | Styles for the select icon (close & expand) | None |
91+
| searchContainerStyle | Styles for the search container | None |
92+
| searchInputStyle | Styles for the search input | None |
93+
| searchBackIconStyle | Styles for the search back icon | None |
94+
| searchClearIconStyle | Styles for the search clear icon | None |
95+
| statsTextStyle | Styles for the stats text | None |
96+
| optionListContainerStyle | Styles for the options list container | None |
97+
| optionListStyle | Styles for the options list | None |
98+
| optionContainerStyle | Styles for the option container | None |
99+
| optionTextStyle | Styles for the option text | None |
100+
| optionCheckColors | Colors for option checkmark | None |
101+
| emptyTextStyle | Styles for the empty text | None |
102+
103+
21104
## Contributing
22105

23106
See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the repository and the development workflow.

example/src/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export default function App() {
2020
<MultiSelect label="Multi Select" />
2121
<SingleSelect label="Single Select" optionsCount={3} />
2222
<SingleSelect label="No Options" optionsCount={0} />
23-
<MultiSelect label="Creatable Select" creatable />
23+
<MultiSelect label="Creatable Select" creatable optionsCount={5} />
2424
<MultiSelect
2525
label="Disabled Select"
2626
optionsCount={10}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
"type": "git",
4444
"url": "git+https://github.com/peter-olom/rn-select.git"
4545
},
46-
"author": "Peter Olom <ptr.olom@gmail.com> (https://github.com/peter-olom)",
46+
"author": "Peter Olom <peter@olom.dev> (https://github.com/peter-olom)",
4747
"license": "MIT",
4848
"bugs": {
4949
"url": "https://github.com/peter-olom/rn-select/issues"

src/components/Anchor.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ export default function Anchor({
6868
paddingHorizontal: tokens.size.sm - 4,
6969
},
7070
iconContainer: { flexDirection: 'row', alignItems: 'center' },
71+
fill: { flex: 1 },
7172
placeholder: { color: '#808080' },
7273
closeIconContainer: {
7374
height: tokens.size.xl + 4,
@@ -109,14 +110,14 @@ export default function Anchor({
109110
>
110111
{selected.length === 0 && (
111112
<Text
112-
style={[styles.placeholder, selectPlaceholderTextStyle]}
113+
style={[styles.placeholder, selectPlaceholderTextStyle, styles.fill]}
113114
numberOfLines={1}
114115
>
115116
{placeholder}
116117
</Text>
117118
)}
118119
{selected.length === 1 && !multi && (
119-
<Text style={[selectTextStyle]} numberOfLines={1}>
120+
<Text style={[selectTextStyle, styles.fill]} numberOfLines={1}>
120121
{selected[0]?.[1]}
121122
</Text>
122123
)}

src/components/BottomSpacer.tsx

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,18 @@
1-
import React, { useState, useEffect } from 'react';
2-
import { Keyboard, View } from 'react-native';
1+
import React from 'react';
2+
import { View } from 'react-native';
33
import useStyles from '../hooks/useStyles';
44

5-
export default function BottomSpacer() {
6-
const [height, setHeight] = useState(0);
5+
interface Props {
6+
height?: number;
7+
}
8+
export default function BottomSpacer({ height = 50 }: Props) {
79
const styles = useStyles(
810
() => ({
9-
container: {
10-
height: height,
11+
spacer: {
12+
height,
1113
},
1214
}),
1315
[height]
1416
);
15-
useEffect(() => {
16-
Keyboard.addListener('keyboardDidShow', () =>
17-
setHeight(Keyboard.metrics()?.height ?? 300)
18-
);
19-
Keyboard.addListener('keyboardDidHide', () => setHeight(0));
20-
});
21-
return <View style={[styles.container]} />;
17+
return <View style={[styles.spacer]} />;
2218
}

src/components/ListContainer.tsx

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,16 @@ import { MIN_WIDTH } from './common';
1313

1414
interface Props extends ModalProps {
1515
position?: AnchorPos;
16+
avoidBottom?: 'height' | 'position';
17+
onOptionsOffet?: (offset: number) => void;
1618
}
1719

1820
export default function ListContainer({
1921
children,
2022
style,
2123
position,
24+
avoidBottom,
25+
onOptionsOffet,
2226
...rest
2327
}: Props) {
2428
const { width, height } = useWindowDimensions();
@@ -32,11 +36,13 @@ export default function ListContainer({
3236
: undefined;
3337

3438
const top = useMemo(() => {
35-
if (height - (position?.y ?? 0) < listHeight + 80) {
36-
return height - listHeight - 80;
39+
if (Platform.OS === 'web' && avoidBottom === 'position') {
40+
if (height - (position?.y ?? 0) < listHeight + 80) {
41+
return height - listHeight - 80;
42+
}
3743
}
3844
return position?.y ?? 0;
39-
}, [height, listHeight, position?.y]);
45+
}, [height, listHeight, position?.y, avoidBottom]);
4046

4147
const styles = useStyles(
4248
({ tokens }) => ({
@@ -78,13 +84,20 @@ export default function ListContainer({
7884
[left, right, top]
7985
);
8086

87+
const handleListHeight = (listContentHeight: number) => {
88+
if (Platform.OS === 'web' && avoidBottom === 'height') {
89+
onOptionsOffet?.(height - listContentHeight - 80);
90+
}
91+
setListHeight(listContentHeight);
92+
};
93+
8194
return (
8295
<Modal {...rest}>
8396
<Pressable style={styles.backdrop} onPress={rest.onRequestClose}>
8497
<SafeAreaProvider>
8598
<ListContent
8699
style={[styles.optionsContainer, style]}
87-
onListHeight={setListHeight}
100+
onListHeight={handleListHeight}
88101
>
89102
<View style={styles.optionsContainerInner}>{children}</View>
90103
</ListContent>

src/components/Select.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import ListContainer from './ListContainer';
1919
import EmptyList from './EmptyList';
2020
import { Platform } from 'react-native';
2121
import { MIN_WIDTH } from './common';
22+
import { uniqBy } from 'lodash';
2223

2324
function extractStyleProps<T extends object>(
2425
obj: T,
@@ -70,12 +71,14 @@ export interface CommonProps {
7071
reverse?: boolean;
7172
selectionEffectColor?: string;
7273
optionsScrollIndicator?: boolean;
74+
emptyOptionsPlaceholder?: string;
7375
emptySearchMsg?: string;
7476
value?: string | string[];
7577
clearable?: boolean;
7678
disabled?: boolean;
7779
searchable?: boolean;
7880
createable?: boolean;
81+
avoidBottom?: 'height' | 'position';
7982
onCreateItem?: (value: string) => void;
8083
onChangeInput?: (value: string) => void;
8184
renderAnchor?: RenderAnchor;
@@ -128,11 +131,13 @@ export default function Select({
128131
reverse,
129132
selectionEffectColor,
130133
optionsScrollIndicator = true,
134+
emptyOptionsPlaceholder = 'No Options',
131135
emptySearchMsg,
132136
clearable = true,
133137
disabled,
134138
searchable = true,
135139
createable,
140+
avoidBottom,
136141
renderAnchor,
137142
renderSearch,
138143
renderOption,
@@ -146,6 +151,7 @@ export default function Select({
146151
const [search, setSearch] = useState('');
147152
const [anchorPosition, setAnchorPosition] = useState<AnchorPos>({});
148153
const [createdOptions, setCreatedOptions] = useState<Option[]>([]);
154+
const [bottomSpacerHeight, setBottomSpacerHeight] = useState(50);
149155

150156
const styles = useStyles(
151157
({ tokens: { size } }) => ({
@@ -195,8 +201,8 @@ export default function Select({
195201

196202
const list = useMemo(
197203
() =>
198-
[...options, ...createdOptions].filter(([_, val]) =>
199-
val.toLowerCase().includes(search.toLowerCase())
204+
uniqBy([...options, ...createdOptions], ([key]) => key).filter(
205+
([_, val]) => val.toLowerCase().includes(search.toLowerCase())
200206
),
201207
[createdOptions, options, search]
202208
);
@@ -261,7 +267,7 @@ export default function Select({
261267
const anchorStyleProps = extractStyleProps(rest, 'select', 'Style');
262268
const searchStyleProps = extractStyleProps(rest, 'search', 'Style');
263269
const optionStyleProps = extractStyleProps(rest, 'option', 'Style');
264-
const noOptions = options.length === 0 ? 'No Options' : undefined;
270+
const noOptions = options.length === 0 ? emptyOptionsPlaceholder : undefined;
265271

266272
const multi = useMemo(() => ('multi' in rest ? true : false), [rest]);
267273

@@ -347,6 +353,8 @@ export default function Select({
347353
},
348354
default: { animationType: 'slide' },
349355
})}
356+
avoidBottom={avoidBottom}
357+
onOptionsOffet={setBottomSpacerHeight}
350358
>
351359
{searchable && (
352360
<>
@@ -383,7 +391,7 @@ export default function Select({
383391
showsVerticalScrollIndicator={optionsScrollIndicator}
384392
ItemSeparatorComponent={optionDivider ?? Divider}
385393
keyboardShouldPersistTaps="handled"
386-
ListFooterComponent={BottomSpacer}
394+
ListFooterComponent={<BottomSpacer height={bottomSpacerHeight} />}
387395
ListEmptyComponent={
388396
<EmptyList
389397
textStyle={emptyTextStyle}

0 commit comments

Comments
 (0)