Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Calendar and DatePicker audits and chromatic stories #3046

Merged
merged 19 commits into from
Apr 21, 2022
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .chromatic/custom-addons/chromatic/disableAnimations.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.disableAnimations * {
transition: none !important;
snowystinger marked this conversation as resolved.
Show resolved Hide resolved
}
1 change: 1 addition & 0 deletions .chromatic/custom-addons/chromatic/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {locales, scales, themes} from '../../constants';
import {makeDecorator} from '@storybook/addons';
import {Provider, View} from '@adobe/react-spectrum';
import React, {useEffect} from 'react';
import './disableAnimations.css';

export const withChromaticProvider = makeDecorator({
name: 'withChromaticProvider',
Expand Down
5 changes: 4 additions & 1 deletion .chromatic/webpack-chromatic.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ const generateScopedName = (localName, resourcePath) => {

module.exports = () => {
return {
plugins: [new webpack.DefinePlugin({REACT_VERSION: JSON.stringify(reactVersion)})],
plugins: [
new webpack.DefinePlugin({REACT_VERSION: JSON.stringify(reactVersion)}),
new webpack.EnvironmentPlugin(['NODE_ENV', 'CHROMATIC'])
],
parallelism: 1,
module: {
rules: [
Expand Down
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
"start": "cross-env NODE_ENV=storybook start-storybook -p 9003 --ci -c '.storybook'",
"build:storybook": "build-storybook -c .storybook -o dist/$(git rev-parse HEAD)/storybook",
"build:storybook-16": "build-storybook -c .storybook -o dist/$(git rev-parse HEAD)/storybook-16",
"start:chromatic": "NODE_ENV=storybook start-storybook -p 9004 --ci -c '.chromatic'",
"build:chromatic": "build-storybook -c .chromatic -o dist/$(git rev-parse HEAD)/chromatic",
"start:chromatic": "CHROMATIC=1 NODE_ENV=storybook start-storybook -p 9004 --ci -c '.chromatic'",
"build:chromatic": "CHROMATIC=1 build-storybook -c .chromatic -o dist/$(git rev-parse HEAD)/chromatic",
"start:docs": "DOCS_ENV=dev parcel 'packages/@react-{spectrum,aria,stately}/*/docs/*.mdx' 'packages/@internationalized/*/docs/*.mdx' 'packages/dev/docs/pages/**/*.mdx'",
"build:docs": "DOCS_ENV=staging parcel build 'packages/@react-{spectrum,aria,stately}/*/docs/*.mdx' 'packages/@internationalized/*/docs/*.mdx' 'packages/dev/docs/pages/**/*.mdx'",
"test": "yarn jest",
Expand All @@ -29,7 +29,7 @@
"clean:icons": "babel-node --presets @babel/env ./scripts/cleanIcons.js",
"postinstall": "yarn build:icons && patch-package",
"plop": "plop --plopfile scripts/plopfile.js",
"chromatic": "chromatic --project-token $CHROMATIC_PROJECT_TOKEN --build-script-name 'build:chromatic'",
"chromatic": "CHROMATIC=1 chromatic --project-token $CHROMATIC_PROJECT_TOKEN --build-script-name 'build:chromatic'",
"merge:css": "babel-node --presets @babel/env ./scripts/merge-spectrum-css.js",
"release": "lerna publish from-package --yes",
"publish:nightly": "lerna publish -y --canary --preid nightly --dist-tag=nightly --exact --force-publish=* --no-push",
Expand Down Expand Up @@ -163,7 +163,7 @@
"storybook-dark-mode": "^1.0.3",
"style-loader": "^0.23.1",
"tempy": "^0.5.0",
"typescript": "^4.5.2",
"typescript": "^4.6.0",
"url-loader": "^1.1.2",
"walk-object": "^4.0.0",
"webpack": "^4.44.2",
Expand Down
43 changes: 42 additions & 1 deletion packages/@internationalized/date/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,44 @@
# @internationalized/date

This package is part of [react-spectrum](https://github.com/adobe/react-spectrum). See the repo for more details.
The `@internationalized/date` package provides objects and functions for representing and manipulating dates and times in a locale-aware manner.

## Features

* **Typed objects** – Includes immutable objects to represent dates, times, calendars, and more.
* **International calendars** – Support for 13 calendar systems used around the world, including Gregorian, Buddhist, Islamic, Persian, and more.
* **Manipulation** – Add and subtract durations, set and cycle fields, and more.
* **Conversion** – Convert between calendar systems, time zones, string representations, and object types.
* **Queries** – Compare dates and times for ordering or full/partial equality. Determine locale-specific metadata such as day of week, weekend/weekday, etc.
* **Time zone aware** – The [ZonedDateTime](https://react-specrum.adobe.com/internationalized/date/ZonedDateTime.html) object supports time zone aware date and time manipulation.
* **Predictable** – The API is designed to resolve ambiguity in all operations explicitly, including time zone conversions, arithmetic involving daylight saving time, locale-specific queries, and more.
* **Small bundle size** – The entire library including all calendars and functions is 8 kB minified and compressed with Brotli.
* **Tree shakeable** – Only include the functions and calendar systems you need. For example, if you only use the Gregorian calendar and builtin `CalendarDate` methods, it's just 2.8 kB.

## Introduction

Dates and times are represented in many different ways by cultures around the world. This includes differences in calendar systems, time zones, daylight saving time rules, date and time formatting, weekday and weekend rules, and much more. When building applications that support users around the world, it is important to handle these aspects correctly for each locale. The `@internationalized/date` package provides a library of objects and functions to perform date and time related manipulation, queries, and conversions that work across locales and calendars.

By default, JavaScript represents dates and times using the [Date](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date) object. However, `Date` has _many_ problems, including a very difficult to use API, lack of all internationalization support, and more. The [Temporal](https://tc39.es/proposal-temporal/docs/index.html) proposal will eventually address this in the language, and `@internationalized/date` is heavily inspired by it. We hope to back the objects in this package with it once it is implemented in browsers.

## Package structure

The `@internationalized/date` package includes the following object types:

* [Calendar](https://react-specrum.adobe.com/internationalized/date/Calendar.html) – An interface which provides calendar conversion and metadata like number of days in month, and number of months in year. Many implementations are provided to support the most commonly used calendar systems.
* [CalendarDate](https://react-specrum.adobe.com/internationalized/date/CalendarDate.html) – An immutable object that stores a date associated with a specific calendar system, without any time components.
* [CalendarDateTime](https://react-specrum.adobe.com/internationalized/date/CalendarDateTime.html) – An immutable object that represents a date and time without a time zone, in a specific calendar system.
* [ZonedDateTime](https://react-specrum.adobe.com/internationalized/date/ZonedDateTime.html) – An immutable object that represents a date and time in a specific time zone and calendar system.
* [Time](https://react-specrum.adobe.com/internationalized/date/Time.html) – An immutable object that stores a clock time without any date components.

Each object includes methods to allow basic manipulation and conversion functionality, such as adding and subtracting durations, and formatting as an ISO 8601 string. Additional less commonly used functions can be imported from the `@internationalized/date` package, and passed a date object as a parameter. This includes functions to parse ISO 8601 strings, query properties such as day of week, convert between time zones and much more. See the documentation for each of the objects to learn more about the supported methods and functions.

This example constructs a `CalendarDate` object, manipulates it to get the start of the next week, and converts it to a string representation.

```tsx
import {CalendarDate, startOfWeek} from '@internationalized/date';

let date = new CalendarDate(2022, 2, 3);
date = date.add({weeks: 1});
date = startOfWeek(date, 'en-US');
date.toString(); // 2022-02-06
```
2 changes: 1 addition & 1 deletion packages/@internationalized/date/docs/Calendar.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ after_version: 3.0.0

## Introduction

While the Gregorian calendar is the most commonly used, many other calendar systems are used throughout the world. The `Calendar` interface is used to represent calendar systems in the `@internationalized/date` library. It encapsulates information such as the number of days in a month, the number of months in a year, and the list of eras in a calendar system, as well as methods that handle correct arithmetic of dates in that calendar system, as well as converting dates between calendar systems. Many implementations of this interface are provided in `@internationalized/date` to handle the most commonly used calendar systems.
While the Gregorian calendar is the most common, many other calendar systems are used throughout the world. The `Calendar` interface represents calendar systems in the `@internationalized/date` library. It encapsulates information such as the number of days in a month, the number of months in a year, and the list of eras in a calendar system, as well as methods that handle correct arithmetic of dates in that calendar system, as well as converting dates between calendar systems. Many implementations of this interface are provided in `@internationalized/date` to handle the most commonly used calendar systems.

As described in the docs for [CalendarDate](CalendarDate.html#calendar-systems) and other date objects, you can pass a `Calendar` instance to a date to represent a date in that calendar. Date manipulation follows the rules defined by that calendar system. You can also convert between calendar systems using the <TypeLink links={docs.links} type={docs.exports.toCalendar} /> function.

Expand Down
4 changes: 2 additions & 2 deletions packages/@internationalized/date/docs/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ The `@internationalized/date` package provides objects and functions for represe

## Features

* **Typed objects** – Includes immutable objects to represent dates, times, calendars, and more.
* **Typed objects** – Includes immutable objects to represent dates, times, and calendars.
* **International calendars** – Support for 13 calendar systems used around the world, including Gregorian, Buddhist, Islamic, Persian, and more.
* **Manipulation** – Add and subtract durations, set and cycle fields, and more.
* **Manipulation** – Add and subtract durations, set and cycle fields, etc.
* **Conversion** – Convert between calendar systems, time zones, string representations, and object types.
* **Queries** – Compare dates and times for ordering or full/partial equality. Determine locale-specific metadata such as day of week, weekend/weekday, etc.
* **Time zone aware** – The [ZonedDateTime](ZonedDateTime.html) object supports time zone aware date and time manipulation.
Expand Down
4 changes: 2 additions & 2 deletions packages/@internationalized/date/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@internationalized/date",
"version": "3.0.0-alpha.4",
"description": "Internationalized calendar and date manipulation utilities",
"description": "Internationalized calendar, date, and time manipulation utilities",
"license": "Apache-2.0",
"main": "dist/main.js",
"module": "dist/module.js",
Expand All @@ -14,7 +14,7 @@
"sideEffects": false,
"repository": {
"type": "git",
"url": "https://github.com/adobe/react-spectrum"
"url": "https://github.com/adobe/react-spectrum/tree/main/packages/@internationalized/date"
},
"dependencies": {
"@babel/runtime": "^7.6.2"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ export class JapaneseCalendar extends GregorianCalendar {
let era = findEraFromGregorianDate(date);
date.era = ERA_NAMES[era];
date.year -= ERA_ADDENDS[era];
this.constrainDate(date);
return date as CalendarDate;
}

Expand All @@ -93,6 +94,9 @@ export class JapaneseCalendar extends GregorianCalendar {
date.era = ERA_NAMES[era];
date.year = gregorianDate.year - ERA_ADDENDS[era];
}

// Constrain in case we went before the first supported era.
this.constrainDate(date);
}

constrainDate(date: Mutable<AnyCalendarDate>) {
Expand All @@ -104,7 +108,7 @@ export class JapaneseCalendar extends GregorianCalendar {
// Constrain the year to the maximum possible value in the era.
// Then constrain the month and day fields within that.
let maxYear = endYear - ERA_ADDENDS[idx];
date.year = Math.min(maxYear, date.year);
date.year = Math.max(1, Math.min(maxYear, date.year));
if (date.year === maxYear) {
date.month = Math.min(endMonth, date.month);

Expand Down
1 change: 1 addition & 0 deletions packages/@internationalized/date/src/createCalendar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {JapaneseCalendar} from './calendars/JapaneseCalendar';
import {PersianCalendar} from './calendars/PersianCalendar';
import {TaiwanCalendar} from './calendars/TaiwanCalendar';

/** Creates a `Calendar` instance from a Unicode calendar identifier string. */
export function createCalendar(name: string): Calendar {
switch (name) {
case 'buddhist':
Expand Down
12 changes: 6 additions & 6 deletions packages/@internationalized/date/src/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,39 +127,39 @@ export function getLocalTimeZone(): string {
return localTimeZone;
}

/** Returns the first date of the month for the given date. */
export function startOfMonth(date: ZonedDateTime): ZonedDateTime;
export function startOfMonth(date: CalendarDateTime): CalendarDateTime;
export function startOfMonth(date: CalendarDate): CalendarDate;
export function startOfMonth(date: DateValue): DateValue;
/** Returns the first date of the month for the given date. */
export function startOfMonth(date: DateValue): DateValue {
// Use `subtract` instead of `set` so we don't get constrained in an era.
return date.subtract({days: date.day - 1});
}

/** Returns the last date of the month for the given date. */
export function endOfMonth(date: ZonedDateTime): ZonedDateTime;
export function endOfMonth(date: CalendarDateTime): CalendarDateTime;
export function endOfMonth(date: CalendarDate): CalendarDate;
export function endOfMonth(date: DateValue): DateValue;
/** Returns the last date of the month for the given date. */
export function endOfMonth(date: DateValue): DateValue {
return date.add({days: date.calendar.getDaysInMonth(date) - date.day});
}

/** Returns the first day of the year for the given date. */
export function startOfYear(date: ZonedDateTime): ZonedDateTime;
export function startOfYear(date: CalendarDateTime): CalendarDateTime;
export function startOfYear(date: CalendarDate): CalendarDate;
export function startOfYear(date: DateValue): DateValue;
/** Returns the first day of the year for the given date. */
export function startOfYear(date: DateValue): DateValue {
return startOfMonth(date.subtract({months: date.month - 1}));
}

/** Returns the last day of the year for the given date. */
export function endOfYear(date: ZonedDateTime): ZonedDateTime;
export function endOfYear(date: CalendarDateTime): CalendarDateTime;
export function endOfYear(date: CalendarDate): CalendarDate;
export function endOfYear(date: DateValue): DateValue;
/** Returns the last day of the year for the given date. */
export function endOfYear(date: DateValue): DateValue {
return endOfMonth(date.add({months: date.calendar.getMonthsInYear(date) - date.month}));
}
Expand All @@ -180,20 +180,20 @@ export function getMinimumDayInMonth(date: AnyCalendarDate) {
return 1;
}

/** Returns the first date of the week for the given date and locale. */
export function startOfWeek(date: ZonedDateTime, locale: string): ZonedDateTime;
export function startOfWeek(date: CalendarDateTime, locale: string): CalendarDateTime;
export function startOfWeek(date: CalendarDate, locale: string): CalendarDate;
export function startOfWeek(date: DateValue, locale: string): DateValue;
/** Returns the first date of the week for the given date and locale. */
export function startOfWeek(date: DateValue, locale: string): DateValue {
let dayOfWeek = getDayOfWeek(date, locale);
return date.subtract({days: dayOfWeek});
}

/** Returns the last date of the week for the given date and locale. */
export function endOfWeek(date: ZonedDateTime, locale: string): ZonedDateTime;
export function endOfWeek(date: CalendarDateTime, locale: string): CalendarDateTime;
export function endOfWeek(date: CalendarDate, locale: string): CalendarDate;
/** Returns the last date of the week for the given date and locale. */
export function endOfWeek(date: DateValue, locale: string): DateValue {
return startOfWeek(date, locale).add({days: 6});
}
Expand Down
17 changes: 17 additions & 0 deletions packages/@internationalized/date/src/string.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const DATE_TIME_RE = /^(\d{4})-(\d{2})-(\d{2})(?:T(\d{2}))?(?::(\d{2}))?(?::(\d{
const ZONED_DATE_TIME_RE = /^(\d{4})-(\d{2})-(\d{2})(?:T(\d{2}))?(?::(\d{2}))?(?::(\d{2}))?(\.\d+)?(?:([+-]\d{2})(?::(\d{2}))?)?\[(.*?)\]$/;
const ABSOLUTE_RE = /^(\d{4})-(\d{2})-(\d{2})(?:T(\d{2}))?(?::(\d{2}))?(?::(\d{2}))?(\.\d+)?(?:(?:([+-]\d{2})(?::(\d{2}))?)|Z)$/;

/** Parses an ISO 8601 time string. */
export function parseTime(value: string): Time {
let m = value.match(TIME_RE);
if (!m) {
Expand All @@ -37,6 +38,7 @@ export function parseTime(value: string): Time {
);
}

/** Parses an ISO 8601 date string, with no time components. */
export function parseDate(value: string): CalendarDate {
let m = value.match(DATE_RE);
if (!m) {
Expand All @@ -53,6 +55,7 @@ export function parseDate(value: string): CalendarDate {
return date as CalendarDate;
}

/** Parses an ISO 8601 date and time string, with no time zone. */
export function parseDateTime(value: string): CalendarDateTime {
let m = value.match(DATE_TIME_RE);
if (!m) {
Expand All @@ -73,6 +76,12 @@ export function parseDateTime(value: string): CalendarDateTime {
return date as CalendarDateTime;
}

/**
* Parses an ISO 8601 date and time string with a time zone extension and optional UTC offset
* (e.g. "2021-11-07T00:45[America/Los_Angeles]" or "2021-11-07T00:45-07:00[America/Los_Angeles]").
* Ambiguous times due to daylight saving time transitions are resolved according to the `disambiguation`
* parameter.
*/
export function parseZonedDateTime(value: string, disambiguation?: Disambiguation): ZonedDateTime {
let m = value.match(ZONED_DATE_TIME_RE);
if (!m) {
Expand Down Expand Up @@ -113,6 +122,10 @@ export function parseZonedDateTime(value: string, disambiguation?: Disambiguatio
return fromAbsolute(ms, date.timeZone);
}

/**
* Parses an ISO 8601 date and time string with a UTC offset (e.g. "2021-11-07T07:45:00Z"
* or "2021-11-07T07:45:00-07:00"). The result is converted to the provided time zone.
*/
export function parseAbsolute(value: string, timeZone: string): ZonedDateTime {
let m = value.match(ABSOLUTE_RE);
if (!m) {
Expand Down Expand Up @@ -140,6 +153,10 @@ export function parseAbsolute(value: string, timeZone: string): ZonedDateTime {
return toTimeZone(date as ZonedDateTime, timeZone);
}

/**
* Parses an ISO 8601 date and time string with a UTC offset (e.g. "2021-11-07T07:45:00Z"
* or "2021-11-07T07:45:00-07:00"). The result is converted to the user's local time zone.
*/
export function parseAbsoluteToLocal(value: string): ZonedDateTime {
return parseAbsolute(value, getLocalTimeZone());
}
Expand Down
5 changes: 5 additions & 0 deletions packages/@internationalized/date/tests/conversion.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,11 @@ describe('CalendarDate conversion', function () {
date = new CalendarDate(new JapaneseCalendar(), 'reiwa', 2, 2, 5);
expect(date.calendar.getDaysInMonth(date)).toBe(29);
});

it('constrains dates outside supported eras', function () {
let date = new CalendarDate(1700, 4, 30);
expect(toCalendar(date, new JapaneseCalendar())).toEqual(new CalendarDate(new JapaneseCalendar(), 'meiji', 1, 9, 30));
});
});

describe('taiwan', function () {
Expand Down
8 changes: 8 additions & 0 deletions packages/@internationalized/date/tests/manipulation.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,14 @@ describe('CalendarDate manipulation', function () {
let date = new CalendarDate(new JapaneseCalendar(), 'reiwa', 1, 5, 1);
expect(date.subtract({days: 1})).toEqual(new CalendarDate(new JapaneseCalendar(), 'heisei', 31, 4, 30));
});

it('should constrain when reaching the minimum supported era', function () {
let date = new CalendarDate(new JapaneseCalendar(), 'meiji', 1, 9, 10);
expect(date.subtract({months: 1})).toEqual(new CalendarDate(new JapaneseCalendar(), 'meiji', 1, 9, 10));

date = new CalendarDate(new JapaneseCalendar(), 'meiji', 1, 9, 10);
expect(date.subtract({years: 1})).toEqual(new CalendarDate(new JapaneseCalendar(), 'meiji', 1, 9, 10));
});
});

describe('Taiwan calendar', function () {
Expand Down
2 changes: 1 addition & 1 deletion packages/@internationalized/message/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
},
"dependencies": {
"@babel/runtime": "^7.6.2",
"intl-messageformat": "^9.6.12"
"intl-messageformat": "^9.12.0"
},
"publishConfig": {
"access": "public"
Expand Down
Loading