UNPKG

@spaced-out/ui-design-system

Version:
385 lines (366 loc) 10.7 kB
// @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> ); }, );