@spaced-out/ui-design-system
Version:
Sense UI components library
385 lines (366 loc) • 10.7 kB
Flow
// @flow strict
import * as React from 'react';
// $FlowFixMe[untyped-import]
import moment from 'moment';
import type {DateRange, DateRangeWithTimezone} from '../../types';
import {
generateAvailableYears,
getAvailableMonths,
getMonthEndDate,
getMonths,
getTimezones,
MARKERS,
NAVIGATION_ACTION,
} from '../../utils';
import classify from '../../utils/classify';
import {
addTimezoneEndOfDay,
addTimezoneStartOfDay,
getFormattedDate,
getTranslation,
isAfter,
isSameOrAfter,
isSameOrBefore,
TIMEZONES,
} from '../../utils/date-range-picker';
import {Button} from '../Button';
import {
Card,
CardActions,
CardContent,
CardFooter,
CardHeader,
CardTitle,
} from '../Card';
import {Dropdown} from '../Dropdown';
import {FocusManager} from '../FocusManager';
import {ClickableIcon, Icon, ICON_SIZE, ICON_TYPE} from '../Icon';
import {BodySmall, TEXT_COLORS} from '../Text';
import {Calendar} from './Calendar.js';
import css from './DateRangeWrapper.module.css';
type HeaderProps = {
date: string,
minDate: string,
maxDate: string,
rangeStartMonth: string,
rangeEndMonth: string,
nextDisabled: boolean,
prevDisabled: boolean,
onClickNext: () => void,
onClickPrevious: () => void,
marker: $Values<typeof MARKERS>,
setMonth: (date: string) => void,
t: ?(key: string, fallback: string) => string,
};
type DateRangeWrapperProps = {
dateRange: DateRange,
hoverDay: string,
minDate: string,
maxDate: string,
rangeStartMonth: string,
rangeEndMonth: string,
cardWrapperClass: ?string,
setRangeStartMonth: (string) => void,
setRangeEndMonth: (string) => void,
setTimezone: (string) => void,
timezone: string,
onApply: (datePickerSelectedRange: DateRangeWithTimezone) => void,
onCancel: () => void,
inHoverRange: (day: string) => boolean,
hideTimezone: boolean,
handlers: {
onDayClick: (day: string) => void,
onDayHover: (day: string) => void,
onMonthNavigate: (
marker: $Values<typeof MARKERS>,
action: $Values<typeof NAVIGATION_ACTION>,
) => void,
},
today: string,
startDateLabel?: string,
endDateLabel?: string,
t?: ?(key: string, fallback: string) => string,
locale?: string,
};
const CalendarHeader = ({
t,
date,
marker,
minDate,
maxDate,
setMonth,
rangeStartMonth,
rangeEndMonth,
onClickNext,
nextDisabled,
prevDisabled,
onClickPrevious,
}: HeaderProps): React.Node => {
const availableYears = generateAvailableYears({
marker,
minDate,
maxDate,
rangeStartMonth,
rangeEndMonth,
});
const availableMonths = getAvailableMonths({
marker,
minDate,
maxDate,
rangeStartMonth,
rangeEndMonth,
t,
});
const MONTHS = getMonths(t);
return (
<div className={css.calendarHeader}>
<ClickableIcon
ariaLabel={getTranslation(t, 'Select Previous Month')}
size="small"
name="chevron-left"
className={classify(css.headerIcon, {
[css.disabledIcon]: prevDisabled,
})}
onClick={() => !prevDisabled && onClickPrevious()}
color={prevDisabled ? 'disabled' : 'secondary'}
/>
<Dropdown
size="small"
disabled={!availableMonths.length || isAfter(minDate, maxDate)}
menu={{
selectedKeys: [moment.utc(date).month().toString()],
options: availableMonths,
}}
onChange={(event) => {
setMonth(getMonthEndDate(moment.utc(date).set('M', event.key)));
}}
dropdownInputText={MONTHS[moment.utc(date).month()].label}
scrollMenuToBottom
/>
<Dropdown
disabled={!availableYears.length || isAfter(minDate, maxDate)}
menu={{
selectedKeys: [moment.utc(date).year().toString()],
options: availableYears,
}}
size="small"
onChange={(event) => {
setMonth(getMonthEndDate(moment.utc(date).set('y', event.key)));
}}
dropdownInputText={moment.utc(date).year()}
scrollMenuToBottom
/>
<ClickableIcon
size="small"
ariaLabel={getTranslation(t, 'Select Next Month')}
name="chevron-right"
className={classify(css.headerIcon, {
[css.disabledIcon]: nextDisabled,
})}
onClick={() => !nextDisabled && onClickNext()}
color={nextDisabled ? 'disabled' : 'secondary'}
/>
</div>
);
};
export const DateRangeWrapper: React$AbstractComponent<
DateRangeWrapperProps,
HTMLDivElement,
> = React.forwardRef<DateRangeWrapperProps, HTMLDivElement>(
(
{
onApply,
onCancel,
handlers,
hoverDay,
minDate,
maxDate,
timezone,
dateRange,
rangeStartMonth,
setTimezone,
rangeEndMonth,
inHoverRange,
setRangeStartMonth,
setRangeEndMonth,
cardWrapperClass,
hideTimezone,
today,
startDateLabel,
endDateLabel,
t,
locale,
}: DateRangeWrapperProps,
ref,
): React.Node => {
const canNavigateCloser =
moment.utc(rangeStartMonth).year() !== moment.utc(rangeEndMonth).year() ||
Math.abs(
moment.utc(rangeStartMonth).month() - moment.utc(rangeEndMonth).month(),
) > 1;
const handleApplyClick = () => {
const {startDate, endDate} = dateRange;
const startDateUTC =
startDate && addTimezoneStartOfDay(startDate, timezone);
const endDateUTC = endDate && addTimezoneEndOfDay(endDate, timezone);
onApply({
startDate,
endDate,
startDateUTC,
endDateUTC,
timezone,
});
};
const {onMonthNavigate} = handlers;
const commonProps = {
inHoverRange,
handlers,
hoverDay,
dateRange,
minDate,
maxDate,
today,
t,
};
return (
<FocusManager>
<Card
classNames={{
wrapper: classify(css.dateRangeWrapper, cardWrapperClass),
}}
ref={ref}
>
<CardHeader className={css.cardHeader}>
<BodySmall
className={css.selectedDate}
color={TEXT_COLORS.secondary}
>
{`${
startDateLabel || getTranslation(t, 'Start Date')
}: ${getFormattedDate(
MARKERS.DATE_RANGE_START,
dateRange,
locale,
)}`}
</BodySmall>
<Icon
name="minus"
size={ICON_SIZE.small}
type={ICON_TYPE.regular}
color={TEXT_COLORS.secondary}
/>
<BodySmall
className={css.selectedDate}
color={TEXT_COLORS.secondary}
>
{`${
endDateLabel || getTranslation(t, 'End Date')
}: ${getFormattedDate(
MARKERS.DATE_RANGE_END,
dateRange,
locale,
)}`}
</BodySmall>
</CardHeader>
<div className={css.calendarMenuContainer}>
<CalendarHeader
t={t}
marker={MARKERS.DATE_RANGE_START}
rangeStartMonth={rangeStartMonth}
rangeEndMonth={rangeEndMonth}
date={rangeStartMonth}
setMonth={setRangeStartMonth}
nextDisabled={!canNavigateCloser}
prevDisabled={isSameOrBefore(rangeStartMonth, minDate, 'month')}
minDate={minDate}
maxDate={maxDate}
onClickNext={() =>
onMonthNavigate(
MARKERS.DATE_RANGE_START,
NAVIGATION_ACTION.NEXT,
)
}
onClickPrevious={() =>
onMonthNavigate(
MARKERS.DATE_RANGE_START,
NAVIGATION_ACTION.PREV,
)
}
/>
<CalendarHeader
t={t}
marker={MARKERS.DATE_RANGE_END}
rangeStartMonth={rangeStartMonth}
rangeEndMonth={rangeEndMonth}
date={rangeEndMonth}
setMonth={setRangeEndMonth}
nextDisabled={isSameOrAfter(rangeEndMonth, maxDate, 'month')}
prevDisabled={!canNavigateCloser}
minDate={minDate}
maxDate={maxDate}
onClickNext={() =>
onMonthNavigate(MARKERS.DATE_RANGE_END, NAVIGATION_ACTION.NEXT)
}
onClickPrevious={() =>
onMonthNavigate(MARKERS.DATE_RANGE_END, NAVIGATION_ACTION.PREV)
}
/>
</div>
<CardContent className={css.dateRangeCalendars}>
<Calendar
marker={MARKERS.DATE_RANGE_START}
value={rangeStartMonth}
{...commonProps}
/>
<div className={css.divider} />
<Calendar
marker={MARKERS.DATE_RANGE_END}
value={rangeEndMonth}
{...commonProps}
/>
</CardContent>
<CardFooter className={css.cardFooter}>
<CardTitle className={css.timezoneDropdownContainer}>
{!hideTimezone && (
<Dropdown
menu={{
selectedKeys: [timezone],
options: getTimezones(t),
allowSearch: true,
virtualization: {
enable: true,
},
staticLabels: {
SEARCH_PLACEHOLDER: `${getTranslation(t, 'Search')}...`,
RESULT: getTranslation(t, 'result'),
RESULTS: getTranslation(t, 'results'),
},
}}
dropdownInputText={getTranslation(t, TIMEZONES[timezone])}
disabled={isAfter(minDate, maxDate)}
classNames={{
box: css.timezoneDropdown,
}}
onChange={(event) => setTimezone(event.key)}
size="small"
/>
)}
</CardTitle>
<CardActions>
<Button type="ghost" onClick={onCancel} size="small">
{getTranslation(t, 'Cancel')}
</Button>
<Button
onClick={handleApplyClick}
size="small"
disabled={!(dateRange.startDate && dateRange.endDate)}
>
{getTranslation(t, 'Apply')}
</Button>
</CardActions>
</CardFooter>
</Card>
</FocusManager>
);
},
);