@spaced-out/ui-design-system
Version:
Sense UI components library
215 lines (192 loc) • 5.88 kB
Flow
// @flow strict
import * as React from 'react';
import {isEmpty} from 'lodash';
// $FlowFixMe[untyped-import]
import moment from 'moment-timezone';
import type {
DateRange,
DateRangePickerError,
DateRangeWithTimezone,
} from '../../types';
import {
getAddedDate,
getMonthEndDate,
getSubtractedDate,
getValidDates,
MARKERS,
NAVIGATION_ACTION,
} from '../../utils';
import classify from '../../utils/classify';
import {
getTodayInTimezone,
isAfter,
isBefore,
isBetween,
isSame,
isSameOrAfter,
isSameOrBefore,
} from '../../utils/date-range-picker';
import {DateRangeWrapper} from './DateRangeWrapper';
import css from './DateRangePicker.module.css';
type ClassNames = $ReadOnly<{wrapper?: string}>;
export type DateRangePickerProps = {
classNames?: ClassNames,
selectedDateRange?: DateRangeWithTimezone,
onApply: (datePickerSelectedRange: DateRangeWithTimezone) => void,
onError?: (DateRangePickerError) => void,
onCancel: () => void,
hideTimezone?: boolean,
minDate?: string,
maxDate?: string,
startDateLabel?: string,
endDateLabel?: string,
t?: ?(key: string, fallback: string) => string,
locale?: string,
};
export const DateRangePicker: React$AbstractComponent<
DateRangePickerProps,
HTMLDivElement,
> = React.forwardRef<DateRangePickerProps, HTMLDivElement>(
(
{
minDate,
maxDate,
onApply,
onError,
onCancel,
classNames,
hideTimezone = false,
selectedDateRange = {},
startDateLabel,
endDateLabel,
t,
locale,
}: DateRangePickerProps,
ref,
): React.Node => {
const localTimezone = moment.tz.guess();
const {timezone: inputTimezone = ''} = selectedDateRange;
const validTimezone = isEmpty(inputTimezone)
? localTimezone
: inputTimezone;
const [today, setToday] = React.useState(() =>
getTodayInTimezone(validTimezone),
);
const {validMinDate, validMaxDate, validDateRange} = React.useMemo(
() =>
getValidDates({
selectedDateRange,
minDate,
maxDate,
today,
onError,
t,
}),
[today],
);
const {startDate, endDate} = validDateRange;
const validRangeEndMonth = endDate
? getMonthEndDate(endDate)
: getMonthEndDate(today);
const validRangeStartMonth =
startDate && endDate && !isSame(startDate, endDate, 'month')
? getMonthEndDate(startDate)
: getMonthEndDate(getSubtractedDate(validRangeEndMonth, 1, 'M'));
const [dateRange, setDateRange] = React.useState<DateRange>(validDateRange);
const [timezone, setTimezone] = React.useState<string>(validTimezone);
const [hoverDay, setHoverDay] = React.useState<string>('');
const [rangeStartMonth, setRangeStartMonth] =
React.useState<string>(validRangeStartMonth);
const [rangeEndMonth, setRangeEndMonth] =
React.useState<string>(validRangeEndMonth);
const setRangeStartMonthValidated = (date: string) => {
if (isSameOrAfter(date, validMinDate) && isBefore(date, rangeEndMonth)) {
setRangeStartMonth(date);
} else {
setRangeStartMonth(getMonthEndDate(validMinDate));
}
};
const setRangeEndMonthValidated = (date: string) => {
if (
isSameOrBefore(date, validMaxDate) &&
isAfter(date, rangeStartMonth)
) {
setRangeEndMonth(date);
} else {
setRangeEndMonth(getMonthEndDate(validMaxDate));
}
};
const onMonthNavigate = (
marker: $Values<typeof MARKERS>,
action: $Values<typeof NAVIGATION_ACTION>,
) => {
if (marker === MARKERS.DATE_RANGE_START) {
const newMonth =
action === NAVIGATION_ACTION.NEXT
? getMonthEndDate(getAddedDate(rangeStartMonth, 1, 'M'))
: getMonthEndDate(getSubtractedDate(rangeStartMonth, 1, 'M'));
setRangeStartMonthValidated(newMonth);
} else {
const newMonth =
action === NAVIGATION_ACTION.NEXT
? getMonthEndDate(getAddedDate(rangeEndMonth, 1, 'M'))
: getMonthEndDate(getSubtractedDate(rangeEndMonth, 1, 'M'));
setRangeEndMonthValidated(newMonth);
}
};
const onDayClick = (day: string) => {
const {startDate, endDate} = dateRange;
if (startDate && !endDate && isSameOrAfter(day, startDate)) {
setDateRange({startDate, endDate: day});
} else {
setDateRange({startDate: day});
}
setHoverDay(day);
};
const inHoverRange = (day: string): boolean => {
const {startDate, endDate} = dateRange;
return Boolean(
startDate &&
!endDate &&
hoverDay &&
isAfter(hoverDay, startDate) &&
isBetween(day, startDate, hoverDay),
);
};
const handlers = {
onDayClick,
onDayHover: setHoverDay,
onMonthNavigate,
};
const handleTimeZone = (newTimezone) => {
setToday(getTodayInTimezone(newTimezone));
setTimezone(newTimezone);
};
return (
<DateRangeWrapper
ref={ref}
minDate={validMinDate}
maxDate={validMaxDate}
onApply={onApply}
handlers={handlers}
hoverDay={hoverDay}
onCancel={onCancel}
timezone={timezone}
dateRange={dateRange}
setTimezone={handleTimeZone}
rangeStartMonth={rangeStartMonth}
rangeEndMonth={rangeEndMonth}
inHoverRange={inHoverRange}
setRangeStartMonth={setRangeStartMonthValidated}
setRangeEndMonth={setRangeEndMonthValidated}
hideTimezone={hideTimezone}
cardWrapperClass={classify(css.container, classNames?.wrapper)}
today={today}
startDateLabel={startDateLabel}
endDateLabel={endDateLabel}
t={t}
locale={locale}
/>
);
},
);