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:
Suzanne Aitchison 2022-06-08 15:48:06 +01:00 committed by GitHub
parent bea711310e
commit 6c9a09c344
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 786 additions and 101 deletions

View file

@ -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),
};

View file

@ -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,
],
};

View file

@ -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(),
);
});
});
});

View file

@ -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(),
);
});
});
});

View file

@ -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);
});
});
});

View file

@ -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);
}
};

View file

@ -1 +1,2 @@
export * from './DateRangePicker';
export * from './dateRangeUtils';