Skip to content

Commit

Permalink
feat(calendar-range): pass props to render calendar in popper (#702)
Browse files Browse the repository at this point in the history
* feat(calendar-range): pass props to render calendar in popper

* test: update test screenshot

* revert: revert update of test screenshot

* refactor(calendar-range): change interface for calendarPosition prop

* chore(calendar-range): add calendarPosition knob to component story

* chore(calendar-range): chanage knob to demo variant

* feat(calendar-range): fully adds popover variant

* test(calendar-range): adds tests for popover variant

* test(calendar-range): small fixes for popover variant tests

* test(calendar-range): fix typo in test

* refactor(calendar-range): fix import, interface, ts feature

* test(calendar-range): add tests for useCalendarMaxMinDates

Co-authored-by: Alexander Yatsenko <reme3d2y@gmail.com>
Co-authored-by: Dmitry Savkin <dmitrsavk@yandex.ru>
  • Loading branch information
3 people committed Jun 30, 2021
1 parent 882841c commit 4369e46
Show file tree
Hide file tree
Showing 9 changed files with 361 additions and 54 deletions.
10 changes: 8 additions & 2 deletions packages/calendar-input/src/Component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
CalendarProps,
dateInLimits,
} from '@alfalab/core-components-calendar';
import { Popover } from '@alfalab/core-components-popover';
import { Popover, PopoverProps } from '@alfalab/core-components-popover';
import mergeRefs from 'react-merge-refs';
import {
NATIVE_DATE_FORMAT,
Expand Down Expand Up @@ -136,6 +136,11 @@ export type CalendarInputProps = Omit<
* Идентификатор для систем автоматизированного тестирования
*/
dataTestId?: string;

/**
* Позиционирование поповера с календарем
*/
popoverPosition?: PopoverProps['position'];
};

export const CalendarInput = forwardRef<HTMLInputElement, CalendarInputProps>(
Expand Down Expand Up @@ -163,6 +168,7 @@ export const CalendarInput = forwardRef<HTMLInputElement, CalendarInputProps>(
onCalendarChange,
readOnly,
Calendar = DefaultCalendar,
popoverPosition = 'bottom-start',
...restProps
},
ref,
Expand Down Expand Up @@ -388,7 +394,7 @@ export const CalendarInput = forwardRef<HTMLInputElement, CalendarInputProps>(
open={open}
anchorElement={inputWrapperRef.current as HTMLElement}
popperClassName={styles.calendarContainer}
position='bottom-start'
position={popoverPosition}
offset={[0, 8]}
withTransition={false}
preventFlip={preventFlip}
Expand Down
7 changes: 7 additions & 0 deletions packages/calendar-range/src/Component.stories.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,14 @@ import { CalendarRange } from '@alfalab/core-components-calendar-range';
onBlur: handleToBlur,
onFocus: handleToFocus
}}
calendarPosition={select('calendarPosition', ['static', 'popover'], 'static')}
/>
);
})}
</Preview>

### Рендерит календари в поповере

<Preview>
<CalendarRange calendarPosition="popover"/>
</Preview>
56 changes: 55 additions & 1 deletion packages/calendar-range/src/Component.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { render, fireEvent, act } from '@testing-library/react';
import { render, waitFor, fireEvent, act } from '@testing-library/react';
import { setMonth, startOfMonth, addMonths, setDate, endOfMonth, startOfDay } from 'date-fns';
import { MONTHS } from '../../calendar/src/utils';
import { formatDate } from '../../calendar-input/src/utils';
Expand Down Expand Up @@ -196,6 +196,60 @@ describe('CalendarRange', () => {
expect(container).toHaveTextContent('Июнь');
});

describe('isPopover', () => {
it('should open empty input calendar with month from filled input', async () => {
const { queryAllByRole } = render(
<CalendarRange
inputFromProps={{ calendarProps: { className: 'from-calendar' } }}
inputToProps={{ calendarProps: { className: 'to-calendar' } }}
calendarPosition='popover'
/>,
);

const [inputFrom, inputTo] = queryAllByRole('textbox') as HTMLInputElement[];

await waitFor(() => {
fireEvent.focus(inputFrom);
expect(
document.querySelector('.from-calendar .button.month .buttonContent'),
).toHaveTextContent(currentMonthName);
});

await waitFor(() => {
fireEvent.focus(inputTo);
expect(
document.querySelector('.to-calendar .button.month .buttonContent'),
).toHaveTextContent(currentMonthName);
});
});

it('should fill "to input" when pick date in "to calendar"', async () => {
const { queryAllByRole } = render(
<CalendarRange
inputToProps={{ calendarProps: { className: 'to-calendar' } }}
calendarPosition='popover'
/>,
);

const [inputFrom, inputTo] = queryAllByRole('textbox') as HTMLInputElement[];

await waitFor(() => {
fireEvent.focus(inputTo);

expect(document.querySelector('.to-calendar')).toBeInTheDocument();
});

const days = document.querySelectorAll('*[data-date]');

act(() => {
(days[0] as HTMLButtonElement).click();
});

expect(inputTo.value).not.toBe('');
expect(inputFrom.value).toBe('');
});
});

