Filters bottom sheet
A "Filters" sheet with a sort order, a status select, a date range and Reset / Apply, all applied at once. The buttons take their colours and corners from usePickerTheme(), so they match the pickers in light and dark mode and in each design. It uses BottomSheet for the layout, HorizontalList for the sort order, and useDraft so nothing changes until Apply.
Loading…
import { useState } from 'react'; import { Pressable, StyleSheet, Text, View } from 'react-native'; import { BottomSheet, DatePicker, HorizontalList, SelectList, useDraft, formatDate, usePickerTheme, } from '@elik56a/react-native-pickers'; import type { DateRange } from '@elik56a/react-native-pickers'; type Sort = 'newest' | 'oldest' | 'priority'; type Filters = { sort: Sort; status: string[]; dates: DateRange }; const EMPTY: Filters = { sort: 'newest', status: [], dates: { start: null, end: null }, }; const SORTS: { label: string; value: Sort }[] = [ { label: 'Newest', value: 'newest' }, { label: 'Oldest', value: 'oldest' }, { label: 'Priority', value: 'priority' }, ]; const STATUSES = ['Open', 'In progress', 'Blocked', 'Done'].map((s) => ({ label: s, value: s, })); function Button({ title, onPress, primary, }: { title: string; onPress: () => void; primary?: boolean; }) { const { colors, radius } = usePickerTheme(); return ( <Pressable onPress={onPress} style={[ styles.btn, { borderRadius: radius.md, backgroundColor: primary ? colors.primary : colors.surface }, ]} > <Text style={[styles.btnText, { color: primary ? colors.onPrimary : colors.text }]}> {title} </Text> </Pressable> ); } export default function FiltersExample() { const { colors } = usePickerTheme(); const [open, setOpen] = useState(false); const [filters, setFilters] = useState<Filters>(EMPTY); const [draft, setDraft] = useDraft(filters, open); const count = filters.status.length + (filters.dates.start ? 1 : 0); return ( <> <Button title={`Filters${count ? ` · ${count}` : ''}`} onPress={() => setOpen(true)} /> <Text style={[styles.summary, { color: colors.textSecondary }]}> {SORTS.find((s) => s.value === filters.sort)?.label} ·{' '} {filters.status.join(', ') || 'Any status'} ·{' '} {filters.dates.start && filters.dates.end ? `${formatDate(filters.dates.start)} → ${formatDate(filters.dates.end)}` : 'Any date'} </Text> <BottomSheet visible={open} onClose={() => setOpen(false)} title="Filters" height={0.85} headerRight={<Button title="Reset" onPress={() => setDraft(EMPTY)} />} footer={ <Button primary title="Apply" onPress={() => { setFilters(draft); setOpen(false); }} /> } > <View style={styles.body}> <Text style={[styles.h, { color: colors.textSecondary }]}>Sort by</Text> <HorizontalList items={SORTS} value={draft.sort} onChange={(sort) => setDraft({ ...draft, sort })} accessibilityLabel="Sort by" style={styles.sort} /> <Text style={[styles.h, { color: colors.textSecondary }]}>Status</Text> <SelectList multiple options={STATUSES} value={draft.status} onChange={(status) => setDraft({ ...draft, status })} /> <Text style={[styles.h, { color: colors.textSecondary }]}>Created</Text> <DatePicker mode="range" value={draft.dates} onChange={(dates) => setDraft({ ...draft, dates })} maxDate={new Date()} dayHeight={38} /> </View> </BottomSheet> </> ); } const styles = StyleSheet.create({ body: { paddingHorizontal: 8 }, h: { fontSize: 12, fontWeight: '700', textTransform: 'uppercase', marginTop: 12, marginBottom: 4, marginStart: 12, }, sort: { marginHorizontal: 12 }, summary: { marginTop: 8, textAlign: 'center' }, btn: { paddingVertical: 12, paddingHorizontal: 16, alignItems: 'center' }, btnText: { fontWeight: '600' }, });
Why useDraft​
useDraft(filters, open) copies filters into draft each time the sheet opens. Edits change only the draft; Apply commits it, and closing any other way discards it. It's the same mechanism the built-in picker sheets use.
Scrolling content
With a fixed height, the sheet's content area is flex: 1. If your filters are taller than the sheet, wrap the body in a ScrollView.