Back to React Spectrum

Calendar

packages/dev/s2-docs/pages/s2/Calendar.mdx

2022-12-167.7 KB
Original Source

import {Layout} from '../../src/Layout'; export default Layout;

import {Calendar} from '@react-spectrum/s2'; import docs from 'docs:@react-spectrum/s2';

export const tags = ['date']; export const description = 'Allows a user to select a single date from a date grid.';

Calendar

<PageDescription>{docs.exports.Calendar.description}</PageDescription>

<VisualExample component={Calendar} docs={docs.exports.Calendar} links={docs.links} props={['visibleMonths', 'pageBehavior', 'firstDayOfWeek', 'isDisabled']} initialProps={{'aria-label': 'Event date'}} controlOptions={{ visibleMonths: { minValue: 1, maxValue: 3 } }} importSource="@react-spectrum/s2" type="s2" wide />

Value

Use the value or defaultValue prop to set the date value, using objects in the @internationalized/date package. This library supports parsing date strings in multiple formats, manipulation across international calendar systems, time zones, etc.

tsx
"use client";
import {parseDate, getLocalTimeZone} from '@internationalized/date';
import {Calendar} from '@react-spectrum/s2';
import {useState} from 'react';

function Example() {
  let [date, setDate] = useState(parseDate('2020-02-03'));
  
  return (
    <>
      <Calendar
        ///- begin highlight -///
        value={date}
        onChange={setDate}
        ///- end highlight -///
      />
      <p>Selected date: {date.toDate(getLocalTimeZone()).toLocaleDateString()}</p>
    </>
  );
}

International calendars

By default, Calendar displays the value using the calendar system for the user's locale. Use <Provider> to override the calendar system by setting the Unicode calendar locale extension. The onChange event always receives a date in the same calendar as the value or defaultValue (Gregorian if no value is provided), regardless of the displayed locale.

tsx
"use client";
import {Provider, Calendar} from '@react-spectrum/s2';
import {parseDate} from '@internationalized/date';

<Provider/* PROPS */>
  <Calendar defaultValue={parseDate('2025-02-03')} />
</Provider>

Custom calendar systems

Calendar also supports custom calendar systems that implement custom business rules, for example a fiscal year calendar that follows a 4-5-4 format, where month ranges don't follow the usual Gregorian calendar. See the @internationalized/date docs for an example implementation.

tsx
"use client";
import type {AnyCalendarDate, Calendar as ICalendar} from '@internationalized/date';
import {CalendarDate, startOfWeek, GregorianCalendar} from '@internationalized/date';
import {Calendar} from '@react-spectrum/s2';

export default function Example() {
  return (
    <Calendar
      firstDayOfWeek="sun"
      ///- begin highlight -///
      createCalendar={() => new Custom454()} />
      ///- end highlight -///
  );
}

// See @internationalized/date docs linked above.
///- begin collapse -///
class Custom454 extends GregorianCalendar {
  // The anchor date, in Gregorian calendar.
  // The anchor date is a date that occurs in the first week of the first month of every fiscal year.
  anchorDate = new CalendarDate(2001, 2, 4);

  private getYear(year: number): [CalendarDate, number[]] {
    let anchor = this.anchorDate.set({year});
    let startOfYear = startOfWeek(anchor, 'en', 'sun');
    let isBigYear = !startOfYear.add({weeks: 53}).compare(anchor.add({years: 1}));
    let weekPattern = [4, 5, 4, 4, 5, 4, 4, 5, 4, 4, 5, isBigYear ? 5 : 4];
    return [startOfYear, weekPattern];
  }

  getDaysInMonth(date: AnyCalendarDate): number {
    let [, weekPattern] = this.getYear(date.year);
    return weekPattern[date.month - 1] * 7;
  }

  fromJulianDay(jd: number): CalendarDate {
    let gregorian = super.fromJulianDay(jd);
    let year = gregorian.year;

    let [monthStart, weekPattern] = this.getYear(year);
    if (gregorian.compare(monthStart) < 0) {
      year--;
      [monthStart, weekPattern] = this.getYear(year);
    }

    for (let month = 1; month <= 12; month++) {
      let weeks = weekPattern[month - 1];
      let nextMonth = monthStart.add({weeks});
      if (nextMonth.compare(gregorian) > 0) {
        let days = gregorian.compare(monthStart);
        return new CalendarDate(this, year, month, days + 1);
      }
      monthStart = nextMonth;
    }

    throw new Error('date not found');
  }

  toJulianDay(date: AnyCalendarDate): number {
    let [monthStart, weekPattern] = this.getYear(date.year);
    for (let month = 1; month < date.month; month++) {
      monthStart = monthStart.add({weeks: weekPattern[month - 1]});
    }

    let gregorian = monthStart.add({days: date.day - 1});
    return super.toJulianDay(gregorian);
  }

  getFormattableMonth(date: AnyCalendarDate): CalendarDate {
    let anchorMonth = this.anchorDate.month - 1;
    let dateMonth = date.month - 1;
    let month = ((anchorMonth + dateMonth) % 12) + 1;
    let year = anchorMonth + dateMonth >= 12 ? date.year + 1 : date.year;
    return new CalendarDate(year, month, 1);
  }

  isEqual(other: ICalendar): boolean {
    return other instanceof Custom454 && other.anchorDate.compare(this.anchorDate) === 0;
  }
}
///- end collapse -///

Validation

Use the minValue and maxValue props to set the valid date range. The isDateUnavailable callback prevents certain dates from being selected. For custom validation rules, set the isInvalid and errorMessage props.

tsx
"use client";
import {isWeekend, today, getLocalTimeZone} from '@internationalized/date';
import {Calendar, useLocale} from '@react-spectrum/s2';

function Example(props) {
  let {locale} = useLocale();
  let now = today(getLocalTimeZone());
  let disabledRanges = [
    [now, now.add({ days: 5 })],
    [now.add({ days: 14 }), now.add({ days: 16 })],
    [now.add({ days: 23 }), now.add({ days: 24 })]
  ];

  return (
    <Calendar
      {...props}
      aria-label="Appointment date"
      ///- begin highlight -///
      /* PROPS */
      minValue={today(getLocalTimeZone())}
      isDateUnavailable={date => (
        isWeekend(date, locale) ||
        disabledRanges.some((interval) =>
          date.compare(interval[0]) >= 0 && date.compare(interval[1]) <= 0
        )
      )} />
      ///- end highlight -///
  );
}

Controlling the focused date

Use the focusedValue or defaultFocusedValue prop to control which date is focused. This controls which month is visible. The onFocusChange event is called when a date is focused by the user.

tsx
"use client";
import {Calendar, ActionButton} from '@react-spectrum/s2';
import {CalendarDate, today, getLocalTimeZone} from '@internationalized/date';
import {style} from '@react-spectrum/s2/style' with {type: 'macro'};
import {useState} from 'react';

function Example() {
  let defaultDate = new CalendarDate(2021, 7, 1);
  let [focusedDate, setFocusedDate] = useState(defaultDate);

  return (
    <div className={style({display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 8})}>
      <ActionButton onPress={() => setFocusedDate(today(getLocalTimeZone()))}>
        Today
      </ActionButton>
      <Calendar
        ///- begin highlight -///
        focusedValue={focusedDate}
        onFocusChange={setFocusedDate}
        ///- end highlight -///
      />
    </div>
  );
}

API

<PropTable component={docs.exports.Calendar} links={docs.links} />