Filters bottom sheet
A "Filters" sheet with a sort order, a status select, a date range and Reset / Apply, all applied at once. It uses BottomSheet for the layout, HorizontalList for the sort order, and useDraft so nothing changes until Apply.
Loading…
import { useState } from 'react'; import { StyleSheet, Text, View } from 'react-native'; import { BottomSheet, DatePicker, HorizontalList, SelectList, useDraft, formatDate, getRangePresets, usePickerTheme, } from '@elik56a/react-native-pickers'; import type { DateRange } from '@elik56a/react-native-pickers'; import { DemoButton } from './DemoButton'; 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, })); 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 ( <> <DemoButton variant="secondary" 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={<DemoButton variant="text" title="Reset" onPress={() => setDraft(EMPTY)} />} footer={ <DemoButton 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 })} presets={getRangePresets()} 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' }, });
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.