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,