Add quick select ranges to date range picker (#17850)
* Add utils for preset date ranges * show the preset options in the calendar * allow ranges to be selected in storybook * add some more docs * add missing prop type * add tests * move story out of beta * fix last time periods * small refactors
This commit is contained in:
parent
bea711310e
commit
6c9a09c344
7 changed files with 786 additions and 101 deletions
|
|
@ -11,7 +11,8 @@ import {
|
|||
VERTICAL_ORIENTATION,
|
||||
HORIZONTAL_ORIENTATION,
|
||||
} from 'react-dates/constants';
|
||||
import { Icon } from '@crayons';
|
||||
import { getDateRangeStartAndEndDates, RANGE_LABELS } from './dateRangeUtils';
|
||||
import { Icon, ButtonNew as Button } from '@crayons';
|
||||
import ChevronLeft from '@images/chevron-left.svg';
|
||||
import ChevronRight from '@images/chevron-right.svg';
|
||||
import Calendar from '@images/calendar.svg';
|
||||
|
|
@ -32,6 +33,14 @@ const MONTH_NAMES = [...Array(12).keys()].map((key) =>
|
|||
const isDateOutsideOfRange = ({ date, minDate, maxDate }) =>
|
||||
!date.isBetween(minDate, maxDate);
|
||||
|
||||
/**
|
||||
* Renders select elements allowing a user to jump to a given month/year
|
||||
* @param {Object} earliestMoment Moment object representing the earliest permitted date
|
||||
* @param {Object} latestMoment Moment object representing the latest permitted date
|
||||
* @param {Function} onMonthSelect Callback passed by react-dates library
|
||||
* @param {Function} onYearSelect Callback passed by react-dates library
|
||||
* @param {Object} month Moment object passed by react-dates library, representing the currently visible calendar
|
||||
*/
|
||||
const MonthYearPicker = ({
|
||||
earliestMoment,
|
||||
latestMoment,
|
||||
|
|
@ -86,6 +95,63 @@ const MonthYearPicker = ({
|
|||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders preset date ranges as 'quick select' buttons, if the range falls within the permitted dates.
|
||||
* Possible preset ranges are defined in ./dateRangeUtils.js
|
||||
*
|
||||
* @param {[string]} presetRanges The preset range names requested
|
||||
* @param {Object} earliestMoment Moment object representing earliest permitted date
|
||||
* @param {Object} latestMoment Moment object representing latest permitted date
|
||||
* @param {Function} onPresetSelected Callback which will receive start and end dates of selected preset
|
||||
* @param {Object} today Moment object representing today's date
|
||||
*/
|
||||
const PresetDateRangeOptions = ({
|
||||
presetRanges = [],
|
||||
earliestMoment,
|
||||
latestMoment,
|
||||
onPresetSelected,
|
||||
today,
|
||||
}) => {
|
||||
// Filter out any requested ranges which extend beyond the valid time period
|
||||
const presetsWithinPermittedDates = presetRanges.filter((dateRangeName) => {
|
||||
const { start: rangeStart, end: rangeEnd } = getDateRangeStartAndEndDates({
|
||||
today,
|
||||
dateRangeName,
|
||||
});
|
||||
|
||||
return (
|
||||
rangeStart.isSameOrBefore(rangeEnd) &&
|
||||
rangeStart.isSameOrAfter(earliestMoment) &&
|
||||
rangeEnd.isSameOrBefore(latestMoment)
|
||||
);
|
||||
});
|
||||
|
||||
if (presetsWithinPermittedDates.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ul className="flex flex-wrap p-3">
|
||||
{presetsWithinPermittedDates.map((rangeName) => (
|
||||
<li key={`quick-select-${rangeName}`}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
onPresetSelected(
|
||||
getDateRangeStartAndEndDates({
|
||||
today,
|
||||
dateRangeName: rangeName,
|
||||
}),
|
||||
);
|
||||
}}
|
||||
>
|
||||
{RANGE_LABELS[rangeName]}
|
||||
</Button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Used to facilitate picking a date range. This component is a wrapper around the one provided from react-dates.
|
||||
*
|
||||
|
|
@ -97,6 +163,8 @@ const MonthYearPicker = ({
|
|||
* @param {Date} props.maxEndDate The latest date that may be selected
|
||||
* @param {Date} props.minStartDate The oldest date that may be selected
|
||||
* @param {Function} props.onDatesChanged Callback function for when dates are selected. Receives an object with startDate and endDate values.
|
||||
* @param {[string]} props.presetRanges Quick-select preset date ranges to offer in the calendar. These will only be shown if they fall within the min and max dates.
|
||||
* @param {Date} props.todaysDate Optional param to pass in today's Date, primarily for testing purposes
|
||||
*/
|
||||
export const DateRangePicker = ({
|
||||
startDateId,
|
||||
|
|
@ -106,6 +174,8 @@ export const DateRangePicker = ({
|
|||
maxEndDate = new Date(),
|
||||
minStartDate = new Date(),
|
||||
onDatesChanged,
|
||||
presetRanges = [],
|
||||
todaysDate = new Date(),
|
||||
}) => {
|
||||
const [focusedInput, setFocusedInput] = useState(START_DATE);
|
||||
const [startMoment, setStartMoment] = useState(
|
||||
|
|
@ -120,14 +190,15 @@ export const DateRangePicker = ({
|
|||
`(max-width: ${BREAKPOINTS.Medium - 1}px)`,
|
||||
);
|
||||
|
||||
const earliestMoment = moment(minStartDate);
|
||||
const latestMoment = moment(maxEndDate);
|
||||
const earliestMoment = moment(minStartDate).startOf('day');
|
||||
const latestMoment = moment(maxEndDate).endOf('day');
|
||||
|
||||
const isMonthSameAsLatestMonth = (relevantDate) =>
|
||||
relevantDate.year() === latestMoment.year() &&
|
||||
relevantDate.month() === latestMoment.month();
|
||||
|
||||
const today = moment();
|
||||
const today = moment(todaysDate);
|
||||
|
||||
const dateFormat =
|
||||
getCurrentLocale().toLowerCase() === 'en-us' ? 'MM/DD/YYYY' : 'DD/MM/YYYY';
|
||||
|
||||
|
|
@ -137,8 +208,10 @@ export const DateRangePicker = ({
|
|||
<ReactDateRangePicker
|
||||
startDateId={startDateId}
|
||||
startDate={startMoment}
|
||||
startDateAriaLabel={`Start date (${dateFormat})`}
|
||||
endDate={endMoment}
|
||||
endDateId={endDateId}
|
||||
endDateAriaLabel={`End date (${dateFormat})`}
|
||||
startDatePlaceholderText={dateFormat}
|
||||
endDatePlaceholderText={dateFormat}
|
||||
displayFormat={dateFormat}
|
||||
|
|
@ -150,11 +223,7 @@ export const DateRangePicker = ({
|
|||
minDate={earliestMoment}
|
||||
maxDate={latestMoment}
|
||||
initialVisibleMonth={() => {
|
||||
let relevantDate = startMoment;
|
||||
|
||||
if (!relevantDate) {
|
||||
relevantDate = latestMoment ? latestMoment : today;
|
||||
}
|
||||
const relevantDate = startMoment ? startMoment : today;
|
||||
|
||||
return isMonthSameAsLatestMonth(relevantDate)
|
||||
? relevantDate.clone().subtract(1, 'month')
|
||||
|
|
@ -181,8 +250,8 @@ export const DateRangePicker = ({
|
|||
setStartMoment(startDate);
|
||||
setEndMoment(endDate);
|
||||
onDatesChanged?.({
|
||||
startDate: startDate.toDate(),
|
||||
endDate: endDate.toDate(),
|
||||
startDate: startDate?.toDate(),
|
||||
endDate: endDate?.toDate(),
|
||||
});
|
||||
}}
|
||||
small={useCompactLayout}
|
||||
|
|
@ -193,6 +262,20 @@ export const DateRangePicker = ({
|
|||
{...props}
|
||||
/>
|
||||
)}
|
||||
renderCalendarInfo={() => (
|
||||
<PresetDateRangeOptions
|
||||
presetRanges={presetRanges}
|
||||
earliestMoment={earliestMoment}
|
||||
latestMoment={latestMoment}
|
||||
today={today}
|
||||
onPresetSelected={({ start, end }) => {
|
||||
setStartMoment(start);
|
||||
setEndMoment(end);
|
||||
// Force the calendar to close, same as if user clicks an end date manually
|
||||
setFocusedInput(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
|
|
@ -206,4 +289,5 @@ DateRangePicker.propTypes = {
|
|||
maxStartDate: PropTypes.instanceOf(Date),
|
||||
maxEndDate: PropTypes.instanceOf(Date),
|
||||
onDatesChanged: PropTypes.func,
|
||||
presetRanges: PropTypes.arrayOf(PropTypes.string),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,9 +1,17 @@
|
|||
import { h } from 'preact';
|
||||
import {
|
||||
LAST_FULL_MONTH,
|
||||
LAST_FULL_QUARTER,
|
||||
LAST_FULL_YEAR,
|
||||
QUARTER_UNTIL_TODAY,
|
||||
YEAR_UNTIL_TODAY,
|
||||
MONTH_UNTIL_TODAY,
|
||||
} from '../dateRangeUtils';
|
||||
import { DateRangePicker } from '@crayons';
|
||||
|
||||
export default {
|
||||
component: DateRangePicker,
|
||||
title: 'BETA/DateRangePicker',
|
||||
title: 'Components/Form Elements/DateRangePicker',
|
||||
argTypes: {
|
||||
startDateId: {
|
||||
description: 'A unique identifier for the start date input (required)',
|
||||
|
|
@ -33,6 +41,19 @@ export default {
|
|||
description:
|
||||
'A callback function for when dates are selected. It receives an object with startDate and endDate values.',
|
||||
},
|
||||
presetRanges: {
|
||||
description:
|
||||
'Quick select buttons to display. These will only be displayed if they fall within the max and min date range set.',
|
||||
control: 'check',
|
||||
options: [
|
||||
LAST_FULL_MONTH,
|
||||
LAST_FULL_QUARTER,
|
||||
LAST_FULL_YEAR,
|
||||
MONTH_UNTIL_TODAY,
|
||||
QUARTER_UNTIL_TODAY,
|
||||
YEAR_UNTIL_TODAY,
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -47,4 +68,26 @@ Default.args = {
|
|||
defaultEndDate: undefined,
|
||||
minStartDate: new Date(2020, 0, 1),
|
||||
maxEndDate: new Date(),
|
||||
presetRanges: [],
|
||||
};
|
||||
|
||||
export const DateRangePickerWithPresetRanges = (args) => {
|
||||
return <DateRangePicker {...args} />;
|
||||
};
|
||||
|
||||
DateRangePickerWithPresetRanges.args = {
|
||||
startDateId: 'start-date',
|
||||
endDateId: 'end-date',
|
||||
defaultStartDate: undefined,
|
||||
defaultEndDate: undefined,
|
||||
minStartDate: new Date(2020, 0, 1),
|
||||
maxEndDate: new Date(),
|
||||
presetRanges: [
|
||||
MONTH_UNTIL_TODAY,
|
||||
LAST_FULL_MONTH,
|
||||
QUARTER_UNTIL_TODAY,
|
||||
LAST_FULL_QUARTER,
|
||||
YEAR_UNTIL_TODAY,
|
||||
LAST_FULL_YEAR,
|
||||
],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,89 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import { render, waitFor } from '@testing-library/preact';
|
||||
import { DateRangePicker } from '@crayons';
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
const windowNavigator = window.navigator;
|
||||
|
||||
describe('<DateRangePicker />', () => {
|
||||
beforeAll(() => {
|
||||
global.window.matchMedia = jest.fn((query) => {
|
||||
return {
|
||||
matches: false,
|
||||
media: query,
|
||||
addListener: jest.fn(),
|
||||
removeListener: jest.fn(),
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
describe('Localization', () => {
|
||||
afterAll(() => {
|
||||
Object.defineProperty(window, 'navigator', {
|
||||
value: windowNavigator,
|
||||
writable: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('localizes for en-US date format', async () => {
|
||||
Object.defineProperty(window, 'navigator', {
|
||||
value: { language: 'en-US' },
|
||||
writable: true,
|
||||
});
|
||||
|
||||
const { getAllByPlaceholderText, getByRole, getByDisplayValue } = render(
|
||||
<DateRangePicker
|
||||
startDateId="start-date"
|
||||
endDateId="end-date"
|
||||
minStartDate={new Date(2020, 0, 1)}
|
||||
maxEndDate={new Date(2020, 0, 31)}
|
||||
/>,
|
||||
);
|
||||
|
||||
const inputs = getAllByPlaceholderText('MM/DD/YYYY');
|
||||
expect(inputs).toHaveLength(2);
|
||||
|
||||
getByRole('button', {
|
||||
name: 'Interact with the calendar and add your start date',
|
||||
}).click();
|
||||
|
||||
getByRole('button', {
|
||||
name: 'Choose Wednesday, January 22, 2020 as start date',
|
||||
}).click();
|
||||
|
||||
await waitFor(() =>
|
||||
expect(getByDisplayValue('01/22/2020')).toBeInTheDocument(),
|
||||
);
|
||||
});
|
||||
|
||||
it('localizes for non en-US format', async () => {
|
||||
Object.defineProperty(window, 'navigator', {
|
||||
value: { language: 'en' },
|
||||
writable: true,
|
||||
});
|
||||
|
||||
const { getAllByPlaceholderText, getByRole, getByDisplayValue } = render(
|
||||
<DateRangePicker
|
||||
startDateId="start-date"
|
||||
endDateId="end-date"
|
||||
minStartDate={new Date(2020, 0, 1)}
|
||||
maxEndDate={new Date(2020, 0, 31)}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(getAllByPlaceholderText('DD/MM/YYYY')).toHaveLength(2);
|
||||
|
||||
getByRole('button', {
|
||||
name: 'Interact with the calendar and add your start date',
|
||||
}).click();
|
||||
|
||||
getByRole('button', {
|
||||
name: 'Choose Wednesday, January 22, 2020 as start date',
|
||||
}).click();
|
||||
|
||||
await waitFor(() =>
|
||||
expect(getByDisplayValue('22/01/2020')).toBeInTheDocument(),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,370 @@
|
|||
import { h } from 'preact';
|
||||
import { render, waitFor, within } from '@testing-library/preact';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import {
|
||||
DateRangePicker,
|
||||
MONTH_UNTIL_TODAY,
|
||||
QUARTER_UNTIL_TODAY,
|
||||
YEAR_UNTIL_TODAY,
|
||||
LAST_FULL_MONTH,
|
||||
LAST_FULL_QUARTER,
|
||||
LAST_FULL_YEAR,
|
||||
} from '@crayons';
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
const windowNavigator = window.navigator;
|
||||
|
||||
describe('<DateRangePicker />', () => {
|
||||
const todayMock = new Date('2022-01-25');
|
||||
|
||||
beforeAll(() => {
|
||||
global.window.matchMedia = jest.fn((query) => {
|
||||
return {
|
||||
matches: false,
|
||||
media: query,
|
||||
addListener: jest.fn(),
|
||||
removeListener: jest.fn(),
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
it('renders without default start and end dates', () => {
|
||||
const { getByRole, getAllByRole } = render(
|
||||
<DateRangePicker
|
||||
startDateId="start-date"
|
||||
endDateId="end-date"
|
||||
todaysDate={todayMock}
|
||||
minStartDate={new Date('2021-01-01')}
|
||||
maxEndDate={new Date('2022-06-01')}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(
|
||||
getByRole('textbox', {
|
||||
name: 'Start date (MM/DD/YYYY)',
|
||||
}),
|
||||
).toHaveValue('');
|
||||
|
||||
expect(getByRole('textbox', { name: 'End date (MM/DD/YYYY)' })).toHaveValue(
|
||||
'',
|
||||
);
|
||||
|
||||
// react-dates renders a hidden (by CSS) month/year picker for off screen previous and next month views
|
||||
// testing library doesn't load the CSS, so we have to "skip over" the first matching select to get the correct visible one
|
||||
const monthPickers = getAllByRole('combobox', {
|
||||
name: 'Navigate to month',
|
||||
});
|
||||
expect(monthPickers).toHaveLength(4);
|
||||
expect(monthPickers[1]).toHaveDisplayValue('January');
|
||||
expect(monthPickers[2]).toHaveDisplayValue('February');
|
||||
});
|
||||
|
||||
it('renders with a default start date', () => {
|
||||
const { getByRole, getAllByRole } = render(
|
||||
<DateRangePicker
|
||||
startDateId="start-date"
|
||||
endDateId="end-date"
|
||||
defaultStartDate={new Date('2022-01-01')}
|
||||
minStartDate={new Date('2021-01-01')}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(
|
||||
getByRole('textbox', {
|
||||
name: 'Start date (MM/DD/YYYY)',
|
||||
}),
|
||||
).toHaveValue('01/01/2022');
|
||||
|
||||
const monthPickers = getAllByRole('combobox', {
|
||||
name: 'Navigate to month',
|
||||
});
|
||||
// react-dates renders a hidden (by CSS) month/year picker for off screen previous and next month views
|
||||
// testing library doesn't load the CSS, so we have to "skip over" the first matching select to get the correct visible one
|
||||
expect(monthPickers).toHaveLength(4);
|
||||
expect(monthPickers[1]).toHaveDisplayValue('January');
|
||||
expect(monthPickers[2]).toHaveDisplayValue('February');
|
||||
|
||||
const yearPickers = getAllByRole('combobox', { name: 'Navigate to year' });
|
||||
expect(yearPickers).toHaveLength(4);
|
||||
|
||||
expect(yearPickers[1]).toHaveDisplayValue('2022');
|
||||
expect(yearPickers[2]).toHaveDisplayValue('2022');
|
||||
});
|
||||
|
||||
it('renders with a default end date', () => {
|
||||
const { getByRole } = render(
|
||||
<DateRangePicker
|
||||
startDateId="start-date"
|
||||
endDateId="end-date"
|
||||
defaultEndDate={new Date('2022-01-01')}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(
|
||||
getByRole('textbox', {
|
||||
name: 'End date (MM/DD/YYYY)',
|
||||
}),
|
||||
).toHaveValue('01/01/2022');
|
||||
});
|
||||
|
||||
it('calls onDatesChanged when dates are selected', async () => {
|
||||
const onDatesChangedSpy = jest.fn();
|
||||
const { getByRole } = render(
|
||||
<DateRangePicker
|
||||
startDateId="start-date"
|
||||
endDateId="end-date"
|
||||
todaysDate={todayMock}
|
||||
minStartDate={new Date('2022-01-01')}
|
||||
maxEndDate={new Date('2022-01-31')}
|
||||
onDatesChanged={onDatesChangedSpy}
|
||||
/>,
|
||||
);
|
||||
|
||||
const startDateInput = getByRole('textbox', {
|
||||
name: 'Start date (MM/DD/YYYY)',
|
||||
});
|
||||
const endDateInput = getByRole('textbox', {
|
||||
name: 'End date (MM/DD/YYYY)',
|
||||
});
|
||||
|
||||
userEvent.type(startDateInput, '01/25/2022');
|
||||
|
||||
await waitFor(() =>
|
||||
expect(startDateInput).toHaveDisplayValue('01/25/2022'),
|
||||
);
|
||||
userEvent.type(endDateInput, '01/26/2022');
|
||||
await waitFor(() => expect(endDateInput).toHaveDisplayValue('01/26/2022'));
|
||||
|
||||
// Called once for every keystroke
|
||||
await waitFor(() => expect(onDatesChangedSpy).toHaveBeenCalledTimes(20));
|
||||
const { startDate, endDate } = onDatesChangedSpy.mock.calls[19][0];
|
||||
|
||||
expect(startDate.getDate()).toEqual(25);
|
||||
expect(startDate.getMonth()).toEqual(0);
|
||||
expect(startDate.getFullYear()).toEqual(2022);
|
||||
|
||||
expect(endDate.getDate()).toEqual(26);
|
||||
expect(endDate.getMonth()).toEqual(0);
|
||||
expect(endDate.getFullYear()).toEqual(2022);
|
||||
});
|
||||
|
||||
it('skips to a selected year', async () => {
|
||||
const { getByRole, getAllByRole } = render(
|
||||
<DateRangePicker
|
||||
startDateId="start-date"
|
||||
endDateId="end-date"
|
||||
todaysDate={todayMock}
|
||||
minStartDate={new Date('2020-01-01')}
|
||||
maxEndDate={new Date('2022-06-02')}
|
||||
/>,
|
||||
);
|
||||
|
||||
// react-dates renders a hidden (by CSS) month/year picker for off screen previous and next month views
|
||||
// testing library doesn't load the CSS, so we have to "skip over" the first matching select to get the correct visible one
|
||||
const startYearPicker = getAllByRole('combobox', {
|
||||
name: 'Navigate to year',
|
||||
})[1];
|
||||
expect(startYearPicker).toHaveDisplayValue('2022');
|
||||
userEvent.selectOptions(
|
||||
startYearPicker,
|
||||
within(startYearPicker).getByRole('option', { name: '2021' }),
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
getByRole('button', {
|
||||
name: 'Choose Monday, January 25, 2021 as start date',
|
||||
}),
|
||||
).toBeInTheDocument(),
|
||||
);
|
||||
});
|
||||
|
||||
it('skips to a selected month', async () => {
|
||||
const { getByRole, getAllByRole } = render(
|
||||
<DateRangePicker
|
||||
startDateId="start-date"
|
||||
endDateId="end-date"
|
||||
todaysDate={todayMock}
|
||||
minStartDate={new Date('2021-01-01')}
|
||||
maxEndDate={new Date('2022-06-01')}
|
||||
/>,
|
||||
);
|
||||
|
||||
// react-dates renders a hidden (by CSS) month/year picker for off screen previous and next month views
|
||||
// testing library doesn't load the CSS, so we have to "skip over" the first matching select to get the correct visible one
|
||||
const startMonthPicker = getAllByRole('combobox', {
|
||||
name: 'Navigate to month',
|
||||
})[1];
|
||||
expect(startMonthPicker).toHaveDisplayValue('January');
|
||||
userEvent.selectOptions(startMonthPicker, 'February');
|
||||
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
getByRole('button', {
|
||||
name: 'Choose Wednesday, February 2, 2022 as start date',
|
||||
}),
|
||||
).toBeInTheDocument(),
|
||||
);
|
||||
});
|
||||
|
||||
it('disables navigation button if previous/next month is outside permitted dates', () => {
|
||||
const { getByRole } = render(
|
||||
<DateRangePicker
|
||||
startDateId="start-date"
|
||||
endDateId="end-date"
|
||||
todaysDate={todayMock}
|
||||
minStartDate={new Date('2022-01-01')}
|
||||
maxEndDate={new Date('2022-01-31')}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(
|
||||
getByRole('button', {
|
||||
name: 'Move forward to switch to the next month.',
|
||||
}),
|
||||
).toHaveAttribute('aria-disabled', 'true');
|
||||
|
||||
expect(
|
||||
getByRole('button', {
|
||||
name: 'Move backward to switch to the previous month.',
|
||||
}),
|
||||
).toHaveAttribute('aria-disabled', 'true');
|
||||
});
|
||||
|
||||
it('displays preset range buttons', () => {
|
||||
const { getByRole } = render(
|
||||
<DateRangePicker
|
||||
startDateId="start-date"
|
||||
endDateId="end-date"
|
||||
todaysDate={todayMock}
|
||||
minStartDate={new Date('2020-01-01')}
|
||||
maxEndDate={todayMock}
|
||||
presetRanges={[
|
||||
MONTH_UNTIL_TODAY,
|
||||
QUARTER_UNTIL_TODAY,
|
||||
YEAR_UNTIL_TODAY,
|
||||
LAST_FULL_MONTH,
|
||||
LAST_FULL_QUARTER,
|
||||
LAST_FULL_YEAR,
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
expect(getByRole('button', { name: 'This month' })).toBeInTheDocument();
|
||||
expect(getByRole('button', { name: 'This quarter' })).toBeInTheDocument();
|
||||
expect(getByRole('button', { name: 'This year' })).toBeInTheDocument();
|
||||
expect(getByRole('button', { name: 'Last month' })).toBeInTheDocument();
|
||||
expect(getByRole('button', { name: 'Last quarter' })).toBeInTheDocument();
|
||||
expect(getByRole('button', { name: 'Last year' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not display preset range buttons if outside permitted dates', () => {
|
||||
const { getByRole, queryByRole } = render(
|
||||
<DateRangePicker
|
||||
startDateId="start-date"
|
||||
endDateId="end-date"
|
||||
todaysDate={todayMock}
|
||||
minStartDate={new Date('2022-01-01')}
|
||||
maxEndDate={todayMock}
|
||||
presetRanges={[MONTH_UNTIL_TODAY, LAST_FULL_YEAR]}
|
||||
/>,
|
||||
);
|
||||
expect(getByRole('button', { name: 'This month' })).toBeInTheDocument();
|
||||
expect(queryByRole('button', { name: 'Last year' })).toBeNull();
|
||||
});
|
||||
|
||||
it('selects a preset range', async () => {
|
||||
const { getByRole } = render(
|
||||
<DateRangePicker
|
||||
startDateId="start-date"
|
||||
endDateId="end-date"
|
||||
todaysDate={todayMock}
|
||||
minStartDate={new Date('2022-01-01')}
|
||||
maxEndDate={todayMock}
|
||||
presetRanges={[MONTH_UNTIL_TODAY]}
|
||||
/>,
|
||||
);
|
||||
|
||||
getByRole('button', { name: 'This month' }).click();
|
||||
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
getByRole('textbox', { name: 'Start date (MM/DD/YYYY)' }),
|
||||
).toHaveDisplayValue('01/01/2022'),
|
||||
);
|
||||
expect(
|
||||
getByRole('textbox', { name: 'End date (MM/DD/YYYY)' }),
|
||||
).toHaveDisplayValue('01/25/2022');
|
||||
});
|
||||
|
||||
describe('Localization', () => {
|
||||
afterAll(() => {
|
||||
Object.defineProperty(window, 'navigator', {
|
||||
value: windowNavigator,
|
||||
writable: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('localizes for en-US date format', async () => {
|
||||
Object.defineProperty(window, 'navigator', {
|
||||
value: { language: 'en-US' },
|
||||
writable: true,
|
||||
});
|
||||
|
||||
const { getAllByPlaceholderText, getByRole, getByDisplayValue } = render(
|
||||
<DateRangePicker
|
||||
startDateId="start-date"
|
||||
endDateId="end-date"
|
||||
minStartDate={new Date('2020-01-01')}
|
||||
maxEndDate={new Date('2020-01-31')}
|
||||
todaysDate={new Date('2020-01-31')}
|
||||
/>,
|
||||
);
|
||||
|
||||
const inputs = getAllByPlaceholderText('MM/DD/YYYY');
|
||||
expect(inputs).toHaveLength(2);
|
||||
|
||||
getByRole('button', {
|
||||
name: 'Interact with the calendar and add your start date',
|
||||
}).click();
|
||||
|
||||
getByRole('button', {
|
||||
name: 'Choose Wednesday, January 22, 2020 as start date',
|
||||
}).click();
|
||||
|
||||
await waitFor(() =>
|
||||
expect(getByDisplayValue('01/22/2020')).toBeInTheDocument(),
|
||||
);
|
||||
});
|
||||
|
||||
it('localizes for non en-US format', async () => {
|
||||
Object.defineProperty(window, 'navigator', {
|
||||
value: { language: 'en' },
|
||||
writable: true,
|
||||
});
|
||||
|
||||
const { getAllByPlaceholderText, getByRole, getByDisplayValue } = render(
|
||||
<DateRangePicker
|
||||
startDateId="start-date"
|
||||
endDateId="end-date"
|
||||
minStartDate={new Date('2020-01-01')}
|
||||
maxEndDate={new Date('2020-01-31')}
|
||||
todaysDate={new Date('2020-01-31')}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(getAllByPlaceholderText('DD/MM/YYYY')).toHaveLength(2);
|
||||
|
||||
getByRole('button', {
|
||||
name: 'Interact with the calendar and add your start date',
|
||||
}).click();
|
||||
|
||||
getByRole('button', {
|
||||
name: 'Choose Wednesday, January 22, 2020 as start date',
|
||||
}).click();
|
||||
|
||||
await waitFor(() =>
|
||||
expect(getByDisplayValue('22/01/2020')).toBeInTheDocument(),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,227 @@
|
|||
import moment from 'moment';
|
||||
import {
|
||||
getDateRangeStartAndEndDates,
|
||||
MONTH_UNTIL_TODAY,
|
||||
LAST_FULL_MONTH,
|
||||
QUARTER_UNTIL_TODAY,
|
||||
LAST_FULL_QUARTER,
|
||||
YEAR_UNTIL_TODAY,
|
||||
LAST_FULL_YEAR,
|
||||
} from '../dateRangeUtils';
|
||||
|
||||
describe('dateRangeUtils', () => {
|
||||
const mockToday = moment('2022-03-12');
|
||||
|
||||
it('returns start and end moments for current month til today', () => {
|
||||
const { start, end } = getDateRangeStartAndEndDates({
|
||||
today: mockToday,
|
||||
dateRangeName: MONTH_UNTIL_TODAY,
|
||||
});
|
||||
|
||||
expect(start.month()).toEqual(2);
|
||||
expect(start.date()).toEqual(1);
|
||||
expect(start.year()).toEqual(2022);
|
||||
|
||||
expect(end.month()).toEqual(2);
|
||||
expect(end.date()).toEqual(12);
|
||||
expect(end.year()).toEqual(2022);
|
||||
});
|
||||
|
||||
it('returns start and end moments for current year til today', () => {
|
||||
const { start, end } = getDateRangeStartAndEndDates({
|
||||
today: mockToday,
|
||||
dateRangeName: YEAR_UNTIL_TODAY,
|
||||
});
|
||||
|
||||
expect(start.month()).toEqual(0);
|
||||
expect(start.date()).toEqual(1);
|
||||
expect(start.year()).toEqual(2022);
|
||||
|
||||
expect(end.month()).toEqual(2);
|
||||
expect(end.date()).toEqual(12);
|
||||
expect(end.year()).toEqual(2022);
|
||||
});
|
||||
|
||||
describe('Last full calendar month', () => {
|
||||
it('returns start and end moments when today is middle of month', () => {
|
||||
const { start, end } = getDateRangeStartAndEndDates({
|
||||
today: mockToday,
|
||||
dateRangeName: LAST_FULL_MONTH,
|
||||
});
|
||||
|
||||
expect(start.month()).toEqual(1);
|
||||
expect(start.date()).toEqual(1);
|
||||
expect(start.year()).toEqual(2022);
|
||||
|
||||
expect(end.month()).toEqual(1);
|
||||
expect(end.date()).toEqual(28);
|
||||
expect(end.year()).toEqual(2022);
|
||||
});
|
||||
|
||||
it('returns start and end moments when today is last day of month', () => {
|
||||
const today = moment('2022-01-31');
|
||||
const { start, end } = getDateRangeStartAndEndDates({
|
||||
today,
|
||||
dateRangeName: LAST_FULL_MONTH,
|
||||
});
|
||||
|
||||
expect(start.month()).toEqual(11);
|
||||
expect(start.date()).toEqual(1);
|
||||
expect(start.year()).toEqual(2021);
|
||||
|
||||
expect(end.month()).toEqual(11);
|
||||
expect(end.date()).toEqual(31);
|
||||
expect(end.year()).toEqual(2021);
|
||||
});
|
||||
|
||||
it('returns start and end moments across months with different day numbers', () => {
|
||||
const today = moment('2022-02-12');
|
||||
const { start, end } = getDateRangeStartAndEndDates({
|
||||
today,
|
||||
dateRangeName: LAST_FULL_MONTH,
|
||||
});
|
||||
|
||||
expect(start.month()).toEqual(0);
|
||||
expect(start.date()).toEqual(1);
|
||||
expect(start.year()).toEqual(2022);
|
||||
|
||||
expect(end.month()).toEqual(0);
|
||||
expect(end.date()).toEqual(31);
|
||||
expect(end.year()).toEqual(2022);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Quarter til today', () => {
|
||||
it('returns start and end moments for date in Q1', () => {
|
||||
const { start, end } = getDateRangeStartAndEndDates({
|
||||
today: mockToday,
|
||||
dateRangeName: QUARTER_UNTIL_TODAY,
|
||||
});
|
||||
|
||||
expect(start.month()).toEqual(0);
|
||||
expect(start.date()).toEqual(1);
|
||||
expect(start.year()).toEqual(2022);
|
||||
|
||||
expect(end.month()).toEqual(2);
|
||||
expect(end.date()).toEqual(12);
|
||||
expect(end.year()).toEqual(2022);
|
||||
});
|
||||
|
||||
it('returns start and end moments for date in Q2', () => {
|
||||
const today = moment('2021-05-05');
|
||||
|
||||
const { start, end } = getDateRangeStartAndEndDates({
|
||||
today,
|
||||
dateRangeName: QUARTER_UNTIL_TODAY,
|
||||
});
|
||||
|
||||
expect(start.month()).toEqual(3);
|
||||
expect(start.date()).toEqual(1);
|
||||
expect(start.year()).toEqual(2021);
|
||||
|
||||
expect(end.month()).toEqual(4);
|
||||
expect(end.date()).toEqual(5);
|
||||
expect(end.year()).toEqual(2021);
|
||||
});
|
||||
|
||||
it('returns start and end moments for date in Q3', () => {
|
||||
const today = moment('2021-08-05');
|
||||
|
||||
const { start, end } = getDateRangeStartAndEndDates({
|
||||
today,
|
||||
dateRangeName: QUARTER_UNTIL_TODAY,
|
||||
});
|
||||
|
||||
expect(start.month()).toEqual(6);
|
||||
expect(start.date()).toEqual(1);
|
||||
expect(start.year()).toEqual(2021);
|
||||
|
||||
expect(end.month()).toEqual(7);
|
||||
expect(end.date()).toEqual(5);
|
||||
expect(end.year()).toEqual(2021);
|
||||
});
|
||||
|
||||
it('returns start and end moments for date in Q4', () => {
|
||||
const today = moment('2021-12-05');
|
||||
|
||||
const { start, end } = getDateRangeStartAndEndDates({
|
||||
today,
|
||||
dateRangeName: QUARTER_UNTIL_TODAY,
|
||||
});
|
||||
|
||||
expect(start.month()).toEqual(9);
|
||||
expect(start.date()).toEqual(1);
|
||||
expect(start.year()).toEqual(2021);
|
||||
|
||||
expect(end.month()).toEqual(11);
|
||||
expect(end.date()).toEqual(5);
|
||||
expect(end.year()).toEqual(2021);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Last full quarter', () => {
|
||||
it('returns start and end moments when today is last day of a quarter', () => {
|
||||
const today = moment('2022-06-30');
|
||||
const { start, end } = getDateRangeStartAndEndDates({
|
||||
today,
|
||||
dateRangeName: LAST_FULL_QUARTER,
|
||||
});
|
||||
|
||||
expect(start.month()).toEqual(0);
|
||||
expect(start.date()).toEqual(1);
|
||||
expect(start.year()).toEqual(2022);
|
||||
|
||||
expect(end.month()).toEqual(2);
|
||||
expect(end.date()).toEqual(31);
|
||||
expect(end.year()).toEqual(2022);
|
||||
});
|
||||
|
||||
it('returns start and end moments when today is in the middle of a quarter', () => {
|
||||
const { start, end } = getDateRangeStartAndEndDates({
|
||||
today: mockToday,
|
||||
dateRangeName: LAST_FULL_QUARTER,
|
||||
});
|
||||
|
||||
expect(start.month()).toEqual(9);
|
||||
expect(start.date()).toEqual(1);
|
||||
expect(start.year()).toEqual(2021);
|
||||
|
||||
expect(end.month()).toEqual(11);
|
||||
expect(end.date()).toEqual(31);
|
||||
expect(end.year()).toEqual(2021);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Last full year', () => {
|
||||
it('returns start and end moments when today is last day of a year', () => {
|
||||
const today = moment('2022-12-31');
|
||||
const { start, end } = getDateRangeStartAndEndDates({
|
||||
today,
|
||||
dateRangeName: LAST_FULL_YEAR,
|
||||
});
|
||||
|
||||
expect(start.month()).toEqual(0);
|
||||
expect(start.date()).toEqual(1);
|
||||
expect(start.year()).toEqual(2021);
|
||||
|
||||
expect(end.month()).toEqual(11);
|
||||
expect(end.date()).toEqual(31);
|
||||
expect(end.year()).toEqual(2021);
|
||||
});
|
||||
|
||||
it('returns start and end moments when today is in the middle of a year', () => {
|
||||
const { start, end } = getDateRangeStartAndEndDates({
|
||||
today: mockToday,
|
||||
dateRangeName: LAST_FULL_YEAR,
|
||||
});
|
||||
|
||||
expect(start.month()).toEqual(0);
|
||||
expect(start.date()).toEqual(1);
|
||||
expect(start.year()).toEqual(2021);
|
||||
|
||||
expect(end.month()).toEqual(11);
|
||||
expect(end.date()).toEqual(31);
|
||||
expect(end.year()).toEqual(2021);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
export const MONTH_UNTIL_TODAY = 'MONTH_UNTIL_TODAY';
|
||||
export const QUARTER_UNTIL_TODAY = 'QUARTER_UNTIL_TODAY';
|
||||
export const YEAR_UNTIL_TODAY = 'YEAR_UNTIL_TODAY';
|
||||
export const LAST_FULL_MONTH = 'LAST_FULL_MONTH';
|
||||
export const LAST_FULL_QUARTER = 'LAST_FULL_QUARTER';
|
||||
export const LAST_FULL_YEAR = 'LAST_FULL_YEAR';
|
||||
|
||||
export const RANGE_LABELS = {
|
||||
MONTH_UNTIL_TODAY: 'This month',
|
||||
QUARTER_UNTIL_TODAY: 'This quarter',
|
||||
YEAR_UNTIL_TODAY: 'This year',
|
||||
LAST_FULL_MONTH: 'Last month',
|
||||
LAST_FULL_QUARTER: 'Last quarter',
|
||||
LAST_FULL_YEAR: 'Last year',
|
||||
};
|
||||
|
||||
const PERIODS = {
|
||||
DAY: 'day',
|
||||
MONTH: 'month',
|
||||
QUARTER: 'quarter',
|
||||
YEAR: 'year',
|
||||
};
|
||||
|
||||
const getPeriodUntilToday = (today, period) => ({
|
||||
start: today.clone().startOf(period),
|
||||
end: today.clone(),
|
||||
});
|
||||
|
||||
const getLastFullPeriod = (today, period) => ({
|
||||
start: today.clone().subtract(1, period).startOf(period),
|
||||
end: today.clone().subtract(1, period).endOf(period),
|
||||
});
|
||||
|
||||
export const getDateRangeStartAndEndDates = ({ today, dateRangeName }) => {
|
||||
switch (dateRangeName) {
|
||||
case MONTH_UNTIL_TODAY:
|
||||
return getPeriodUntilToday(today, PERIODS.MONTH);
|
||||
case LAST_FULL_MONTH:
|
||||
return getLastFullPeriod(today, PERIODS.MONTH);
|
||||
case QUARTER_UNTIL_TODAY:
|
||||
return getPeriodUntilToday(today, PERIODS.QUARTER);
|
||||
case LAST_FULL_QUARTER:
|
||||
return getLastFullPeriod(today, PERIODS.QUARTER);
|
||||
case YEAR_UNTIL_TODAY:
|
||||
return getPeriodUntilToday(today, PERIODS.YEAR);
|
||||
case LAST_FULL_YEAR:
|
||||
return getLastFullPeriod(today, PERIODS.YEAR);
|
||||
}
|
||||
};
|
||||
|
|
@ -1 +1,2 @@
|
|||
export * from './DateRangePicker';
|
||||
export * from './dateRangeUtils';
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue