Skip to content

Commit

Permalink
feat: Month granularity for date-range-picker (#3077)
Browse files Browse the repository at this point in the history
  • Loading branch information
dpitcock authored Feb 3, 2025
1 parent 7a16ac8 commit c5d53ed
Show file tree
Hide file tree
Showing 57 changed files with 5,719 additions and 2,253 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import { Box, DateRangePicker, DateRangePickerProps, SpaceBetween } from '~compo
import createPermutations from '../utils/permutations';
import PermutationsView from '../utils/permutations-view';
import ScreenshotArea from '../utils/screenshot-area';
import { generateI18nStrings, generatePlaceholder, isValid } from './common';
import { generatePlaceholder, i18nStrings, isValid } from './common';

const permutations = createPermutations<
Pick<DateRangePickerProps, 'absoluteFormat' | 'dateOnly' | 'hideTimeOffset' | 'value'>
Pick<DateRangePickerProps, 'absoluteFormat' | 'dateOnly' | 'hideTimeOffset' | 'value' | 'granularity'>
>([
{
absoluteFormat: ['iso', 'long-localized'],
Expand Down Expand Up @@ -41,7 +41,7 @@ export default function DateRangePickerPermutations() {
return (
<Box padding="s">
<SpaceBetween direction="vertical" size="m">
<h1>Absolute date range picker with custom absolute format</h1>
<h1>Absolute date range picker month calendar with custom absolute format</h1>
<hr />
<ScreenshotArea>
<PermutationsView
Expand All @@ -51,12 +51,13 @@ export default function DateRangePickerPermutations() {
value={permutation.value}
absoluteFormat={permutation.absoluteFormat}
dateOnly={permutation.dateOnly}
granularity="day"
hideTimeOffset={permutation.hideTimeOffset}
locale="en-US"
i18nStrings={generateI18nStrings(permutation.dateOnly || false, false)}
i18nStrings={i18nStrings}
placeholder={generatePlaceholder(permutation.dateOnly, false)}
relativeOptions={[]}
isValidRange={isValid}
isValidRange={value => isValid('day')(value)}
rangeSelectorMode={'absolute-only'}
getTimeOffset={() => 60}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React from 'react';

import { Box, DateRangePicker, DateRangePickerProps, SpaceBetween } from '~components';

import createPermutations from '../utils/permutations';
import PermutationsView from '../utils/permutations-view';
import ScreenshotArea from '../utils/screenshot-area';
import { generatePlaceholder, i18nStrings, isValid } from './common';

const permutations = createPermutations<
Pick<DateRangePickerProps, 'absoluteFormat' | 'dateOnly' | 'hideTimeOffset' | 'value' | 'granularity'>
>([
{
absoluteFormat: ['iso', 'long-localized'],
value: [
{
type: 'absolute',
startDate: '2024-12',
endDate: '2025-01',
},
],
},
{
absoluteFormat: ['iso', 'long-localized'],
hideTimeOffset: [true, false],
value: [
{
type: 'absolute',
startDate: '2023-06',
endDate: '2024-02',
},
],
},
]);

export default function DateRangePickerPermutations() {
return (
<Box padding="s">
<SpaceBetween direction="vertical" size="m">
<h1>Absolute date range picker year calendar with custom absolute format</h1>
<hr />
<ScreenshotArea>
<PermutationsView
permutations={permutations}
render={permutation => (
<DateRangePicker
value={permutation.value}
absoluteFormat={permutation.absoluteFormat}
dateOnly={false}
granularity="month"
hideTimeOffset={permutation.hideTimeOffset}
locale="en-US"
i18nStrings={i18nStrings}
placeholder={generatePlaceholder(permutation.dateOnly, permutation.granularity === 'month')}
relativeOptions={[]}
isValidRange={value => isValid('month')(value)}
rangeSelectorMode={'absolute-only'}
getTimeOffset={() => 60}
/>
)}
/>
</ScreenshotArea>
</SpaceBetween>
</Box>
);
}
49 changes: 30 additions & 19 deletions pages/date-range-picker/absolute-format.localization.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
import React, { useContext, useState } from 'react';

import { Box, DateRangePicker, DateRangePickerProps, Grid, SpaceBetween } from '~components';
import { Box, DateRangePicker, DateRangePickerProps, FormField, Grid, SpaceBetween } from '~components';

import AppContext from '../app/app-context';
import {
Expand All @@ -11,8 +11,8 @@ import {
DateRangePickerDemoContext,
dateRangePickerDemoDefaults,
DisabledDate,
generateI18nStrings,
generatePlaceholder,
i18nStrings,
isValid,
} from './common';

Expand Down Expand Up @@ -47,7 +47,7 @@ const initialRange = {
export default function DateRangePickerScenario() {
const { urlParams, setUrlParams } = useContext(AppContext as DateRangePickerDemoContext);
const dateOnly = urlParams.dateOnly ?? dateRangePickerDemoDefaults.dateOnly;
const monthOnly = false;
const monthOnly = urlParams.monthOnly ?? dateRangePickerDemoDefaults.monthOnly;
const disabledDates =
(urlParams.disabledDates as DisabledDate) ?? (dateRangePickerDemoDefaults.disabledDates as DisabledDate);
const withDisabledReason = urlParams.withDisabledReason ?? dateRangePickerDemoDefaults.withDisabledReason;
Expand Down Expand Up @@ -117,6 +117,14 @@ export default function DateRangePickerScenario() {
/>{' '}
Date only
</label>
<label>
<input
type="checkbox"
checked={monthOnly}
onChange={event => setUrlParams({ monthOnly: !!event.target.checked })}
/>{' '}
Month only
</label>
<label>
Time offset from UTC in minutes{' '}
<input
Expand All @@ -142,22 +150,25 @@ export default function DateRangePickerScenario() {
<div key={`pickers-${locale}`} dir={rtlLocales.has(locale) ? 'rtl' : 'ltr'}>
<Grid gridDefinition={[{ colspan: 1 }, { colspan: 11 }]}>
<div style={{ textAlign: 'right' }}>{locale}</div>
<DateRangePicker
value={value}
locale={locale}
i18nStrings={generateI18nStrings(dateOnly, monthOnly)}
placeholder={generatePlaceholder(dateOnly, monthOnly)}
onChange={e => setValue(e.detail.value)}
relativeOptions={[]}
isValidRange={isValid}
rangeSelectorMode={'absolute-only'}
isDateEnabled={date => checkIfDisabled(date, disabledDates, monthOnly)}
dateDisabledReason={date => applyDisabledReason(withDisabledReason, date, disabledDates, monthOnly)}
getTimeOffset={timeOffset === undefined ? undefined : () => timeOffset!}
absoluteFormat={absoluteFormat}
dateOnly={dateOnly}
hideTimeOffset={hideTimeOffset}
/>
<FormField label="Date Range Picker field">
<DateRangePicker
value={value}
locale={locale}
i18nStrings={i18nStrings}
placeholder={generatePlaceholder(dateOnly, monthOnly)}
onChange={e => setValue(e.detail.value)}
relativeOptions={[]}
isValidRange={isValid(monthOnly ? 'month' : 'day')}
rangeSelectorMode={'absolute-only'}
isDateEnabled={date => checkIfDisabled(date, disabledDates, monthOnly)}
dateDisabledReason={date => applyDisabledReason(withDisabledReason, date, disabledDates, monthOnly)}
getTimeOffset={timeOffset === undefined ? undefined : () => timeOffset!}
absoluteFormat={absoluteFormat}
dateOnly={dateOnly}
granularity={monthOnly ? 'month' : 'day'}
hideTimeOffset={hideTimeOffset}
/>
</FormField>
</Grid>
</div>
))}
Expand Down
83 changes: 50 additions & 33 deletions pages/date-range-picker/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { DateRangePickerProps } from '~components/date-range-picker';

import { AppContextType } from '../app/app-context';
import { makeIsValidFunction } from './is-valid-range';
import { makeIsDateValidFunction, makeIsMonthValidFunction } from './is-valid-range';

export type DateRangePickerDemoContext = React.Context<
AppContextType<{
Expand All @@ -19,6 +19,7 @@ export type DateRangePickerDemoContext = React.Context<
expandToViewport?: boolean;
disabledDates?: string;
withDisabledReason?: boolean;
hasValue?: boolean;
}>
>;

Expand Down Expand Up @@ -111,30 +112,39 @@ export const dateRangePickerDemoDefaults = {
expandToViewport: false,
disabledDates: 'none',
withDisabledReason: true,
hasValue: true,
};

function formatRelativeRange(range: DateRangePickerProps.RelativeValue): string {
const unit = range.amount === 1 ? range.unit : `${range.unit}s`;
return `Previous ${range.amount} ${unit}`;
return `Last ${range.amount} ${unit}`;
}

export const i18nStrings: DateRangePickerProps['i18nStrings'] = {
ariaLabel: 'Filter by a date and time range',
todayAriaLabel: 'Today',
nextMonthAriaLabel: 'Next month',
previousMonthAriaLabel: 'Previous month',
nextYearAriaLabel: 'Next year',
previousYearAriaLabel: 'Previous year',
currentMonthAriaLabel: 'This month',
customRelativeRangeDurationLabel: 'Duration',
customRelativeRangeDurationPlaceholder: 'Enter duration',
customRelativeRangeOptionLabel: 'Custom range',
customRelativeRangeOptionDescription: 'Set a custom range in the past',
customRelativeRangeUnitLabel: 'Unit of time',
formatRelativeRange: formatRelativeRange,
formatUnit: (unit, value) => (value === 1 ? unit : `${unit}s`),
dateConstraintText: 'Range must be between 6 and 30 days.',
dateTimeConstraintText: 'Range must be between 6 and 30 days. Use 24 hour format.',
monthConstraintText: 'For month use YYYY/MM.',
modeSelectionLabel: 'Date range mode',
relativeModeTitle: 'Relative range',
absoluteModeTitle: 'Absolute range',
relativeRangeSelectionHeading: 'Choose a range',
relativeRangeSelectionMonthlyDescription: 'Each month counts from the first day to the last day.',
startMonthLabel: 'Start month',
endMonthLabel: 'End month',
startDateLabel: 'Start date',
endDateLabel: 'End date',
startTimeLabel: 'Start time',
Expand All @@ -146,33 +156,25 @@ export const i18nStrings: DateRangePickerProps['i18nStrings'] = {
renderSelectedAbsoluteRangeAriaLive: (startDate, endDate) => `Range selected from ${startDate} to ${endDate}`,
};

export function generateI18nStrings(isDateOnly: boolean, isMonthOnly: boolean): DateRangePickerProps['i18nStrings'] {
return {
...i18nStrings,
...(isDateOnly ? { dateTimeConstraintText: 'Range must be between 6 and 30 days.' } : {}),
...(isMonthOnly ? { dateTimeConstraintText: 'For month use YYYY/MM.' } : {}),
};
}

export const relativeOptions = [
{ key: 'previous-5-minutes', amount: 5, unit: 'minute', type: 'relative' },
{ key: 'previous-30-minutes', amount: 30, unit: 'minute', type: 'relative' },
{ key: 'previous-1-hour', amount: 1, unit: 'hour', type: 'relative' },
{ key: 'previous-6-hours', amount: 6, unit: 'hour', type: 'relative' },
{ key: 'last-5-minutes', amount: 5, unit: 'minute', type: 'relative' },
{ key: 'last-30-minutes', amount: 30, unit: 'minute', type: 'relative' },
{ key: 'last-1-hour', amount: 1, unit: 'hour', type: 'relative' },
{ key: 'last-6-hours', amount: 6, unit: 'hour', type: 'relative' },
] as const;

export const dateOnlyRelativeOptions = [
{ key: 'previous-1-day', amount: 5, unit: 'day', type: 'relative' },
{ key: 'previous-7-days', amount: 7, unit: 'day', type: 'relative' },
{ key: 'previous-1-month', amount: 1, unit: 'month', type: 'relative' },
{ key: 'previous-6-months', amount: 6, unit: 'month', type: 'relative' },
{ key: 'last-1-day', amount: 5, unit: 'day', type: 'relative' },
{ key: 'last-7-days', amount: 7, unit: 'day', type: 'relative' },
{ key: 'last-1-month', amount: 1, unit: 'month', type: 'relative' },
{ key: 'last-6-months', amount: 6, unit: 'month', type: 'relative' },
] as const;

export const monthOnlyRelativeOptions = [
{ key: 'previous-1-month', amount: 1, unit: 'month', type: 'relative' },
{ key: 'previous-2-months', amount: 2, unit: 'month', type: 'relative' },
{ key: 'previous-3-months', amount: 3, unit: 'month', type: 'relative' },
{ key: 'previous-6-months', amount: 6, unit: 'month', type: 'relative' },
{ key: 'last-1-month', amount: 1, unit: 'month', type: 'relative' },
{ key: 'last-2-months', amount: 2, unit: 'month', type: 'relative' },
{ key: 'last-3-months', amount: 3, unit: 'month', type: 'relative' },
{ key: 'last-6-months', amount: 6, unit: 'month', type: 'relative' },
] as const;

export function generateRelativeOptions(dateOnly: boolean, monthOnly: boolean) {
Expand All @@ -185,14 +187,29 @@ export function generateRelativeOptions(dateOnly: boolean, monthOnly: boolean) {
return relativeOptions;
}

export const isValid = makeIsValidFunction({
durationBetweenOneAndTwenty: 'The amount part of the range needs to be between 1 and 20.',
durationMissing: 'You need to provide a duration.',
minimumStartDate: 'The range cannot start before 2018.',
noValueSelected: 'You need to select a range.',
startDateMissing: 'You need to provide a start date.',
endDateMissing: 'You need to provide an end date.',
});

export const generatePlaceholder = (dateOnly?: boolean, monthOnly?: boolean) =>
`Filter by ${monthOnly ? 'month' : 'date'} ${dateOnly ? '' : ' and time '}range`;
export const isValid = (granularity: DateRangePickerProps.Granularity) => {
const errorMessages = {
durationBetweenOneAndTwenty: 'The amount part of the range needs to be between 1 and 20.',
durationMissing: 'You need to provide a duration.',
notLongEnough: 'The selected date range is too small. Select a range of one month or larger.',
minimumStartDate: 'The range cannot start before 2018.',
noValueSelected: 'You need to select a range.',
startDateMissing: 'You need to provide a start date.',
endDateMissing: 'You need to provide an end date.',
};

if (granularity === 'month') {
return makeIsMonthValidFunction(errorMessages);
}
return makeIsDateValidFunction(errorMessages);
};

export const generatePlaceholder = (dateOnly?: boolean, monthOnly?: boolean) => {
if (monthOnly) {
return `Filter by month range`;
}
if (dateOnly) {
return `Filter by date range`;
}
return `Filter by date and time range`;
};
Loading

0 comments on commit c5d53ed

Please sign in to comment.