uui/components/datePickers/RangeDatePickerBody.tsx (254 lines of code) (raw):

import React, { forwardRef, useState } from 'react'; import { Dayjs, uuiDayjs } from '../../helpers/dayJsHelper'; import { cx, IControlled } from '@epam/uui-core'; import { uuiDaySelection, Day, DayProps, RangeDatePickerPresets, } from '@epam/uui-components'; import { FlexCell, FlexRow } from '../layout'; import { CalendarPresets } from './CalendarPresets'; import css from './RangeDatePickerBody.module.scss'; import { defaultRangeValue, getMonthOnOpen, getWithFrom, getWithTo, uuiDatePickerBodyBase, valueFormat, } from './helpers'; import { CommonDatePickerBodyProps, RangeDatePickerInputType, RangeDatePickerValue, RangeDatePickerBodyValue, ViewType, } from './types'; import { StatelessDatePickerBody, StatelessDatePickerBodyValue } from './DatePickerBody'; export const uuiRangeDatePickerBody = { inRange: 'uui-range-datepicker-in-range', firstDayInRangeWrapper: 'uui-range-datepicker-first-day-in-range-wrapper', lastDayInRangeWrapper: 'uui-range-datepicker-last-day-in-range-wrapper', separator: 'uui-range-datepicker-separator', }; export const rangeDatePickerPresets: RangeDatePickerPresets = { today: { name: 'Today', getRange: () => ({ from: uuiDayjs.dayjs().toString(), to: undefined, order: 1, }), }, thisWeek: { name: 'This Week', getRange: () => ({ from: uuiDayjs.dayjs().startOf('isoWeek').toString(), to: uuiDayjs.dayjs().endOf('isoWeek').toString(), order: 2, }), }, lastWeek: { name: 'Last Week', getRange: () => ({ from: uuiDayjs.dayjs().startOf('isoWeek').subtract(1, 'week').toString(), to: uuiDayjs.dayjs().endOf('isoWeek').subtract(1, 'week').toString(), order: 3, }), }, thisMonth: { name: 'This Month', getRange: () => ({ from: uuiDayjs.dayjs().startOf('month').toString(), to: uuiDayjs.dayjs().endOf('month').toString(), order: 4, }), }, lastMonth: { name: 'Last Month', getRange: () => ({ from: uuiDayjs.dayjs().startOf('month').subtract(1, 'month').toString(), to: uuiDayjs.dayjs().subtract(1, 'month').endOf('month').toString(), order: 5, }), }, last3Month: { name: 'Last 3 Months', getRange: () => ({ from: uuiDayjs.dayjs().startOf('month').subtract(3, 'month').toString(), to: uuiDayjs.dayjs().subtract(1, 'month').endOf('month').toString(), order: 5, }), }, thisYear: { name: 'This Year', getRange: () => ({ from: uuiDayjs.dayjs().startOf('year').toString(), to: uuiDayjs.dayjs().endOf('year').toString(), order: 7, }), }, lastYear: { name: 'Last Year', getRange: () => ({ from: uuiDayjs.dayjs().startOf('year').subtract(1, 'year').toString(), to: uuiDayjs.dayjs().subtract(1, 'year').endOf('year').toString(), order: 8, }), }, }; export interface RangeDatePickerBodyProps<T> extends CommonDatePickerBodyProps, IControlled<RangeDatePickerBodyValue<T>> { renderFooter?(): React.ReactNode; isHoliday?: (day: Dayjs) => boolean; } export const RangeDatePickerBody = forwardRef(RangeDatePickerBodyComp); function RangeDatePickerBodyComp(props: RangeDatePickerBodyProps<RangeDatePickerValue | null>, ref: React.ForwardedRef<HTMLDivElement>): JSX.Element { const { value: _value, filter } = props; const { selectedDate: _selectedDate, inFocus, } = _value; const selectedDate = _selectedDate || defaultRangeValue; // also handles null in comparison to default value const [activeMonth, setActiveMonth] = useState<RangeDatePickerInputType>(inFocus); const [view, setView] = useState<ViewType>('DAY_SELECTION'); const [month, setMonth] = useState(() => { return getMonthOnOpen(selectedDate, inFocus); }); const getRange = (newValue: string | null) => { if (!filter || filter(uuiDayjs.dayjs(newValue))) { if (inFocus === 'from') { return getWithFrom(selectedDate, newValue); } if (inFocus === 'to') { return getWithTo(selectedDate, newValue); } } }; const onBodyValueChange = (v: string | null, part: 'from' | 'to') => { // selectedDate can be null, other params should always have values const newRange = v ? getRange(v) : selectedDate; let newInFocus: RangeDatePickerInputType = null; const fromChanged = selectedDate.from !== newRange?.from; const toChanged = selectedDate.to !== newRange?.to; if (inFocus === 'from' && fromChanged) { newInFocus = 'to'; } else if (inFocus === 'to' && toChanged) { newInFocus = 'from'; } setActiveMonth(part); props.onValueChange({ selectedDate: newRange ? newRange : selectedDate, inFocus: newInFocus ?? inFocus, }); }; const renderDay = (renderProps: DayProps): JSX.Element => { return ( <Day { ...renderProps } cx={ getDayCX(renderProps.value, selectedDate) } /> ); }; const from: StatelessDatePickerBodyValue<string> = { month, view: activeMonth === 'from' ? view : 'DAY_SELECTION', value: null, }; const to: StatelessDatePickerBodyValue<string> = { view: activeMonth === 'to' ? view : 'DAY_SELECTION', month: month.add(1, 'month'), value: null, }; const renderPresets = (presets: RangeDatePickerPresets) => { return ( <React.Fragment> <div className={ uuiRangeDatePickerBody.separator } /> <CalendarPresets onPresetSet={ (presetVal) => { // enable day if smth other were selected setView('DAY_SELECTION'); setMonth(uuiDayjs.dayjs(presetVal.from)); props.onValueChange({ inFocus: props.value.inFocus, selectedDate: { from: uuiDayjs.dayjs(presetVal.from).format(valueFormat), to: uuiDayjs.dayjs(presetVal.to).format(valueFormat), }, }); } } presets={ presets } /> </React.Fragment> ); }; return ( <div ref={ ref } className={ cx(css.root, uuiDatePickerBodyBase.container, props.cx) } { ...props.rawProps } > <FlexRow cx={ [view === 'DAY_SELECTION' && css.daySelection, css.container] } alignItems="top" > <FlexCell width="auto"> <FlexRow> <FlexRow cx={ css.bodesWrapper } alignItems="top" > <StatelessDatePickerBody key="date-picker-body-left" cx={ cx(css.fromPicker) } { ...from } onValueChange={ (v) => onBodyValueChange(v, 'from') } onMonthChange={ (m) => { setMonth(m); } } onViewChange={ (v) => setView(v) } filter={ props.filter } isHoliday={ props.isHoliday } renderDay={ props.renderDay || renderDay } isDisabled={ view !== 'DAY_SELECTION' && activeMonth === 'to' } /> <StatelessDatePickerBody key="date-picker-body-right" cx={ cx(css.toPicker) } { ...to } onValueChange={ (v) => onBodyValueChange(v, 'to') } onMonthChange={ (m) => { setMonth(m.subtract(1, 'month')); } } onViewChange={ (v) => setView(v) } filter={ props.filter } renderDay={ props.renderDay || renderDay } isHoliday={ props.isHoliday } isDisabled={ view !== 'DAY_SELECTION' && activeMonth === 'from' } /> {view !== 'DAY_SELECTION' && ( <div style={ { left: activeMonth === 'from' ? '50%' : undefined, right: activeMonth === 'to' ? '50%' : undefined, } } className={ css.blocker } /> )} </FlexRow> {props.presets && renderPresets(props.presets)} </FlexRow> {props.renderFooter && props.renderFooter()} </FlexCell> </FlexRow> </div> ); } const getDayCX = (day: Dayjs, selectedDate: RangeDatePickerValue): string[] => { const dayValue = day.valueOf(); const fromValue = selectedDate?.from ? uuiDayjs.dayjs(selectedDate.from).valueOf() : null; const toValue = selectedDate?.to ? uuiDayjs.dayjs(selectedDate.to).valueOf() : null; const inRange = fromValue && toValue && dayValue >= fromValue && dayValue <= toValue && fromValue !== toValue; const isFirst = dayValue === fromValue; const isLast = dayValue === toValue; return [cx( inRange && uuiRangeDatePickerBody.inRange, isFirst && uuiRangeDatePickerBody.firstDayInRangeWrapper, !inRange && isFirst && uuiRangeDatePickerBody.lastDayInRangeWrapper, isLast && uuiRangeDatePickerBody.lastDayInRangeWrapper, !inRange && isLast && uuiRangeDatePickerBody.firstDayInRangeWrapper, (dayValue === fromValue || dayValue === toValue) && uuiDaySelection.selectedDay, )]; };