describe('Period selection', () => {
it('should select day, fill inputFrom and start selection on first click', () => {
const { container, queryAllByRole } = render(<CalendarRange />);
Expand Down
146 changes: 96 additions & 50 deletions packages/calendar-range/src/Component.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
/* eslint-disable multiline-comment-style */
import React, { useCallback, useState, MouseEvent, FC } from 'react';
import cn from 'classnames';
import { startOfMonth, subMonths, addMonths, endOfMonth } from 'date-fns';
import { usePeriod, dateInLimits, limitDate } from '@alfalab/core-components-calendar';
import { startOfMonth, subMonths } from 'date-fns';
import { usePeriod, dateInLimits } from '@alfalab/core-components-calendar';
import {
CalendarInput,
CalendarInputProps,
Expand All @@ -12,6 +12,9 @@ import {
} from '@alfalab/core-components-calendar-input';
import { isDayButton, ValueState, getCorrectValueState, initialValueState } from './utils';

import { useCalendarMonthes } from './useCalendarMonthes';
import { useCalendarMaxMinDates } from './useCalendarMaxMinDates';

import styles from './index.module.css';

export type CalendarRangeProps = {
Expand Down Expand Up @@ -69,6 +72,11 @@ export type CalendarRangeProps = {
* Идентификатор для систем автоматизированного тестирования
*/
dataTestId?: string;

/**
* Определяет, как рендерить календарь — в поповере или снизу инпута
*/
calendarPosition?: 'static' | 'popover';
};

export const CalendarRange: FC<CalendarRangeProps> = ({
Expand All @@ -82,9 +90,11 @@ export const CalendarRange: FC<CalendarRangeProps> = ({
onDateToChange,
inputFromProps = {},
inputToProps = {},
calendarPosition = 'static',
dataTestId,
}) => {
const uncontrolled = valueFrom === undefined && valueTo === undefined;
const isPopover = calendarPosition === 'popover';

const period = usePeriod({
initialSelectedFrom: valueFrom ? parseDateString(valueFrom).getTime() : undefined,
Expand All @@ -97,16 +107,6 @@ export const CalendarRange: FC<CalendarRangeProps> = ({
if (!dateInLimits(selectedFrom, minDate, maxDate)) selectedFrom = undefined;
if (!dateInLimits(selectedTo, minDate, maxDate)) selectedTo = undefined;

const [nextMonthHighlighted, setNextMonthHighlighted] = useState(false);

const initialMonth =
uncontrolled || !valueFrom
? defaultMonth
: startOfMonth(parseDateString(valueFrom)).getTime();

const [month, setMonth] = useState(initialMonth);
const monthTo = addMonths(month, 1).getTime();

const [stateFrom, setStateFrom] = useState<ValueState>(initialValueState);
const [stateTo, setStateTo] = useState<ValueState>(initialValueState);

Expand All @@ -129,25 +129,34 @@ export const CalendarRange: FC<CalendarRangeProps> = ({
[onDateToChange, uncontrolled],
);

const { monthFrom, monthTo, handleMonthFromChange, handleMonthToChange } = useCalendarMonthes({
inputValueFrom,
inputValueTo,
defaultMonth,
isPopover,
});

const handleInputFromChange = useCallback<Required<CalendarInputProps>['onInputChange']>(
(_, { value, date }) => {
if (value === '') {
setStart(undefined);
handleStateFromChange(initialValueState);
}

if (isCompleteDateInput(value)) {
if (dateInLimits(date, minDate, maxDate)) {
setStart(date.getTime());
setMonth(startOfMonth(date).getTime());
handleStateFromChange({ date: date.getTime(), value });
} else {
setStart(undefined);
handleStateFromChange({ date: null, value });
}
if (!isCompleteDateInput(value)) {
return;
}

if (dateInLimits(date, minDate, maxDate)) {
setStart(date.getTime());
handleMonthFromChange(startOfMonth(date).getTime());
handleStateFromChange({ date: date.getTime(), value });
} else {
setStart(undefined);
handleStateFromChange({ date: null, value });
}
},
[handleStateFromChange, maxDate, minDate, setStart],
[handleMonthFromChange, handleStateFromChange, maxDate, minDate, setStart],
);

const handleInputToChange = useCallback<Required<CalendarInputProps>['onInputChange']>(
Expand All @@ -157,18 +166,20 @@ export const CalendarRange: FC<CalendarRangeProps> = ({
handleStateToChange(initialValueState);
}

if (isCompleteDateInput(value)) {
if (dateInLimits(date, minDate, maxDate)) {
setEnd(date.getTime());
setMonth(subMonths(startOfMonth(date), 1).getTime());
handleStateToChange({ date: date.getTime(), value });
} else {
setEnd(undefined);
handleStateToChange({ date: null, value });
}
if (!isCompleteDateInput(value)) {
return;
}

if (dateInLimits(date, minDate, maxDate)) {
setEnd(date.getTime());
handleMonthToChange(subMonths(startOfMonth(date), 1).getTime());
handleStateToChange({ date: date.getTime(), value });
} else {
setEnd(undefined);
handleStateToChange({ date: null, value });
}
},
[handleStateToChange, maxDate, minDate, setEnd],
[handleMonthToChange, handleStateToChange, maxDate, minDate, setEnd],
);

const handleCalendarChange = useCallback(
Expand Down Expand Up @@ -211,13 +222,35 @@ export const CalendarRange: FC<CalendarRangeProps> = ({
],
);

const handleMonthFromChange = useCallback((value: number) => {
setMonth(value);
}, []);
const handleFromCalendarChange = useCallback(
(date: number) => {
if (!isPopover) {
handleCalendarChange(date);

return;
}

setStart(date);
handleStateFromChange({ date, value: formatDate(date) });
},
[handleCalendarChange, handleStateFromChange, isPopover, setStart],
);

const handleToCalendarChange = useCallback(
(date: number) => {
if (!isPopover) {
handleCalendarChange(date);

const handleMonthToChange = useCallback((value: number) => {
setMonth(subMonths(value, 1).getTime());
}, []);
return;
}

handleStateToChange({ date, value: formatDate(date) });
setEnd(date);
},
[handleCalendarChange, handleStateToChange, isPopover, setEnd],
);

const [nextMonthHighlighted, setNextMonthHighlighted] = useState(false);

const handleCalendarToMouseOver = useCallback(
(event: MouseEvent<HTMLDivElement>) => {
Expand All @@ -231,23 +264,35 @@ export const CalendarRange: FC<CalendarRangeProps> = ({
[nextMonthHighlighted],
);

const selectorView = isPopover ? 'full' : 'month-only';
const calendarSelectedTo = selectedTo || (nextMonthHighlighted ? monthTo : undefined);
const maxMinDates = useCalendarMaxMinDates({
isPopover,
monthTo,
monthFrom,
selectedFrom,
selectedTo: calendarSelectedTo,
maxDate,
minDate,
});

return (
<div className={cn(styles.component, className)} data-test-id={dataTestId}>
<CalendarInput
{...inputFromProps}
calendarPosition='static'
calendarPosition={calendarPosition}
onInputChange={handleInputFromChange}
onCalendarChange={handleCalendarChange}
onCalendarChange={handleFromCalendarChange}
value={inputValueFrom.value}
minDate={minDate}
maxDate={limitDate(endOfMonth(month), minDate, maxDate).getTime()}
minDate={maxMinDates.fromMinDate}
maxDate={maxMinDates.fromMaxDate}
calendarProps={{
...inputFromProps.calendarProps,
month,
month: monthFrom,
onMonthChange: handleMonthFromChange,
selectorView: 'month-only',
selectorView,
selectedFrom,
selectedTo: selectedTo || (nextMonthHighlighted ? monthTo : undefined),
selectedTo: calendarSelectedTo,
}}
/>

Expand All @@ -257,17 +302,18 @@ export const CalendarRange: FC<CalendarRangeProps> = ({
<div onMouseOver={handleCalendarToMouseOver}>
<CalendarInput
{...inputToProps}
calendarPosition='static'
calendarPosition={calendarPosition}
popoverPosition='bottom-end'
onInputChange={handleInputToChange}
onCalendarChange={handleCalendarChange}
onCalendarChange={handleToCalendarChange}
value={inputValueTo.value}
minDate={limitDate(startOfMonth(monthTo), minDate, maxDate).getTime()}
maxDate={maxDate}
minDate={maxMinDates.toMinDate}
maxDate={maxMinDates.toMaxDate}
calendarProps={{
...inputToProps.calendarProps,
month: monthTo,
onMonthChange: handleMonthToChange,
selectorView: 'month-only',
selectorView,
selectedFrom,
selectedTo,
}}
Expand Down

0 comments on commit 4369e46

Please sign in to comment.