Skip to content

Commit dd2ac23

Browse files
Merge pull request #44 from nandorojo/multi-dates
Support multiple date selection
2 parents d89138b + fd39aab commit dd2ac23

File tree

6 files changed

+171
-7
lines changed

6 files changed

+171
-7
lines changed

example/src/App.tsx

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ function App({
6060
hour12: false,
6161
});
6262
const [date, setDate] = React.useState<Date | undefined>(undefined);
63+
const [dates, setDates] = React.useState<Date[] | undefined>();
6364
const [range, setRange] = React.useState<{
6465
startDate: Date | undefined;
6566
endDate: Date | undefined;
@@ -78,6 +79,7 @@ function App({
7879
const onDismissTime = React.useCallback(() => {
7980
setTimeOpen(false);
8081
}, [setTimeOpen]);
82+
const [multiOpen, setMultiOpen] = React.useState(false);
8183

8284
const onDismissRange = React.useCallback(() => {
8385
setRangeOpen(false);
@@ -91,6 +93,10 @@ function App({
9193
setSingleOpen(false);
9294
}, [setSingleOpen]);
9395

96+
const onDismissMulti = React.useCallback(() => {
97+
setMultiOpen(false);
98+
}, []);
99+
94100
const onDismissCustom = React.useCallback(() => {
95101
setCustomOpen(false);
96102
}, [setCustomOpen]);
@@ -119,6 +125,12 @@ function App({
119125
[setSingleOpen, setDate]
120126
);
121127

128+
const onChangeMulti = React.useCallback((params) => {
129+
setMultiOpen(false);
130+
setDates(params.dates);
131+
console.log('[on-change-multi]', params);
132+
}, []);
133+
122134
const onConfirmTime = React.useCallback(
123135
({ hours, minutes }) => {
124136
setTimeOpen(false);
@@ -227,6 +239,15 @@ function App({
227239
: '-'}
228240
</Text>
229241
</Row>
242+
<Row>
243+
<Label>Dates</Label>
244+
<Text>
245+
{dates
246+
?.map((date) => date && dateFormatter.format(date))
247+
.filter(Boolean)
248+
.join(', ')}
249+
</Text>
250+
</Row>
230251
</View>
231252
<Enter />
232253
<Enter />
@@ -240,6 +261,15 @@ function App({
240261
Pick single date
241262
</Button>
242263
<View style={styles.buttonSeparator} />
264+
<Button
265+
onPress={() => setMultiOpen(true)}
266+
uppercase={false}
267+
mode="outlined"
268+
style={styles.pickButton}
269+
>
270+
Pick multiple dates
271+
</Button>
272+
<View style={styles.buttonSeparator} />
243273
<Button
244274
onPress={() => setRangeOpen(true)}
245275
uppercase={false}
@@ -340,6 +370,19 @@ function App({
340370
// animationType="slide" // optional, default is 'slide' on ios/android and 'none' on web
341371
/>
342372

373+
<DatePickerModal
374+
// locale={'en'} optional, default: automatic
375+
mode="multi"
376+
visible={multiOpen}
377+
onDismiss={onDismissMulti}
378+
dates={dates}
379+
onConfirm={onChangeMulti}
380+
// onChange={onChangeMulti}
381+
// saveLabel="Save" // optional
382+
// label="Select date" // optional
383+
// animationType="slide" // optional, default is 'slide' on ios/android and 'none' on web
384+
/>
385+
343386
<TimePickerModal
344387
locale={'nl'} //optional, default: automatic
345388
visible={timeOpen}

src/Date/Calendar.tsx

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import Color from 'color'
1818
import { useTheme } from 'react-native-paper'
1919
import { useLatest } from '../utils'
2020

21-
export type ModeType = 'single' | 'range' | 'excludeInRange'
21+
export type ModeType = 'single' | 'range' | 'excludeInRange' | 'multi'
2222

2323
export type ScrollModeType = 'horizontal' | 'vertical'
2424

@@ -38,6 +38,14 @@ export type RangeChange = (params: {
3838

3939
export type SingleChange = (params: { date: CalendarDate }) => any
4040

41+
export type MultiChange = (params: {
42+
dates: CalendarDate[]
43+
datePressed: CalendarDate
44+
change: 'added' | 'removed'
45+
}) => any
46+
47+
export type MultiConfirm = (params: { dates: CalendarDate[] }) => any
48+
4149
export interface CalendarSingleProps extends BaseCalendarProps {
4250
mode: 'single'
4351
date?: CalendarDate
@@ -59,8 +67,18 @@ export interface CalendarExcludeInRangeProps extends BaseCalendarProps {
5967
onChange: ExcludeInRangeChange
6068
}
6169

70+
export interface CalendarMultiProps extends BaseCalendarProps {
71+
mode: 'multi'
72+
dates?: CalendarDate[]
73+
onChange: MultiChange
74+
}
75+
6276
function Calendar(
63-
props: CalendarSingleProps | CalendarRangeProps | CalendarExcludeInRangeProps
77+
props:
78+
| CalendarSingleProps
79+
| CalendarRangeProps
80+
| CalendarExcludeInRangeProps
81+
| CalendarMultiProps
6482
) {
6583
const {
6684
locale,
@@ -75,6 +93,8 @@ function Calendar(
7593
// @ts-ignore
7694
excludedDates,
7795
disableWeekDays,
96+
// @ts-ignore
97+
dates,
7898
} = props
7999

80100
const theme = useTheme()
@@ -107,8 +127,9 @@ function Calendar(
107127
const excludedDatesRef = useLatest<Date[]>(excludedDates)
108128
const endDateRef = useLatest<CalendarDate>(endDate)
109129
const onChangeRef = useLatest<
110-
RangeChange | SingleChange | ExcludeInRangeChange
130+
RangeChange | SingleChange | ExcludeInRangeChange | MultiChange
111131
>(onChange)
132+
const datesRef = useLatest<Date[]>(dates)
112133

113134
const onPressDate = useCallback(
114135
(d: Date) => {
@@ -143,9 +164,23 @@ function Calendar(
143164
;(onChangeRef.current as ExcludeInRangeChange)({
144165
excludedDates: newExcludedDates,
145166
})
167+
} else if (mode === 'multi') {
168+
datesRef.current = datesRef.current || []
169+
const exists = datesRef.current.some((ed) => areDatesOnSameDay(ed, d))
170+
171+
const newDates = exists
172+
? datesRef.current.filter((ed) => !areDatesOnSameDay(ed, d))
173+
: [...datesRef.current, d]
174+
175+
newDates.sort((a, b) => a.getTime() - b.getTime())
176+
;(onChangeRef.current as MultiChange)({
177+
dates: newDates,
178+
datePressed: d,
179+
change: exists ? 'removed' : 'added',
180+
})
146181
}
147182
},
148-
[mode, onChangeRef, startDateRef, endDateRef, excludedDatesRef]
183+
[mode, onChangeRef, startDateRef, endDateRef, excludedDatesRef, datesRef]
149184
)
150185

151186
return (
@@ -163,6 +198,7 @@ function Calendar(
163198
startDate={startDate}
164199
endDate={endDate}
165200
date={date}
201+
dates={dates}
166202
onPressYear={onPressYear}
167203
selectingYear={selectingYear}
168204
onPressDate={onPressDate}

src/Date/DatePickerModal.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
import { useTheme } from 'react-native-paper'
1313
import DatePickerModalContent, {
1414
DatePickerModalContentExcludeInRangeProps,
15+
DatePickerModalContentMultiProps,
1516
DatePickerModalContentRangeProps,
1617
DatePickerModalContentSingleProps,
1718
} from './DatePickerModalContent'
@@ -30,6 +31,10 @@ interface DatePickerModalSingleProps
3031
extends DatePickerModalContentSingleProps,
3132
DatePickerModalProps {}
3233

34+
interface DatePickerModalMultiProps
35+
extends DatePickerModalContentMultiProps,
36+
DatePickerModalProps {}
37+
3338
interface DatePickerModalRangeProps
3439
extends DatePickerModalContentRangeProps,
3540
DatePickerModalProps {}
@@ -43,6 +48,7 @@ export function DatePickerModal(
4348
| DatePickerModalRangeProps
4449
| DatePickerModalSingleProps
4550
| DatePickerModalExcludeInRangeProps
51+
| DatePickerModalMultiProps
4652
) {
4753
const theme = useTheme()
4854
const dimensions = useWindowDimensions()

src/Date/DatePickerModalContent.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import Calendar, {
44
BaseCalendarProps,
55
CalendarDate,
66
ExcludeInRangeChange,
7+
MultiChange,
8+
MultiConfirm,
79
RangeChange,
810
SingleChange,
911
} from './Calendar'
@@ -22,6 +24,7 @@ export type LocalState = {
2224
endDate: CalendarDate
2325
date: CalendarDate
2426
excludedDates: Date[]
27+
dates: CalendarDate[]
2528
}
2629

2730
interface DatePickerModalContentBaseProps {
@@ -51,6 +54,16 @@ export interface DatePickerModalContentSingleProps
5154
onConfirm: SingleChange
5255
}
5356

57+
export interface DatePickerModalContentMultiProps
58+
extends HeaderPickProps,
59+
BaseCalendarProps,
60+
DatePickerModalContentBaseProps {
61+
mode: 'multi'
62+
dates?: Date[] | null | undefined
63+
onChange?: MultiChange
64+
onConfirm: MultiConfirm
65+
}
66+
5467
export interface DatePickerModalContentExcludeInRangeProps
5568
extends HeaderPickProps,
5669
BaseCalendarProps,
@@ -68,6 +81,7 @@ export function DatePickerModalContent(
6881
| DatePickerModalContentRangeProps
6982
| DatePickerModalContentSingleProps
7083
| DatePickerModalContentExcludeInRangeProps
84+
| DatePickerModalContentMultiProps
7185
) {
7286
const {
7387
mode,
@@ -87,6 +101,7 @@ export function DatePickerModalContent(
87101
startDate: anyProps.startDate,
88102
endDate: anyProps.endDate,
89103
excludedDates: anyProps.excludedDates,
104+
dates: anyProps.dates,
90105
})
91106

92107
// update local state if changed from outside or if modal is opened
@@ -96,12 +111,14 @@ export function DatePickerModalContent(
96111
startDate: anyProps.startDate,
97112
endDate: anyProps.endDate,
98113
excludedDates: anyProps.excludedDates,
114+
dates: anyProps.dates,
99115
})
100116
}, [
101117
anyProps.date,
102118
anyProps.startDate,
103119
anyProps.endDate,
104120
anyProps.excludedDates,
121+
anyProps.dates,
105122
])
106123

107124
const [collapsed, setCollapsed] = React.useState<boolean>(true)
@@ -128,6 +145,10 @@ export function DatePickerModalContent(
128145
;(onConfirm as DatePickerModalContentExcludeInRangeProps['onConfirm'])({
129146
excludedDates: state.excludedDates,
130147
})
148+
} else if (mode === 'multi') {
149+
;(onConfirm as DatePickerModalContentMultiProps['onConfirm'])({
150+
dates: state.dates || [],
151+
})
131152
}
132153
}, [state, mode, onConfirm])
133154

@@ -170,6 +191,7 @@ export function DatePickerModalContent(
170191
excludedDates={state.excludedDates}
171192
onChange={onInnerChange}
172193
disableWeekDays={disableWeekDays}
194+
dates={state.dates}
173195
/>
174196
}
175197
calendarEdit={

src/Date/DatePickerModalContentHeader.tsx

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ function getLabel(mode: ModeType, configuredLabel?: string) {
3636
if (mode === 'excludeInRange') {
3737
return 'Select excluded dates'
3838
}
39+
if (mode === 'multi') {
40+
return 'Select dates'
41+
}
3942
return '...?'
4043
}
4144

@@ -61,10 +64,13 @@ export default function DatePickerModalHeader(props: HeaderContentProps) {
6164
{mode === 'excludeInRange' ? (
6265
<HeaderContentExcludeInRange {...props} color={color} />
6366
) : null}
67+
{mode === 'multi' ? (
68+
<HeaderContentMulti {...props} color={color} />
69+
) : null}
6470
</View>
6571
</View>
6672
<View style={styles.fill} />
67-
{mode !== 'excludeInRange' ? (
73+
{mode !== 'excludeInRange' && mode !== 'multi' ? (
6874
<IconButton
6975
icon={collapsed ? 'pencil' : 'calendar'}
7076
color={color}
@@ -98,6 +104,37 @@ export function HeaderContentSingle({
98104
)
99105
}
100106

107+
export function HeaderContentMulti({
108+
state,
109+
emptyLabel = ' ',
110+
color,
111+
locale,
112+
}: HeaderContentProps & { color: string }) {
113+
const dateCount = state.dates?.length || 0
114+
const lighterColor = Color(color).fade(0.5).rgb().toString()
115+
const dateColor = dateCount ? color : lighterColor
116+
117+
const formatter = React.useMemo(() => {
118+
return new Intl.DateTimeFormat(locale, {
119+
month: 'short',
120+
day: 'numeric',
121+
})
122+
}, [locale])
123+
124+
let label = emptyLabel
125+
if (dateCount) {
126+
if (dateCount <= 2) {
127+
label = state.dates.map((date) => formatter.format(date)).join(', ')
128+
} else {
129+
label = formatter.format(state.dates[0]) + ` (+ ${dateCount - 1} more)`
130+
}
131+
}
132+
133+
return (
134+
<Text style={[styles.singleHeaderText, { color: dateColor }]}>{label}</Text>
135+
)
136+
}
137+
101138
export function HeaderContentExcludeInRange({
102139
state,
103140
emptyLabel = ' ',

0 commit comments

Comments
 (0)