diff --git a/.changeset/four-geckos-complain.md b/.changeset/four-geckos-complain.md new file mode 100644 index 0000000000..bc641f41ca --- /dev/null +++ b/.changeset/four-geckos-complain.md @@ -0,0 +1,5 @@ +--- +'@sumup-oss/circuit-ui': major +--- + +Changed the `PlainDateRange` type from a tuple to an object with `start` and `end` properties. This affects the Calendar component's `selection` prop. Use the new `updatePlainDateRange` helper function to update a date range when a user selects a date. diff --git a/docs/introduction/2-getting-started.mdx b/docs/introduction/2-getting-started.mdx index 5cceff8e00..8dd11e7211 100644 --- a/docs/introduction/2-getting-started.mdx +++ b/docs/introduction/2-getting-started.mdx @@ -30,13 +30,13 @@ npm install @sumup-oss/circuit-ui yarn add @sumup-oss/circuit-ui ``` -Circuit UI relies on some mandatory peer dependencies, namely [@sumup-oss/design-tokens](https://www.npmjs.com/package/@sumup-oss/design-tokens), [@sumup-oss/icons](https://www.npmjs.com/package/@sumup-oss/icons), [@sumup-oss/intl](https://www.npmjs.com/package/@sumup-oss/intl), and [React](https://reactjs.org/). You should install them with the following command: +Circuit UI relies on some mandatory peer dependencies, namely [@sumup-oss/design-tokens](https://www.npmjs.com/package/@sumup-oss/design-tokens), [@sumup-oss/icons](https://www.npmjs.com/package/@sumup-oss/icons), [@sumup-oss/intl](https://www.npmjs.com/package/@sumup-oss/intl), [React](https://reactjs.org/), and [temporal-polyfill](https://www.npmjs.com/package/temporal-polyfill). You should install them with the following command: ```sh # With npm: -npm install --save @sumup-oss/design-tokens @sumup-oss/icons @sumup-oss/intl react react-dom +npm install --save @sumup-oss/design-tokens @sumup-oss/icons @sumup-oss/intl react react-dom temporal-polyfill # With yarn v1 -yarn add @sumup-oss/design-tokens @sumup-oss/icons @sumup-oss/intl react react-dom +yarn add @sumup-oss/design-tokens @sumup-oss/icons @sumup-oss/intl react react-dom temporal-polyfill ``` We also recommend installing and configuring [`@sumup-oss/eslint-plugin-circuit-ui`](Packages/eslint-plugin-circuit-ui/Docs) and [`@sumup-oss/stylelint-plugin-circuit-ui`](Packages/stylelint-plugin-circuit-ui/Docs). The plugins will lint [Circuit UI custom properties](Features/Theme/Docs) and include codemods for Circuit UI breaking changes. diff --git a/packages/circuit-ui/components/Calendar/Calendar.mdx b/packages/circuit-ui/components/Calendar/Calendar.mdx index 391877e719..8d776c7b45 100644 --- a/packages/circuit-ui/components/Calendar/Calendar.mdx +++ b/packages/circuit-ui/components/Calendar/Calendar.mdx @@ -12,22 +12,11 @@ The Calendar component displays a monthly date grid. This is a low-level compone -## Dependencies - -The Calendar component uses the experimental [`Temporal` API](https://tc39.es/proposal-temporal/docs/) which has reached [stage 3](https://github.com/tc39/proposals#stage-3) in the [ECMAScript proposal](https://github.com/tc39/proposal-temporal) process but isn't implemented in [most browsers](https://caniuse.com/temporal) yet. Circuit UI depends on a [polyfill](https://github.com/fullcalendar/temporal-polyfill) which you need to install to use the component in your application: - -```bash -# npm -npm install temporal-polyfill -# yarn v1 -yarn add temporal-polyfill -``` - ## Usage ### Selection -Use the `selection` prop to set the currently selected date or date range and the `onSelect` prop to update the selection when a user picks a different date. Use the `minDate` and `maxDate` props to restrict the available date range or use [modifiers](#modifiers) to disable individual days. +Use the `selection` prop to set the currently selected date or date range and the `onSelect` prop to update the selection when a user picks a different date. Use the exported `updatePlainDateRange` function to update a date range when a user selects a date. Use the `minDate` and `maxDate` props to restrict the available date range or use [modifiers](#modifiers) to disable individual days. diff --git a/packages/circuit-ui/components/Calendar/Calendar.module.css b/packages/circuit-ui/components/Calendar/Calendar.module.css index caa581146d..e3d5814b05 100644 --- a/packages/circuit-ui/components/Calendar/Calendar.module.css +++ b/packages/circuit-ui/components/Calendar/Calendar.module.css @@ -19,6 +19,7 @@ .months { display: flex; + isolation: isolate; } .month:not(:last-child) { @@ -97,7 +98,7 @@ touch-action: manipulation; cursor: pointer; background: none; - border: 0; + border: 1px solid transparent; border-radius: var(--cui-border-radius-circle); } @@ -114,17 +115,17 @@ } .day[aria-current="date"] { - border: 1px solid var(--cui-border-normal); + border-color: var(--cui-border-normal); } .day:hover { background: var(--cui-bg-normal-hovered); - border: 1px solid var(--cui-border-strong-hovered); + border-color: var(--cui-border-strong-hovered); } .day:active { background: var(--cui-bg-normal-pressed); - border: 1px solid var(--cui-border-strong-pressed); + border-color: var(--cui-border-strong-pressed); } /* Selected */ @@ -203,7 +204,7 @@ td:not(:last-of-type) .range-end.last-day::before { } .day[aria-current="date"][aria-disabled="true"] { - border: 1px solid var(--cui-border-normal-disabled); + border-color: var(--cui-border-normal-disabled); } .day[aria-disabled="true"].selected, diff --git a/packages/circuit-ui/components/Calendar/Calendar.spec.tsx b/packages/circuit-ui/components/Calendar/Calendar.spec.tsx index 2d9a263698..b9b42e788a 100644 --- a/packages/circuit-ui/components/Calendar/Calendar.spec.tsx +++ b/packages/circuit-ui/components/Calendar/Calendar.spec.tsx @@ -25,7 +25,6 @@ import { waitFor, act, } from '../../util/test-utils.js'; -import type { PlainDateRange } from '../../util/date.js'; import { Calendar } from './Calendar.js'; @@ -224,10 +223,10 @@ describe('Calendar', () => { }); it('should mark the selected date range', () => { - const selection = [ - new Temporal.PlainDate(2020, 3, 15), - new Temporal.PlainDate(2020, 3, 25), - ] satisfies PlainDateRange; + const selection = { + start: new Temporal.PlainDate(2020, 3, 15), + end: new Temporal.PlainDate(2020, 3, 25), + }; const { container } = render( , ); @@ -236,8 +235,8 @@ describe('Calendar', () => { expect(selectedDays).toHaveLength(11); for ( - let index = selection[0].day; - index <= selection[1].day; + let index = selection.start.day; + index <= selection.end.day; index += 1 ) { const selectedDay = screen.getByRole('button', { diff --git a/packages/circuit-ui/components/Calendar/Calendar.stories.tsx b/packages/circuit-ui/components/Calendar/Calendar.stories.tsx index b11f761763..18b8c51b4e 100644 --- a/packages/circuit-ui/components/Calendar/Calendar.stories.tsx +++ b/packages/circuit-ui/components/Calendar/Calendar.stories.tsx @@ -18,7 +18,11 @@ import isChromatic from 'chromatic/isChromatic'; import { Temporal } from 'temporal-polyfill'; import { Stack } from '../../../../.storybook/components/index.js'; -import { getTodaysDate, type PlainDateRange } from '../../util/date.js'; +import { + getTodaysDate, + updatePlainDateRange, + type PlainDateRange, +} from '../../util/date.js'; import { Calendar, type CalendarProps } from './Calendar.js'; @@ -118,19 +122,7 @@ export const Range = (args: CalendarProps) => { const [selection, setSelection] = useState(args.selection as PlainDateRange); const handleSelect = (date: Temporal.PlainDate) => { - setSelection((prevSelection) => { - if ( - // Nothing selected yet - prevSelection.length === 0 || - // Full range already selected - prevSelection.length === 2 || - // Selected date is before previous start date - Temporal.PlainDate.compare(prevSelection[0], date) > 0 - ) { - return [date]; - } - return [prevSelection[0], date]; - }); + setSelection((prevSelection) => updatePlainDateRange(prevSelection, date)); }; return ; @@ -138,6 +130,9 @@ export const Range = (args: CalendarProps) => { Range.args = { ...Base.args, - selection: [today.subtract({ days: 3 }), today.add({ days: 3 })], + selection: { + start: today.subtract({ days: 3 }), + end: today.add({ days: 3 }), + }, numberOfMonths: 2, }; diff --git a/packages/circuit-ui/components/Calendar/CalendarService.spec.ts b/packages/circuit-ui/components/Calendar/CalendarService.spec.ts index 4905931abf..2bbbfd07cc 100644 --- a/packages/circuit-ui/components/Calendar/CalendarService.spec.ts +++ b/packages/circuit-ui/components/Calendar/CalendarService.spec.ts @@ -16,8 +16,6 @@ import { describe, expect, it, vi } from 'vitest'; import { Temporal } from 'temporal-polyfill'; -import type { PlainDateRange } from '../../util/date.js'; - import { CalendarActionType, calendarReducer, @@ -85,10 +83,10 @@ describe('CalendarService', () => { }); it('should focus the start date of a selected date range', () => { - const selection: PlainDateRange = [ - new Temporal.PlainDate(2020, 3, 28), - new Temporal.PlainDate(2020, 3, 18), - ]; + const selection = { + start: new Temporal.PlainDate(2020, 3, 18), + end: new Temporal.PlainDate(2020, 3, 28), + }; const minDate = null; const maxDate = null; const numberOfMonths = 1; @@ -372,75 +370,77 @@ describe('CalendarService', () => { it('should return true if the date matches the start and end of the selected range', () => { const date = new Temporal.PlainDate(2020, 3, 15); - const selection = [ - new Temporal.PlainDate(2020, 3, 15), - new Temporal.PlainDate(2020, 3, 15), - ] satisfies PlainDateRange; + const selection = { + start: new Temporal.PlainDate(2020, 3, 15), + end: new Temporal.PlainDate(2020, 3, 15), + }; const actual = isDateActive(date, selection); expect(actual).toBe(true); }); it('should return true if the date matches the start of the selected range', () => { const date = new Temporal.PlainDate(2020, 3, 15); - const selection = [ - new Temporal.PlainDate(2020, 3, 20), - new Temporal.PlainDate(2020, 3, 15), - ] satisfies PlainDateRange; + const selection = { + start: new Temporal.PlainDate(2020, 3, 15), + end: new Temporal.PlainDate(2020, 3, 20), + }; const actual = isDateActive(date, selection); expect(actual).toBe(true); }); it('should return true if the date is in the middle of the selected range', () => { const date = new Temporal.PlainDate(2020, 3, 15); - const selection = [ - new Temporal.PlainDate(2020, 3, 20), - new Temporal.PlainDate(2020, 3, 10), - ] satisfies PlainDateRange; + const selection = { + start: new Temporal.PlainDate(2020, 3, 10), + end: new Temporal.PlainDate(2020, 3, 20), + }; const actual = isDateActive(date, selection); expect(actual).toBe(true); }); it('should return true if the date matches the end of the selected range', () => { const date = new Temporal.PlainDate(2020, 3, 15); - const selection = [ - new Temporal.PlainDate(2020, 3, 15), - new Temporal.PlainDate(2020, 3, 10), - ] satisfies PlainDateRange; + const selection = { + start: new Temporal.PlainDate(2020, 3, 10), + end: new Temporal.PlainDate(2020, 3, 15), + }; const actual = isDateActive(date, selection); expect(actual).toBe(true); }); it('should return true if the date matches the start of an incomplete range', () => { const date = new Temporal.PlainDate(2020, 3, 15); - const selection = [ - new Temporal.PlainDate(2020, 3, 15), - ] satisfies PlainDateRange; + const selection = { + start: new Temporal.PlainDate(2020, 3, 15), + end: undefined, + }; const actual = isDateActive(date, selection); expect(actual).toBe(true); }); it('should return false for an empty range', () => { const date = new Temporal.PlainDate(2020, 3, 15); - const selection = [] satisfies PlainDateRange; + const selection = { start: undefined, end: undefined }; const actual = isDateActive(date, selection); expect(actual).toBe(false); }); it('should return false if the date falls outside the selected range', () => { const date = new Temporal.PlainDate(2020, 3, 15); - const selection = [ - new Temporal.PlainDate(2020, 3, 16), - new Temporal.PlainDate(2020, 3, 18), - ] satisfies PlainDateRange; + const selection = { + start: new Temporal.PlainDate(2020, 3, 16), + end: new Temporal.PlainDate(2020, 3, 18), + }; const actual = isDateActive(date, selection); expect(actual).toBe(false); }); it('should return false if the date does not match the start date of an incomplete range', () => { const date = new Temporal.PlainDate(2020, 3, 15); - const selection = [ - new Temporal.PlainDate(2020, 3, 16), - ] satisfies PlainDateRange; + const selection = { + start: new Temporal.PlainDate(2020, 3, 16), + end: undefined, + }; const actual = isDateActive(date, selection); expect(actual).toBe(false); }); @@ -473,7 +473,7 @@ describe('CalendarService', () => { it('should return null if the selected range is empty', () => { const date = new Temporal.PlainDate(2020, 3, 15); const hoveredDate = null; - const selection: PlainDateRange = []; + const selection = { start: undefined, end: undefined }; const actual = getSelectionType(date, hoveredDate, selection); expect(actual).toBeNull(); }); @@ -489,9 +489,10 @@ describe('CalendarService', () => { it('should return "selected" if the date matches the start of an incomplete range', () => { const date = new Temporal.PlainDate(2020, 3, 15); const hoveredDate = null; - const selection = [ - new Temporal.PlainDate(2020, 3, 15), - ] satisfies PlainDateRange; + const selection = { + start: new Temporal.PlainDate(2020, 3, 15), + end: undefined, + }; const actual = getSelectionType(date, hoveredDate, selection); expect(actual).toBe('selected'); }); @@ -499,9 +500,10 @@ describe('CalendarService', () => { it('should return "selected" if the start of an incomplete range matches the hovered date', () => { const date = new Temporal.PlainDate(2020, 3, 15); const hoveredDate = new Temporal.PlainDate(2020, 3, 15); - const selection = [ - new Temporal.PlainDate(2020, 3, 15), - ] satisfies PlainDateRange; + const selection = { + start: new Temporal.PlainDate(2020, 3, 15), + end: undefined, + }; const actual = getSelectionType(date, hoveredDate, selection); expect(actual).toBe('selected'); }); @@ -509,10 +511,10 @@ describe('CalendarService', () => { it('should return "range-start" if the date matches the start of the selected range', () => { const date = new Temporal.PlainDate(2020, 3, 15); const hoveredDate = null; - const selection = [ - new Temporal.PlainDate(2020, 3, 15), - new Temporal.PlainDate(2020, 3, 25), - ] satisfies PlainDateRange; + const selection = { + start: new Temporal.PlainDate(2020, 3, 15), + end: new Temporal.PlainDate(2020, 3, 25), + }; const actual = getSelectionType(date, hoveredDate, selection); expect(actual).toBe('range-start'); }); @@ -520,9 +522,10 @@ describe('CalendarService', () => { it('should return "range-start" if the date matches the start of an incomplete range and hovered date', () => { const date = new Temporal.PlainDate(2020, 3, 15); const hoveredDate = new Temporal.PlainDate(2020, 3, 25); - const selection = [ - new Temporal.PlainDate(2020, 3, 15), - ] satisfies PlainDateRange; + const selection = { + start: new Temporal.PlainDate(2020, 3, 15), + end: undefined, + }; const actual = getSelectionType(date, hoveredDate, selection); expect(actual).toBe('range-start'); }); @@ -530,10 +533,10 @@ describe('CalendarService', () => { it('should return "range-middle" if the date is part of the selected range', () => { const date = new Temporal.PlainDate(2020, 3, 15); const hoveredDate = null; - const selection = [ - new Temporal.PlainDate(2020, 3, 20), - new Temporal.PlainDate(2020, 3, 10), - ] satisfies PlainDateRange; + const selection = { + start: new Temporal.PlainDate(2020, 3, 10), + end: new Temporal.PlainDate(2020, 3, 20), + }; const actual = getSelectionType(date, hoveredDate, selection); expect(actual).toBe('range-middle'); }); @@ -541,9 +544,10 @@ describe('CalendarService', () => { it('should return "range-middle" if the date is between the incomplete range and hovered date', () => { const date = new Temporal.PlainDate(2020, 3, 15); const hoveredDate = new Temporal.PlainDate(2020, 3, 20); - const selection = [ - new Temporal.PlainDate(2020, 3, 10), - ] satisfies PlainDateRange; + const selection = { + start: new Temporal.PlainDate(2020, 3, 10), + end: undefined, + }; const actual = getSelectionType(date, hoveredDate, selection); expect(actual).toBe('range-middle'); }); @@ -551,10 +555,10 @@ describe('CalendarService', () => { it('should return "range-end" if the date matches the end of the selected range', () => { const date = new Temporal.PlainDate(2020, 3, 15); const hoveredDate = null; - const selection = [ - new Temporal.PlainDate(2020, 3, 5), - new Temporal.PlainDate(2020, 3, 15), - ] satisfies PlainDateRange; + const selection = { + start: new Temporal.PlainDate(2020, 3, 5), + end: new Temporal.PlainDate(2020, 3, 15), + }; const actual = getSelectionType(date, hoveredDate, selection); expect(actual).toBe('range-end'); }); @@ -562,9 +566,10 @@ describe('CalendarService', () => { it('should return "range-end" if the date matches the end of an incomplete range and hovered date', () => { const date = new Temporal.PlainDate(2020, 3, 20); const hoveredDate = new Temporal.PlainDate(2020, 3, 20); - const selection = [ - new Temporal.PlainDate(2020, 3, 15), - ] satisfies PlainDateRange; + const selection = { + start: new Temporal.PlainDate(2020, 3, 15), + end: undefined, + }; const actual = getSelectionType(date, hoveredDate, selection); expect(actual).toBe('range-end'); }); @@ -572,10 +577,10 @@ describe('CalendarService', () => { it('should prioritize the end date over the hovered date', () => { const date = new Temporal.PlainDate(2020, 3, 15); const hoveredDate = new Temporal.PlainDate(2020, 3, 25); - const selection = [ - new Temporal.PlainDate(2020, 3, 15), - new Temporal.PlainDate(2020, 3, 10), - ] satisfies PlainDateRange; + const selection = { + start: new Temporal.PlainDate(2020, 3, 10), + end: new Temporal.PlainDate(2020, 3, 15), + }; const actual = getSelectionType(date, hoveredDate, selection); expect(actual).toBe('range-end'); }); @@ -583,9 +588,10 @@ describe('CalendarService', () => { it('should ignore the hovered date if it is before the start of an incomplete range', () => { const date = new Temporal.PlainDate(2020, 3, 25); const hoveredDate = new Temporal.PlainDate(2020, 3, 5); - const selection = [ - new Temporal.PlainDate(2020, 3, 15), - ] satisfies PlainDateRange; + const selection = { + start: new Temporal.PlainDate(2020, 3, 15), + end: undefined, + }; const actual = getSelectionType(date, hoveredDate, selection); expect(actual).toBeNull(); }); @@ -593,10 +599,10 @@ describe('CalendarService', () => { it('should return null if the date is not part of any range', () => { const date = new Temporal.PlainDate(2020, 3, 5); const hoveredDate = null; - const selection = [ - new Temporal.PlainDate(2020, 3, 15), - new Temporal.PlainDate(2020, 3, 12), - ] satisfies PlainDateRange; + const selection = { + start: new Temporal.PlainDate(2020, 3, 12), + end: new Temporal.PlainDate(2020, 3, 15), + }; const actual = getSelectionType(date, hoveredDate, selection); expect(actual).toBeNull(); }); diff --git a/packages/circuit-ui/components/Calendar/CalendarService.ts b/packages/circuit-ui/components/Calendar/CalendarService.ts index d33cb2dbe2..37916a80b5 100644 --- a/packages/circuit-ui/components/Calendar/CalendarService.ts +++ b/packages/circuit-ui/components/Calendar/CalendarService.ts @@ -13,8 +13,8 @@ * limitations under the License. */ -// biome-ignore lint/suspicious/noShadowRestrictedNames: Necessary to add support for Temporal objects to the `Intl` APIs -import { Temporal, Intl } from 'temporal-polyfill'; +import { Temporal } from 'temporal-polyfill'; +import { formatDateTime } from '@sumup-oss/intl'; import type { Locale } from '../../util/i18n.js'; import { chunk, last } from '../../util/helpers.js'; @@ -24,7 +24,6 @@ import { getLastDateOfWeek, getTodaysDate, isPlainDate, - sortDateRange, type DaysInWeek, type FirstDayOfWeek, type PlainDateRange, @@ -68,7 +67,7 @@ export function initCalendar({ const today = getTodaysDate(); let date: Temporal.PlainDate | undefined; if (selection) { - date = isPlainDate(selection) ? selection : sortDateRange(selection)[0]; + date = isPlainDate(selection) ? selection : selection.start; } const focusedDate = clampDate(date || today, minDate, maxDate); const hoveredDate = null; @@ -146,20 +145,18 @@ export function getWeekdays( locale?: Locale, calendar?: string, ) { - const narrow = new Intl.DateTimeFormat(locale, { - weekday: 'narrow', - calendar, - }); - const long = new Intl.DateTimeFormat(locale, { - weekday: 'long', - calendar, - }); return Array.from(Array(daysInWeek)).map((_, index) => { // 1973 started with a Monday const date = new Temporal.PlainDate(1973, 1, index + firstDayOfWeek); return { - narrow: narrow.format(date), - long: long.format(date), + narrow: formatDateTime(date, locale, { + weekday: 'narrow', + calendar, + }), + long: formatDateTime(date, locale, { + weekday: 'long', + calendar, + }), }; }) as Weekdays; } @@ -169,12 +166,11 @@ export function getMonthHeadline( locale?: Locale, calendar = 'iso8601', ) { - const intl = new Intl.DateTimeFormat(locale, { + return formatDateTime(yearMonth, locale, { year: 'numeric', month: 'long', calendar, }); - return intl.format(yearMonth); } export function getDatesInRange( @@ -217,15 +213,14 @@ export function isDateActive( if (isPlainDate(selection)) { return date.equals(selection); } - const [startDate, endDate] = sortDateRange(selection); - if (startDate && endDate) { + if (selection.start && selection.end) { return ( - Temporal.PlainDate.compare(date, startDate) >= 0 && - Temporal.PlainDate.compare(date, endDate) <= 0 + Temporal.PlainDate.compare(date, selection.start) >= 0 && + Temporal.PlainDate.compare(date, selection.end) <= 0 ); } - if (startDate) { - return date.equals(startDate); + if (selection.start) { + return date.equals(selection.start); } return false; } @@ -241,32 +236,32 @@ export function getSelectionType( if (isPlainDate(selection)) { return date.equals(selection) ? 'selected' : null; } - if (selection.length === 0) { + if (!selection.start && !selection.end) { return null; } - const [startDate, endDate] = sortDateRange(selection); if ( - endDate || - (hoveredDate && Temporal.PlainDate.compare(hoveredDate, startDate) > 0) + selection.end || + (hoveredDate && + Temporal.PlainDate.compare(hoveredDate, selection.start) > 0) ) { - const laterDate = (endDate || hoveredDate) as Temporal.PlainDate; - if (date.equals(startDate) && date.equals(laterDate)) { + const laterDate = (selection.end || hoveredDate) as Temporal.PlainDate; + if (date.equals(selection.start) && date.equals(laterDate)) { return 'selected'; } - if (date.equals(startDate)) { + if (date.equals(selection.start)) { return 'range-start'; } if (date.equals(laterDate)) { return 'range-end'; } if ( - Temporal.PlainDate.compare(date, startDate) > 0 && + Temporal.PlainDate.compare(date, selection.start) > 0 && Temporal.PlainDate.compare(date, laterDate) < 0 ) { return 'range-middle'; } } - if (date.equals(selection[0])) { + if (date.equals(selection.start)) { return 'selected'; } return null; diff --git a/packages/circuit-ui/index.ts b/packages/circuit-ui/index.ts index b75001c063..e7883e33f7 100644 --- a/packages/circuit-ui/index.ts +++ b/packages/circuit-ui/index.ts @@ -66,6 +66,7 @@ export { ImageInput } from './components/ImageInput/index.js'; export type { ImageInputProps } from './components/ImageInput/index.js'; export { Calendar } from './components/Calendar/index.js'; export type { CalendarProps } from './components/Calendar/index.js'; +export { updatePlainDateRange } from './util/date.js'; export type { PlainDateRange } from './util/date.js'; // Actions diff --git a/packages/circuit-ui/util/date.spec.ts b/packages/circuit-ui/util/date.spec.ts index cce3a93789..fd7830bc4c 100644 --- a/packages/circuit-ui/util/date.spec.ts +++ b/packages/circuit-ui/util/date.spec.ts @@ -22,9 +22,8 @@ import { getLastDateOfWeek, getMonthName, isPlainDate, - sortDateRange, toPlainDate, - type PlainDateRange, + updatePlainDateRange, } from './date.js'; describe('CalendarService', () => { @@ -71,18 +70,6 @@ describe('CalendarService', () => { }); }); - describe('sortDateRange', () => { - it('should sort the start and end date in ascending order', () => { - const dateRange: PlainDateRange = [ - new Temporal.PlainDate(2020, 3, 25), - new Temporal.PlainDate(2020, 3, 15), - ]; - const actual = sortDateRange(dateRange); - expect(actual[0].toString()).toBe('2020-03-15'); - expect(actual[1].toString()).toBe('2020-03-25'); - }); - }); - describe('clampDate', () => { it('should return the date if it is within the range', () => { const date = new Temporal.PlainDate(2020, 3, 5); @@ -125,6 +112,60 @@ describe('CalendarService', () => { }); }); + describe('updatePlainDateRange', () => { + it('should set the start date if the range is empty', () => { + const previousRange = { start: undefined, end: undefined }; + const date = new Temporal.PlainDate(2020, 3, 11); + const actual = updatePlainDateRange(previousRange, date); + expect(actual.start).toEqual(date); + expect(actual.end).toBeUndefined(); + }); + + it('should start a new range if the range is complete', () => { + const previousRange = { + start: new Temporal.PlainDate(2020, 3, 9), + end: new Temporal.PlainDate(2020, 3, 15), + }; + const date = new Temporal.PlainDate(2020, 3, 11); + const actual = updatePlainDateRange(previousRange, date); + expect(actual.start).toEqual(date); + expect(actual.end).toBeUndefined(); + }); + + it('should set a new start date if the date is before the start date', () => { + const previousRange = { + start: new Temporal.PlainDate(2020, 3, 9), + end: undefined, + }; + const date = new Temporal.PlainDate(2020, 3, 5); + const actual = updatePlainDateRange(previousRange, date); + expect(actual.start).toEqual(date); + expect(actual.end).toBeUndefined(); + }); + + it('should set the end date if the date is equal to the start date', () => { + const previousRange = { + start: new Temporal.PlainDate(2020, 3, 9), + end: undefined, + }; + const date = new Temporal.PlainDate(2020, 3, 9); + const actual = updatePlainDateRange(previousRange, date); + expect(actual.start).toEqual(previousRange.start); + expect(actual.end).toEqual(date); + }); + + it('should set the end date if the date is after the start date', () => { + const previousRange = { + start: new Temporal.PlainDate(2020, 3, 9), + end: undefined, + }; + const date = new Temporal.PlainDate(2020, 3, 11); + const actual = updatePlainDateRange(previousRange, date); + expect(actual.start).toEqual(previousRange.start); + expect(actual.end).toEqual(date); + }); + }); + describe('getFirstDateOfWeek', () => { it('should return the first date of the week for a date', () => { const date = new Temporal.PlainDate(2020, 3, 28); // Saturday diff --git a/packages/circuit-ui/util/date.ts b/packages/circuit-ui/util/date.ts index 52b537ddd8..ef38ec19ea 100644 --- a/packages/circuit-ui/util/date.ts +++ b/packages/circuit-ui/util/date.ts @@ -21,9 +21,9 @@ import type { Locale } from './i18n.js'; export type FirstDayOfWeek = 1 | 7; export type DaysInWeek = number; export type PlainDateRange = - | [] - | [Temporal.PlainDate] - | [Temporal.PlainDate, Temporal.PlainDate]; + | { start: undefined; end: undefined } + | { start: Temporal.PlainDate; end: undefined } + | { start: Temporal.PlainDate; end: Temporal.PlainDate }; // ISO 8601 timestamps only support positive 4-digit years export const MIN_YEAR = 1; @@ -54,10 +54,6 @@ export function toPlainDate(date?: string): Temporal.PlainDate | undefined { } } -export function sortDateRange(dateRange: T): T { - return dateRange.sort((a, b) => Temporal.PlainDate.compare(a, b)) as T; -} - export function clampDate( date: Temporal.PlainDate, minDate?: Temporal.PlainDate | null, @@ -72,6 +68,23 @@ export function clampDate( return date; } +export function updatePlainDateRange( + previousRange: PlainDateRange, + date: Temporal.PlainDate, +): PlainDateRange { + if ( + // Nothing selected yet + (!previousRange.start && !previousRange.end) || + // Full range already selected + (previousRange.start && previousRange.end) || + // Selected date is before previous start date + Temporal.PlainDate.compare(previousRange.start, date) > 0 + ) { + return { start: date, end: undefined }; + } + return { start: previousRange.start, end: date }; +} + export function getFirstDateOfWeek( date: Temporal.PlainDate, firstDayOfWeek: FirstDayOfWeek,