Skip to content

Commit 8cf72b8

Browse files
committed
feat: avoid being bottom hidden, creatable and disabled
1 parent d5fc8ff commit 8cf72b8

File tree

7 files changed

+164
-46
lines changed

7 files changed

+164
-46
lines changed

example/src/App.tsx

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ 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 />
24+
<MultiSelect
25+
label="Disabled Select"
26+
optionsCount={10}
27+
defaults={['value-3', 'value-7']}
28+
disabled
29+
/>
2330
</View>
2431
</SafeAreaView>
2532
</SafeAreaProvider>
@@ -29,7 +36,6 @@ export default function App() {
2936

3037
interface SingleSelectProps {
3138
label: string;
32-
zIndex?: number;
3339
optionsCount?: number;
3440
}
3541
function SingleSelect({ label, optionsCount = 50 }: SingleSelectProps) {
@@ -48,6 +54,7 @@ function SingleSelect({ label, optionsCount = 50 }: SingleSelectProps) {
4854
onChangeValue={setOptions}
4955
placeholder="Select Option"
5056
searchPlaceholder="Search Available Options"
57+
searchPlaceholderTextColor="gray"
5158
listTitle="Options"
5259
reverse
5360
/>
@@ -58,9 +65,18 @@ function SingleSelect({ label, optionsCount = 50 }: SingleSelectProps) {
5865
interface MultiSelectProps {
5966
label: string;
6067
optionsCount?: number;
68+
creatable?: boolean;
69+
disabled?: boolean;
70+
defaults?: string[];
6171
}
62-
function MultiSelect({ label, optionsCount = 50 }: MultiSelectProps) {
63-
const [options, setOptions] = useState<string[]>([]);
72+
function MultiSelect({
73+
label,
74+
optionsCount = 50,
75+
creatable,
76+
disabled,
77+
defaults = [],
78+
}: MultiSelectProps) {
79+
const [options, setOptions] = useState<string[]>(defaults);
6480

6581
return (
6682
<View style={styles.preview}>
@@ -77,6 +93,9 @@ function MultiSelect({ label, optionsCount = 50 }: MultiSelectProps) {
7793
placeholder="Select Option"
7894
searchPlaceholder="Search Available Options"
7995
listTitle="Options"
96+
searchPlaceholderTextColor="gray"
97+
createable={creatable}
98+
disabled={disabled}
8099
reverse
81100
multi
82101
/>
@@ -87,14 +106,14 @@ function MultiSelect({ label, optionsCount = 50 }: MultiSelectProps) {
87106
const styles = StyleSheet.create({
88107
main: {
89108
flex: 1,
90-
gap: 8,
91-
justifyContent: 'center',
92-
alignItems: 'center',
93109
},
94110
container: {
111+
flex: 1,
95112
padding: 16,
96113
gap: 16,
97-
marginTop: -180,
114+
flexDirection: 'column',
115+
justifyContent: 'center',
116+
alignSelf: 'center',
98117
},
99118
header: {
100119
fontSize: 32,

src/components/Anchor.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,14 @@ import ChevronDownIcon from '../icons/ChevronDownIcon';
1212
import type { IconStyle, LayoutRect, Option } from '../types';
1313
import Selections from './Selections';
1414
import CloseIcon from '../icons/CloseIcon';
15+
import { StyleSheet } from 'react-native';
1516

1617
interface Props extends Omit<PressableProps, 'onLayout'> {
1718
placeholder?: string;
1819
selected: Option[];
1920
multi: boolean;
21+
clearable?: boolean;
22+
disabled?: boolean;
2023
onRemove: (key: string) => void;
2124
onClear: () => void;
2225
onLayout: (rect: LayoutRect) => void;
@@ -33,6 +36,8 @@ export default function Anchor({
3336
placeholder,
3437
selected,
3538
multi = false,
39+
clearable,
40+
disabled,
3641
onRemove,
3742
onClear,
3843
onLayout,
@@ -68,8 +73,14 @@ export default function Anchor({
6873
height: tokens.size.xl + 4,
6974
justifyContent: 'center',
7075
},
76+
disabled: {
77+
cursor: 'not-allowed',
78+
backgroundColor: 'white',
79+
opacity: 0.4,
80+
borderRadius: tokens.size.xs,
81+
},
7182
}),
72-
[selectIconStyle]
83+
[selectIconStyle, disabled]
7384
);
7485
const ref = useRef<View>(null);
7586
const onLayoutRef = useRef(onLayout);
@@ -119,7 +130,7 @@ export default function Anchor({
119130
/>
120131
)}
121132
<View style={styles.iconContainer}>
122-
{selected.length > 0 && (
133+
{selected.length > 0 && clearable && !disabled && (
123134
<Pressable style={styles.closeIconContainer} onPress={onClear}>
124135
<CloseIcon
125136
stroke={selectIconStyle?.color ?? '#c5c5c5'}
@@ -132,6 +143,7 @@ export default function Anchor({
132143
style={styles.icon}
133144
/>
134145
</View>
146+
{disabled && <View style={[StyleSheet.absoluteFill, styles.disabled]} />}
135147
</Pressable>
136148
);
137149
}

src/components/EmptyList.tsx

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,25 @@
11
import React from 'react';
22
import { Text, View, type TextStyle } from 'react-native';
33
import useStyles from '../hooks/useStyles';
4+
import { Pressable } from 'react-native';
45

56
interface Props {
67
msg?: string;
78
textStyle?: TextStyle;
9+
createOption?: string;
10+
onCreate?: (value: string) => void;
811
}
9-
export default function EmptyList({ msg, textStyle }: Props) {
12+
export default function EmptyList({
13+
msg,
14+
textStyle,
15+
createOption,
16+
onCreate,
17+
}: Props) {
1018
const styles = useStyles(
1119
({ tokens: { size } }) => ({
1220
container: {
1321
flex: 1,
14-
justifyContent: 'center',
22+
justifyContent: !createOption ? 'center' : undefined,
1523
alignItems: 'center',
1624
},
1725
text: {
@@ -21,14 +29,29 @@ export default function EmptyList({ msg, textStyle }: Props) {
2129
textAlign: 'center',
2230
paddingVertical: size.lg,
2331
},
32+
create: {
33+
padding: size.sm,
34+
backgroundColor: '#f9f9f9',
35+
width: '100%',
36+
},
2437
}),
2538
[]
2639
);
2740
return (
2841
<View style={[styles.container]}>
29-
<Text style={[styles.text, textStyle]}>
30-
{msg ?? '"No option matched your query"'}
31-
</Text>
42+
{!createOption && (
43+
<Text style={[styles.text, textStyle]}>
44+
{msg ?? '"No option matched your search"'}
45+
</Text>
46+
)}
47+
{createOption && (
48+
<Pressable
49+
style={styles.create}
50+
onPress={() => onCreate?.(createOption)}
51+
>
52+
<Text>Create "{createOption}"</Text>
53+
</Pressable>
54+
)}
3255
</View>
3356
);
3457
}

src/components/ListContainer.tsx

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from 'react';
1+
import React, { useEffect, useMemo, useState } from 'react';
22
import type { ModalProps } from 'react-native';
33
import { Modal, SafeAreaView } from 'react-native';
44
import { SafeAreaProvider } from 'react-native-safe-area-context';
@@ -8,6 +8,8 @@ import { Platform } from 'react-native';
88
import { Pressable } from 'react-native';
99
import type { AnchorPos } from '../types';
1010
import { useWindowDimensions } from 'react-native';
11+
import type { ViewProps } from 'react-native';
12+
import { MIN_WIDTH } from './common';
1113

1214
interface Props extends ModalProps {
1315
position?: AnchorPos;
@@ -19,14 +21,22 @@ export default function ListContainer({
1921
position,
2022
...rest
2123
}: Props) {
22-
const { width } = useWindowDimensions();
23-
const willBleed = (position?.x ?? 0) + 280 > width;
24+
const { width, height } = useWindowDimensions();
25+
const [listHeight, setListHeight] = useState(0);
2426

27+
// will bleed horizontally
28+
const willBleed = (position?.x ?? 0) + MIN_WIDTH > width;
2529
const left = willBleed ? undefined : position?.x ?? 0;
2630
const right = willBleed
2731
? width - ((position?.x ?? 0) + (position?.width ?? 0))
2832
: undefined;
29-
const top = position?.y ?? 0;
33+
34+
const top = useMemo(() => {
35+
if (height - (position?.y ?? 0) < listHeight + 80) {
36+
return height - listHeight - 80;
37+
}
38+
return position?.y ?? 0;
39+
}, [height, listHeight, position?.y]);
3040

3141
const styles = useStyles(
3242
({ tokens }) => ({
@@ -72,11 +82,32 @@ export default function ListContainer({
7282
<Modal {...rest}>
7383
<Pressable style={styles.backdrop} onPress={rest.onRequestClose}>
7484
<SafeAreaProvider>
75-
<SafeAreaView style={[styles.optionsContainer, style]}>
85+
<ListContent
86+
style={[styles.optionsContainer, style]}
87+
onListHeight={setListHeight}
88+
>
7689
<View style={styles.optionsContainerInner}>{children}</View>
77-
</SafeAreaView>
90+
</ListContent>
7891
</SafeAreaProvider>
7992
</Pressable>
8093
</Modal>
8194
);
8295
}
96+
97+
interface Props extends ViewProps {
98+
onListHeight?: (height: number) => void;
99+
}
100+
function ListContent({ onListHeight, ...rest }: Props) {
101+
const [containerRef, setContainerRef] = useState<View | null>(null);
102+
103+
useEffect(() => {
104+
const onViewLayout = () => {
105+
containerRef?.measure((_x, _y, _w, h, _pageX, _pageY) => {
106+
onListHeight?.(h);
107+
});
108+
};
109+
onViewLayout();
110+
}, [containerRef, onListHeight]);
111+
112+
return <SafeAreaView ref={setContainerRef} {...rest} />;
113+
}

src/components/SearchBox.tsx

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ interface Props extends Omit<TextInputProps, 'style'> {
1818
export default function SearchBox({
1919
onBackPress,
2020
onChangeText,
21+
placeholder,
2122
searchContainerStyle,
2223
searchInputStyle,
2324
searchBackIconStyle,
@@ -34,11 +35,14 @@ export default function SearchBox({
3435
alignItems: 'center',
3536
paddingHorizontal: size.sm,
3637
},
38+
inputWrapper: {
39+
flex: 1,
40+
height: size.xl,
41+
},
3742
input: {
3843
flex: 1,
3944
backgroundColor: '#f9f9f9',
4045
borderRadius: size.xs,
41-
height: size.xl + 4,
4246
paddingRight: size.lg,
4347
paddingLeft: size.sm,
4448
},
@@ -71,15 +75,18 @@ export default function SearchBox({
7175
/>
7276
</Pressable>
7377
)}
74-
<TextInput
75-
{...rest}
76-
ref={ref}
77-
style={[styles.input, searchInputStyle]}
78-
onChangeText={(v) => {
79-
setValue(v);
80-
onChangeText?.(v);
81-
}}
82-
/>
78+
<Pressable style={styles.inputWrapper}>
79+
<TextInput
80+
ref={ref}
81+
{...rest}
82+
placeholder={placeholder ?? 'Search...'}
83+
style={[styles.input, searchInputStyle]}
84+
onChangeText={(v) => {
85+
setValue(v);
86+
onChangeText?.(v);
87+
}}
88+
/>
89+
</Pressable>
8390
{!!value && (
8491
<Pressable onPress={clearText}>
8592
<Close

0 commit comments

Comments
 (0)