@umituz/react-native-bottom-sheet 1.3.4 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +20 -8
- package/src/domain/entities/BottomSheet.ts +60 -3
- package/src/domain/entities/Filter.ts +48 -0
- package/src/index.ts +6 -0
- package/src/presentation/components/BottomSheet.tsx +92 -250
- package/src/presentation/components/BottomSheetModal.tsx +3 -29
- package/src/presentation/components/SafeBottomSheetModalProvider.tsx +5 -30
- package/src/presentation/components/filter/FilterBottomSheet.tsx +166 -0
- package/src/presentation/components/filter/FilterSheet.tsx +115 -0
- package/src/presentation/components/filter/FilterSheetComponents/FilterSheetHeader.tsx +33 -0
- package/src/presentation/components/filter/FilterSheetComponents/FilterSheetOption.tsx +59 -0
- package/src/presentation/hooks/useBottomSheet.ts +9 -126
- package/src/presentation/hooks/useBottomSheetModal.ts +9 -126
- package/src/presentation/hooks/useListFilters.ts +95 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Presentation - Filter Bottom Sheet component
|
|
3
|
+
*
|
|
4
|
+
* Advanced filtering UI using @umituz/react-native-bottom-sheet
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React, { forwardRef, useCallback } from 'react';
|
|
8
|
+
import { View, StyleSheet, TouchableOpacity, ScrollView } from 'react-native';
|
|
9
|
+
import { BottomSheetModal } from '../BottomSheetModal';
|
|
10
|
+
import type { BottomSheetModalRef } from '../../../domain/entities/BottomSheet';
|
|
11
|
+
import { AtomicText, AtomicIcon, AtomicButton } from '@umituz/react-native-design-system';
|
|
12
|
+
import { useAppDesignTokens } from '@umituz/react-native-design-system';
|
|
13
|
+
import { useLocalization } from '@umituz/react-native-localization';
|
|
14
|
+
import type { FilterOption, FilterCategory } from '../../../domain/entities/Filter';
|
|
15
|
+
import { FilterUtils } from '../../../domain/entities/Filter';
|
|
16
|
+
|
|
17
|
+
export interface FilterBottomSheetProps {
|
|
18
|
+
readonly categories: FilterCategory[];
|
|
19
|
+
readonly selectedIds: string[];
|
|
20
|
+
readonly onFilterPress: (id: string, categoryId: string) => void;
|
|
21
|
+
readonly onClearFilters: () => void;
|
|
22
|
+
readonly onDismiss?: () => void;
|
|
23
|
+
readonly title?: string;
|
|
24
|
+
readonly defaultId?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const FilterBottomSheet = forwardRef<BottomSheetModalRef, FilterBottomSheetProps>(({
|
|
28
|
+
categories,
|
|
29
|
+
selectedIds,
|
|
30
|
+
onFilterPress,
|
|
31
|
+
onClearFilters,
|
|
32
|
+
onDismiss,
|
|
33
|
+
title,
|
|
34
|
+
defaultId = 'all'
|
|
35
|
+
}, ref) => {
|
|
36
|
+
const tokens = useAppDesignTokens();
|
|
37
|
+
const { t } = useLocalization();
|
|
38
|
+
|
|
39
|
+
const styles = React.useMemo(() => StyleSheet.create({
|
|
40
|
+
container: {
|
|
41
|
+
flex: 1,
|
|
42
|
+
padding: 16,
|
|
43
|
+
},
|
|
44
|
+
header: {
|
|
45
|
+
flexDirection: 'row',
|
|
46
|
+
justifyContent: 'space-between',
|
|
47
|
+
alignItems: 'center',
|
|
48
|
+
marginBottom: 20,
|
|
49
|
+
},
|
|
50
|
+
category: {
|
|
51
|
+
marginBottom: 24,
|
|
52
|
+
},
|
|
53
|
+
categoryTitle: {
|
|
54
|
+
marginBottom: 12,
|
|
55
|
+
opacity: 0.7,
|
|
56
|
+
},
|
|
57
|
+
optionsGrid: {
|
|
58
|
+
flexDirection: 'row',
|
|
59
|
+
flexWrap: 'wrap',
|
|
60
|
+
gap: 8,
|
|
61
|
+
},
|
|
62
|
+
option: {
|
|
63
|
+
flexDirection: 'row',
|
|
64
|
+
alignItems: 'center',
|
|
65
|
+
paddingHorizontal: 12,
|
|
66
|
+
paddingVertical: 8,
|
|
67
|
+
borderRadius: 20,
|
|
68
|
+
backgroundColor: tokens.colors.surfaceVariant,
|
|
69
|
+
gap: 6,
|
|
70
|
+
borderWidth: 1,
|
|
71
|
+
borderColor: 'transparent',
|
|
72
|
+
},
|
|
73
|
+
optionLeft: {
|
|
74
|
+
flexDirection: 'row',
|
|
75
|
+
alignItems: 'center',
|
|
76
|
+
gap: 6,
|
|
77
|
+
},
|
|
78
|
+
footer: {
|
|
79
|
+
marginTop: 16,
|
|
80
|
+
paddingBottom: 8,
|
|
81
|
+
}
|
|
82
|
+
}), [tokens]);
|
|
83
|
+
|
|
84
|
+
const renderOption = useCallback((option: FilterOption, categoryId: string) => {
|
|
85
|
+
const isSelected = selectedIds.includes(option.id);
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<TouchableOpacity
|
|
89
|
+
key={option.id}
|
|
90
|
+
style={[
|
|
91
|
+
styles.option,
|
|
92
|
+
isSelected && { backgroundColor: tokens.colors.primary + '15' }
|
|
93
|
+
]}
|
|
94
|
+
onPress={() => onFilterPress(option.id, categoryId)}
|
|
95
|
+
>
|
|
96
|
+
<View style={styles.optionLeft}>
|
|
97
|
+
{option.icon && (
|
|
98
|
+
<AtomicIcon
|
|
99
|
+
name={option.icon as any}
|
|
100
|
+
size="sm"
|
|
101
|
+
color={isSelected ? 'primary' : 'secondary'}
|
|
102
|
+
/>
|
|
103
|
+
)}
|
|
104
|
+
<AtomicText
|
|
105
|
+
type="bodyMedium"
|
|
106
|
+
style={[isSelected && { color: tokens.colors.primary, fontWeight: '600' }]}
|
|
107
|
+
>
|
|
108
|
+
{option.label}
|
|
109
|
+
</AtomicText>
|
|
110
|
+
</View>
|
|
111
|
+
{isSelected && (
|
|
112
|
+
<AtomicIcon name="checkmark" size="sm" color="primary" />
|
|
113
|
+
)}
|
|
114
|
+
</TouchableOpacity>
|
|
115
|
+
);
|
|
116
|
+
}, [selectedIds, tokens, onFilterPress]);
|
|
117
|
+
|
|
118
|
+
const renderCategory = useCallback((category: FilterCategory) => (
|
|
119
|
+
<View key={category.id} style={styles.category}>
|
|
120
|
+
<AtomicText type="labelLarge" style={styles.categoryTitle}>
|
|
121
|
+
{category.title}
|
|
122
|
+
</AtomicText>
|
|
123
|
+
<View style={styles.optionsGrid}>
|
|
124
|
+
{category.options.map(option => renderOption(option, category.id))}
|
|
125
|
+
</View>
|
|
126
|
+
</View>
|
|
127
|
+
), [renderOption, styles]);
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
const hasActiveFilters = FilterUtils.hasActiveFilter(selectedIds, defaultId);
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<BottomSheetModal
|
|
134
|
+
ref={ref}
|
|
135
|
+
preset="medium"
|
|
136
|
+
onDismiss={onDismiss}
|
|
137
|
+
backgroundColor={tokens.colors.surface}
|
|
138
|
+
>
|
|
139
|
+
<View style={styles.container}>
|
|
140
|
+
<View style={styles.header}>
|
|
141
|
+
<AtomicText type="headlineSmall">{title || t('common.filter')}</AtomicText>
|
|
142
|
+
{hasActiveFilters && (
|
|
143
|
+
<TouchableOpacity onPress={onClearFilters}>
|
|
144
|
+
<AtomicText type="labelLarge" color="error">{t('common.clear')}</AtomicText>
|
|
145
|
+
</TouchableOpacity>
|
|
146
|
+
)}
|
|
147
|
+
</View>
|
|
148
|
+
|
|
149
|
+
<ScrollView showsVerticalScrollIndicator={false}>
|
|
150
|
+
{categories.map(renderCategory)}
|
|
151
|
+
</ScrollView>
|
|
152
|
+
|
|
153
|
+
<View style={styles.footer}>
|
|
154
|
+
<AtomicButton
|
|
155
|
+
onPress={() => (ref as any).current?.dismiss()}
|
|
156
|
+
fullWidth
|
|
157
|
+
>
|
|
158
|
+
{t('common.apply')}
|
|
159
|
+
</AtomicButton>
|
|
160
|
+
</View>
|
|
161
|
+
</View>
|
|
162
|
+
</BottomSheetModal>
|
|
163
|
+
);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
FilterBottomSheet.displayName = 'FilterBottomSheet';
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import React, { useCallback } from "react";
|
|
2
|
+
import { View, StyleSheet, ScrollView, Modal, Pressable } from "react-native";
|
|
3
|
+
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
4
|
+
import { AtomicButton, useAppDesignTokens } from "@umituz/react-native-design-system";
|
|
5
|
+
import { useLocalization } from "@umituz/react-native-localization";
|
|
6
|
+
import type { FilterOption } from "../../../domain/entities/Filter";
|
|
7
|
+
import { FilterUtils } from "../../../domain/entities/Filter";
|
|
8
|
+
import { FilterSheetHeader } from "./FilterSheetComponents/FilterSheetHeader";
|
|
9
|
+
import { FilterSheetOption } from "./FilterSheetComponents/FilterSheetOption";
|
|
10
|
+
|
|
11
|
+
export interface FilterSheetProps {
|
|
12
|
+
visible: boolean;
|
|
13
|
+
options: FilterOption[];
|
|
14
|
+
selectedIds: string[];
|
|
15
|
+
onFilterPress: (filterId: string) => void;
|
|
16
|
+
onClearFilters: () => void;
|
|
17
|
+
onClose?: () => void;
|
|
18
|
+
defaultFilterId?: string;
|
|
19
|
+
title?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const FilterSheet: React.FC<FilterSheetProps> = ({
|
|
23
|
+
visible,
|
|
24
|
+
options,
|
|
25
|
+
selectedIds,
|
|
26
|
+
onFilterPress,
|
|
27
|
+
onClearFilters,
|
|
28
|
+
onClose,
|
|
29
|
+
defaultFilterId = "all",
|
|
30
|
+
title,
|
|
31
|
+
}) => {
|
|
32
|
+
const tokens = useAppDesignTokens();
|
|
33
|
+
const { t } = useLocalization();
|
|
34
|
+
const insets = useSafeAreaInsets();
|
|
35
|
+
|
|
36
|
+
const hasActiveFilter = FilterUtils.hasActiveFilter(selectedIds, defaultFilterId);
|
|
37
|
+
|
|
38
|
+
const handleFilterPressWithClose = useCallback((id: string) => {
|
|
39
|
+
onFilterPress(id);
|
|
40
|
+
onClose?.();
|
|
41
|
+
}, [onFilterPress, onClose]);
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<Modal visible={visible} transparent animationType="slide" onRequestClose={onClose}>
|
|
45
|
+
<Pressable style={styles.backdrop} onPress={onClose}>
|
|
46
|
+
<Pressable
|
|
47
|
+
style={[styles.sheet, { backgroundColor: tokens.colors.surface, paddingBottom: insets.bottom }]}
|
|
48
|
+
onPress={(e) => e.stopPropagation()}
|
|
49
|
+
>
|
|
50
|
+
<View style={[styles.handle, { backgroundColor: tokens.colors.border }]} />
|
|
51
|
+
|
|
52
|
+
<FilterSheetHeader
|
|
53
|
+
title={title || t("general.filter")}
|
|
54
|
+
onClose={() => onClose?.()}
|
|
55
|
+
tokens={tokens}
|
|
56
|
+
/>
|
|
57
|
+
|
|
58
|
+
<ScrollView style={styles.optionsList} showsVerticalScrollIndicator={false}>
|
|
59
|
+
{options.map((option) => (
|
|
60
|
+
<FilterSheetOption
|
|
61
|
+
key={option.id}
|
|
62
|
+
option={option}
|
|
63
|
+
isSelected={selectedIds.includes(option.id)}
|
|
64
|
+
onPress={handleFilterPressWithClose}
|
|
65
|
+
tokens={tokens}
|
|
66
|
+
/>
|
|
67
|
+
))}
|
|
68
|
+
</ScrollView>
|
|
69
|
+
|
|
70
|
+
{hasActiveFilter && (
|
|
71
|
+
<View style={[styles.footer, { borderTopColor: tokens.colors.border, borderTopWidth: tokens.borders.width.thin }]}>
|
|
72
|
+
<AtomicButton variant="outline" onPress={onClearFilters} fullWidth>
|
|
73
|
+
{t("general.clear")}
|
|
74
|
+
</AtomicButton>
|
|
75
|
+
</View>
|
|
76
|
+
)}
|
|
77
|
+
</Pressable>
|
|
78
|
+
</Pressable>
|
|
79
|
+
</Modal>
|
|
80
|
+
);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
FilterSheet.displayName = "FilterSheet";
|
|
84
|
+
|
|
85
|
+
const styles = StyleSheet.create({
|
|
86
|
+
backdrop: {
|
|
87
|
+
flex: 1,
|
|
88
|
+
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
|
89
|
+
justifyContent: "flex-end",
|
|
90
|
+
},
|
|
91
|
+
sheet: {
|
|
92
|
+
borderTopLeftRadius: 16,
|
|
93
|
+
borderTopRightRadius: 16,
|
|
94
|
+
maxHeight: "80%",
|
|
95
|
+
},
|
|
96
|
+
handle: {
|
|
97
|
+
width: 40,
|
|
98
|
+
height: 4,
|
|
99
|
+
borderRadius: 2,
|
|
100
|
+
alignSelf: "center",
|
|
101
|
+
marginTop: 8,
|
|
102
|
+
marginBottom: 8,
|
|
103
|
+
},
|
|
104
|
+
optionsList: {
|
|
105
|
+
maxHeight: 400,
|
|
106
|
+
paddingVertical: 8,
|
|
107
|
+
},
|
|
108
|
+
footer: {
|
|
109
|
+
paddingHorizontal: 20,
|
|
110
|
+
paddingTop: 16,
|
|
111
|
+
paddingBottom: 8,
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, StyleSheet, TouchableOpacity } from 'react-native';
|
|
3
|
+
import { AtomicText, AtomicIcon } from '@umituz/react-native-design-system';
|
|
4
|
+
import type { useAppDesignTokens } from '@umituz/react-native-design-system';
|
|
5
|
+
|
|
6
|
+
interface FilterSheetHeaderProps {
|
|
7
|
+
title: string;
|
|
8
|
+
onClose: () => void;
|
|
9
|
+
tokens: ReturnType<typeof useAppDesignTokens>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const FilterSheetHeader = ({ title, onClose, tokens }: FilterSheetHeaderProps) => (
|
|
13
|
+
<View style={[styles.header, { borderBottomColor: tokens.colors.border, borderBottomWidth: tokens.borders.width.thin }]}>
|
|
14
|
+
<AtomicText type="headlineMedium" style={styles.title}>{title}</AtomicText>
|
|
15
|
+
<TouchableOpacity onPress={onClose} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}>
|
|
16
|
+
<AtomicIcon name="close" size="md" color="primary" />
|
|
17
|
+
</TouchableOpacity>
|
|
18
|
+
</View>
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const styles = StyleSheet.create({
|
|
22
|
+
header: {
|
|
23
|
+
flexDirection: 'row',
|
|
24
|
+
alignItems: 'center',
|
|
25
|
+
justifyContent: 'space-between',
|
|
26
|
+
paddingHorizontal: 20,
|
|
27
|
+
paddingBottom: 16,
|
|
28
|
+
paddingTop: 8,
|
|
29
|
+
},
|
|
30
|
+
title: {
|
|
31
|
+
fontWeight: '600',
|
|
32
|
+
},
|
|
33
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, StyleSheet, TouchableOpacity } from 'react-native';
|
|
3
|
+
import { AtomicText, AtomicIcon } from '@umituz/react-native-design-system';
|
|
4
|
+
import type { useAppDesignTokens } from '@umituz/react-native-design-system';
|
|
5
|
+
import type { FilterOption } from '../../../../domain/entities/Filter';
|
|
6
|
+
|
|
7
|
+
interface FilterSheetOptionProps {
|
|
8
|
+
option: FilterOption;
|
|
9
|
+
isSelected: boolean;
|
|
10
|
+
onPress: (id: string) => void;
|
|
11
|
+
tokens: ReturnType<typeof useAppDesignTokens>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const FilterSheetOption = ({ option, isSelected, onPress, tokens }: FilterSheetOptionProps) => (
|
|
15
|
+
<TouchableOpacity
|
|
16
|
+
onPress={() => onPress(option.id)}
|
|
17
|
+
style={[
|
|
18
|
+
styles.option,
|
|
19
|
+
{ borderBottomColor: tokens.colors.borderLight, borderBottomWidth: tokens.borders.width.thin },
|
|
20
|
+
isSelected && { backgroundColor: tokens.colors.primary + '15' }
|
|
21
|
+
]}
|
|
22
|
+
>
|
|
23
|
+
<View style={styles.optionContent}>
|
|
24
|
+
{option.icon && (
|
|
25
|
+
<AtomicIcon
|
|
26
|
+
name={option.icon as any}
|
|
27
|
+
size="md"
|
|
28
|
+
color={isSelected ? 'primary' : 'secondary'}
|
|
29
|
+
/>
|
|
30
|
+
)}
|
|
31
|
+
<AtomicText
|
|
32
|
+
type="bodyLarge"
|
|
33
|
+
style={[styles.optionLabel, isSelected && { color: tokens.colors.primary, fontWeight: '600' }]}
|
|
34
|
+
>
|
|
35
|
+
{option.label}
|
|
36
|
+
</AtomicText>
|
|
37
|
+
</View>
|
|
38
|
+
{isSelected && <AtomicIcon name="checkmark-circle" size="md" color="primary" />}
|
|
39
|
+
</TouchableOpacity>
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const styles = StyleSheet.create({
|
|
43
|
+
option: {
|
|
44
|
+
flexDirection: 'row',
|
|
45
|
+
alignItems: 'center',
|
|
46
|
+
justifyContent: 'space-between',
|
|
47
|
+
paddingHorizontal: 20,
|
|
48
|
+
paddingVertical: 16,
|
|
49
|
+
},
|
|
50
|
+
optionContent: {
|
|
51
|
+
flexDirection: 'row',
|
|
52
|
+
alignItems: 'center',
|
|
53
|
+
gap: 12,
|
|
54
|
+
flex: 1,
|
|
55
|
+
},
|
|
56
|
+
optionLabel: {
|
|
57
|
+
flex: 1,
|
|
58
|
+
},
|
|
59
|
+
});
|
|
@@ -1,133 +1,16 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* useBottomSheet Hook
|
|
3
|
-
*
|
|
4
|
-
* React hook for managing bottom sheet state and interactions.
|
|
5
|
-
* Provides a clean API for common bottom sheet operations.
|
|
6
|
-
*
|
|
7
|
-
* Features:
|
|
8
|
-
* - Open/close bottom sheet
|
|
9
|
-
* - Snap to specific index
|
|
10
|
-
* - Expand to max height
|
|
11
|
-
* - Collapse to min height
|
|
12
|
-
* - Track current position
|
|
13
|
-
* - Preset configurations
|
|
14
|
-
*
|
|
15
|
-
* Usage:
|
|
16
|
-
* ```tsx
|
|
17
|
-
* const { sheetRef, open, close, expand, collapse } = useBottomSheet();
|
|
18
|
-
*
|
|
19
|
-
* return (
|
|
20
|
-
* <>
|
|
21
|
-
* <Button onPress={open}>Open Sheet</Button>
|
|
22
|
-
* <BottomSheet ref={sheetRef} preset="medium">
|
|
23
|
-
* <Text>Content</Text>
|
|
24
|
-
* </BottomSheet>
|
|
25
|
-
* </>
|
|
26
|
-
* );
|
|
27
|
-
* ```
|
|
28
|
-
*/
|
|
29
|
-
|
|
30
1
|
import { useRef, useCallback } from 'react';
|
|
31
|
-
import type { BottomSheetRef } from '
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Return type for useBottomSheet hook
|
|
35
|
-
*/
|
|
36
|
-
export interface UseBottomSheetReturn {
|
|
37
|
-
/**
|
|
38
|
-
* Ref to attach to BottomSheet component
|
|
39
|
-
*/
|
|
40
|
-
sheetRef: React.RefObject<BottomSheetRef | null>;
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Open bottom sheet to initial index
|
|
44
|
-
*/
|
|
45
|
-
open: () => void;
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Close bottom sheet completely
|
|
49
|
-
*/
|
|
50
|
-
close: () => void;
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Expand to maximum height
|
|
54
|
-
*/
|
|
55
|
-
expand: () => void;
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Collapse to minimum height
|
|
59
|
-
*/
|
|
60
|
-
collapse: () => void;
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Snap to specific index
|
|
64
|
-
*/
|
|
65
|
-
snapToIndex: (index: number) => void;
|
|
2
|
+
import type { BottomSheetRef, UseBottomSheetReturn } from '../../domain/entities/BottomSheet';
|
|
66
3
|
|
|
67
|
-
/**
|
|
68
|
-
* Snap to specific position
|
|
69
|
-
*/
|
|
70
|
-
snapToPosition: (position: string | number) => void;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* useBottomSheet Hook
|
|
75
|
-
*
|
|
76
|
-
* Hook for managing bottom sheet state and interactions.
|
|
77
|
-
* Provides imperative methods for controlling the sheet.
|
|
78
|
-
*/
|
|
79
4
|
export const useBottomSheet = (): UseBottomSheetReturn => {
|
|
80
5
|
const sheetRef = useRef<BottomSheetRef>(null);
|
|
81
6
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Close bottom sheet completely
|
|
91
|
-
*/
|
|
92
|
-
const close = useCallback(() => {
|
|
93
|
-
sheetRef.current?.close();
|
|
94
|
-
}, []);
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Expand to maximum height (last snap point)
|
|
98
|
-
*/
|
|
99
|
-
const expand = useCallback(() => {
|
|
100
|
-
sheetRef.current?.expand();
|
|
101
|
-
}, []);
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Collapse to minimum height (first snap point)
|
|
105
|
-
*/
|
|
106
|
-
const collapse = useCallback(() => {
|
|
107
|
-
sheetRef.current?.collapse();
|
|
108
|
-
}, []);
|
|
7
|
+
const open = useCallback(() => sheetRef.current?.snapToIndex(0), []);
|
|
8
|
+
const close = useCallback(() => sheetRef.current?.close(), []);
|
|
9
|
+
const expand = useCallback(() => sheetRef.current?.expand(), []);
|
|
10
|
+
const collapse = useCallback(() => sheetRef.current?.collapse(), []);
|
|
11
|
+
const snapToIndex = useCallback((index: number) => sheetRef.current?.snapToIndex(index), []);
|
|
12
|
+
const snapToPosition = useCallback((pos: string | number) => sheetRef.current?.snapToPosition(pos), []);
|
|
109
13
|
|
|
110
|
-
|
|
111
|
-
* Snap to specific index
|
|
112
|
-
*/
|
|
113
|
-
const snapToIndex = useCallback((index: number) => {
|
|
114
|
-
sheetRef.current?.snapToIndex(index);
|
|
115
|
-
}, []);
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Snap to specific position (percentage or fixed)
|
|
119
|
-
*/
|
|
120
|
-
const snapToPosition = useCallback((position: string | number) => {
|
|
121
|
-
sheetRef.current?.snapToPosition(position);
|
|
122
|
-
}, []);
|
|
123
|
-
|
|
124
|
-
return {
|
|
125
|
-
sheetRef,
|
|
126
|
-
open,
|
|
127
|
-
close,
|
|
128
|
-
expand,
|
|
129
|
-
collapse,
|
|
130
|
-
snapToIndex,
|
|
131
|
-
snapToPosition,
|
|
132
|
-
};
|
|
14
|
+
return { sheetRef, open, close, expand, collapse, snapToIndex, snapToPosition };
|
|
133
15
|
};
|
|
16
|
+
|
|
@@ -1,134 +1,17 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* useBottomSheetModal Hook
|
|
3
|
-
*
|
|
4
|
-
* React hook for managing bottom sheet modal state and interactions.
|
|
5
|
-
* Provides a clean API for common bottom sheet modal operations.
|
|
6
|
-
*
|
|
7
|
-
* Features:
|
|
8
|
-
* - Present/dismiss bottom sheet modal
|
|
9
|
-
* - Snap to specific index
|
|
10
|
-
* - Expand to max height
|
|
11
|
-
* - Collapse to min height
|
|
12
|
-
* - Track current position
|
|
13
|
-
* - Preset configurations
|
|
14
|
-
*
|
|
15
|
-
* Usage:
|
|
16
|
-
* ```tsx
|
|
17
|
-
* const { modalRef, present, dismiss, expand, collapse } = useBottomSheetModal();
|
|
18
|
-
*
|
|
19
|
-
* return (
|
|
20
|
-
* <>
|
|
21
|
-
* <Button onPress={present}>Open Modal</Button>
|
|
22
|
-
* <BottomSheetModal ref={modalRef} preset="medium">
|
|
23
|
-
* <Text>Content</Text>
|
|
24
|
-
* </BottomSheetModal>
|
|
25
|
-
* </>
|
|
26
|
-
* );
|
|
27
|
-
* ```
|
|
28
|
-
*/
|
|
29
|
-
|
|
30
1
|
import { useRef, useCallback } from 'react';
|
|
31
|
-
import type { BottomSheetModalRef } from '
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Return type for useBottomSheetModal hook
|
|
35
|
-
*/
|
|
36
|
-
export interface UseBottomSheetModalReturn {
|
|
37
|
-
/**
|
|
38
|
-
* Ref to attach to BottomSheetModal component
|
|
39
|
-
*/
|
|
40
|
-
modalRef: React.RefObject<BottomSheetModalRef | null>;
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Present bottom sheet modal
|
|
44
|
-
*/
|
|
45
|
-
present: () => void;
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Dismiss bottom sheet modal
|
|
49
|
-
*/
|
|
50
|
-
dismiss: () => void;
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Expand to maximum height
|
|
54
|
-
*/
|
|
55
|
-
expand: () => void;
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Collapse to minimum height
|
|
59
|
-
*/
|
|
60
|
-
collapse: () => void;
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Snap to specific index
|
|
64
|
-
*/
|
|
65
|
-
snapToIndex: (index: number) => void;
|
|
2
|
+
import type { BottomSheetModalRef, UseBottomSheetModalReturn } from '../../domain/entities/BottomSheet';
|
|
66
3
|
|
|
67
|
-
/**
|
|
68
|
-
* Snap to specific position
|
|
69
|
-
*/
|
|
70
|
-
snapToPosition: (position: string | number) => void;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* useBottomSheetModal Hook
|
|
75
|
-
*
|
|
76
|
-
* Hook for managing bottom sheet modal state and interactions.
|
|
77
|
-
* Provides imperative methods for controlling the modal.
|
|
78
|
-
*/
|
|
79
4
|
export const useBottomSheetModal = (): UseBottomSheetModalReturn => {
|
|
80
5
|
const modalRef = useRef<BottomSheetModalRef>(null);
|
|
81
6
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Dismiss bottom sheet modal
|
|
91
|
-
*/
|
|
92
|
-
const dismiss = useCallback(() => {
|
|
93
|
-
modalRef.current?.dismiss();
|
|
94
|
-
}, []);
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Expand to maximum height (last snap point)
|
|
98
|
-
*/
|
|
99
|
-
const expand = useCallback(() => {
|
|
100
|
-
modalRef.current?.expand();
|
|
101
|
-
}, []);
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Collapse to minimum height (first snap point)
|
|
105
|
-
*/
|
|
106
|
-
const collapse = useCallback(() => {
|
|
107
|
-
modalRef.current?.collapse();
|
|
108
|
-
}, []);
|
|
7
|
+
const present = useCallback(() => modalRef.current?.present(), []);
|
|
8
|
+
const dismiss = useCallback(() => modalRef.current?.dismiss(), []);
|
|
9
|
+
const expand = useCallback(() => modalRef.current?.expand(), []);
|
|
10
|
+
const collapse = useCallback(() => modalRef.current?.collapse(), []);
|
|
11
|
+
const snapToIndex = useCallback((index: number) => modalRef.current?.snapToIndex(index), []);
|
|
12
|
+
const snapToPosition = useCallback((pos: string | number) => modalRef.current?.snapToPosition(pos), []);
|
|
109
13
|
|
|
110
|
-
|
|
111
|
-
* Snap to specific index
|
|
112
|
-
*/
|
|
113
|
-
const snapToIndex = useCallback((index: number) => {
|
|
114
|
-
modalRef.current?.snapToIndex(index);
|
|
115
|
-
}, []);
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Snap to specific position (percentage or fixed)
|
|
119
|
-
*/
|
|
120
|
-
const snapToPosition = useCallback((position: string | number) => {
|
|
121
|
-
modalRef.current?.snapToPosition(position);
|
|
122
|
-
}, []);
|
|
123
|
-
|
|
124
|
-
return {
|
|
125
|
-
modalRef,
|
|
126
|
-
present,
|
|
127
|
-
dismiss,
|
|
128
|
-
expand,
|
|
129
|
-
collapse,
|
|
130
|
-
snapToIndex,
|
|
131
|
-
snapToPosition,
|
|
132
|
-
};
|
|
14
|
+
return { modalRef, present, dismiss, expand, collapse, snapToIndex, snapToPosition };
|
|
133
15
|
};
|
|
134
16
|
|
|
17
|
+
|