Skip to content

Latest commit

 

History

History
351 lines (291 loc) · 12 KB

File metadata and controls

351 lines (291 loc) · 12 KB

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

import docs from 'docs:react-aria-components'; import vanillaDocs from 'docs:vanilla-starter/RangeCalendar'; import {RangeCalendar as VanillaRangeCalendar} from 'vanilla-starter/RangeCalendar'; import {RangeCalendar as TailwindRangeCalendar} from 'tailwind-starter/RangeCalendar'; import '../../tailwind/tailwind.css'; import Anatomy from '@react-aria/calendar/docs/rangecalendar-anatomy.svg';

export const tags = ['calendar'];

RangeCalendar

{docs.exports.RangeCalendar.description}

Value

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

"use client";
import {parseDate, getLocalTimeZone} from '@internationalized/date';
import {useDateFormatter} from 'react-aria';
import {RangeCalendar} from 'vanilla-starter/RangeCalendar';
import {useState} from 'react';

function Example() {
  let [range, setRange] = useState({
    start: parseDate('2025-02-03'),
    end: parseDate('2025-02-12')
  });
  let formatter = useDateFormatter({ dateStyle: 'long' });
  
  return (
    <>
      <RangeCalendar
        ///- begin highlight -///
        value={range}
        onChange={setRange}
        ///- end highlight -///
      />
      <p>Selected range: {formatter.formatRange(
        range.start.toDate(getLocalTimeZone()),
        range.end.toDate(getLocalTimeZone())
      )}</p>
    </>
  );
}

International calendars

By default, RangeCalendar displays the value using the calendar system for the user's locale. Use <I18nProvider> 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.

"use client";
import {I18nProvider} from 'react-aria-components';
import {parseDate} from '@internationalized/date';
import {RangeCalendar} from 'vanilla-starter/RangeCalendar';

<I18nProvider/* PROPS */>
  <RangeCalendar
    defaultValue={{
      start: parseDate('2025-02-03'),
      end: parseDate('2025-02-12')
    }} />
</I18nProvider>

Custom calendar systems

RangeCalendar 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.

"use client";
import type {AnyCalendarDate} from '@internationalized/date';
import {CalendarDate, startOfWeek, toCalendar, GregorianCalendar} from '@internationalized/date';
import {RangeCalendar} from 'vanilla-starter/RangeCalendar';

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

// See @internationalized/date docs linked above.
///- begin collapse -///
class Custom454 extends GregorianCalendar {
  weekPattern = [4, 5, 4, 4, 5, 4, 4, 5, 4, 4, 5, 4];
  getDaysInMonth(date) {
    return this.weekPattern[date.month - 1] * 7;
  }

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

    let monthStart = startOfWeek(new CalendarDate(gregorian.year, 1, 1), 'en');
    for (let months = 0; months < this.weekPattern.length; months++) {
      let weeksInMonth = this.weekPattern[months];
      let monthEnd = monthStart.add({weeks: weeksInMonth});
      if (monthEnd.compare(gregorian) > 0) {
        let days = gregorian.compare(monthStart);
        return new CalendarDate(this, monthStart.year, months + 1, days + 1);
      }
      monthStart = monthEnd;
    }

    throw Error('Date is not in any month somehow!');
  }

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

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

  getFormattableMonth(date) {
    let gregorian = toCalendar(date, new GregorianCalendar());
    return gregorian.set({month: date.month, day: 1});
  }

  isEqual(other) {
    return other instanceof Custom454;
  }
}
///- end collapse -///

Validation

Use the minValue and maxValue props to set the valid date range. The isDateUnavailable callback prevents certain dates from being selected. Use allowsNonContiguousRanges to allow selecting ranges containing unavailable dates. For custom validation rules, set the isInvalid prop and the errorMessage slot.

"use client";
import {today, getLocalTimeZone} from '@internationalized/date';
import {useLocale} from 'react-aria';
import {RangeCalendar} from 'vanilla-starter/RangeCalendar';
import {useState} from 'react';

function Example(props) {
  let {locale} = useLocale();
  let now = today(getLocalTimeZone());
  let [range, setRange] = useState({
    start: now.add({days: 6}),
    end: now.add({ days: 14 })
  });
  let disabledRanges = [
    [now, now.add({ days: 5 })],
    [now.add({ days: 15 }), now.add({ days: 17 })],
    [now.add({ days: 23 }), now.add({ days: 24 })]
  ];
  let isInvalid = range.end.compare(range.start) > 7;

  return (
    <RangeCalendar
      {...props}
      aria-label="Trip dates"
      value={range}
      onChange={setRange}
      ///- begin highlight -///
      /* PROPS */
      minValue={today(getLocalTimeZone())}
      isDateUnavailable={date => (
        disabledRanges.some((interval) =>
          date.compare(interval[0]) >= 0 && date.compare(interval[1]) <= 0
        )
      )}
      isInvalid={isInvalid}
      errorMessage={isInvalid ? 'Maximum stay duration is 1 week' : undefined} />
      ///- end highlight -///
  );
}

Display options

Set the visibleDuration prop and render multiple CalendarGrid elements to display more than one month at a time. The pageBehavior prop controls whether pagination advances by a single month or multiple. The firstDayOfWeek prop overrides the locale-specified first day of the week.

"use client";
import {RangeCalendar, Heading, Button, CalendarGrid, CalendarCell} from 'react-aria-components';
import {useDateFormatter} from 'react-aria';

// TODO: move this into the starter example.
function Example(props) {
  let monthFormatter = useDateFormatter({
    month: 'long',
    year: 'numeric',
  });

  return (
    <RangeCalendar
      {...props}
      aria-label="Trip dates"
      ///- begin highlight -///
      /* PROPS */
      ///- end highlight -///
      style={{display: 'flex', gap: 30, overflow: 'auto'}}
    >
      {({state}) => (
        [...Array(props.visibleDuration.months).keys()].map(i => (
          <div key={i} style={{flex: 1}}>
            <header>
              {i === 0 && 
                <Button slot="previous"></Button>
              }
              <h2 style={{flex: 1, textAlign: 'center'}}>{monthFormatter.format(state.visibleRange.start.add({months: i}).toDate(state.timeZone))}</h2>
              {i === props.visibleDuration.months - 1 && 
                <Button slot="next"></Button>
              }
            </header>
            <CalendarGrid offset={{months: i}}>
              {date => <CalendarCell date={date} />}
            </CalendarGrid>
          </div>
        ))
      )}
    </RangeCalendar>
  );
}

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.

"use client";
import {RangeCalendar} from 'vanilla-starter/RangeCalendar';
import {Button} from 'vanilla-starter/Button';
import {CalendarDate, today, getLocalTimeZone} from '@internationalized/date';
import {useState} from 'react';

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

  return (
    <div>
      <Button
        style={{marginBottom: 20}}
        onPress={() => setFocusedDate(today(getLocalTimeZone()))}>
        Today
      </Button>
      <RangeCalendar
        ///- begin highlight -///
        focusedValue={focusedDate}
        onFocusChange={setFocusedDate}
        ///- end highlight -///
      />
    </div>
  );
}

Month and year pickers

You can also control the focused date via CalendarStateContext. This example shows month and year dropdown components that work inside any <RangeCalendar>.

"use client";
import {RangeCalendar, CalendarGrid, CalendarCell} from 'react-aria-components';
import {MonthDropdown} from './MonthDropdown';
import {YearDropdown} from './YearDropdown';
import {Button} from 'vanilla-starter/Button';

<RangeCalendar>
  <header style={{display: 'flex', gap: 4}}>
    <Button slot="previous"></Button>
    {/*- begin highlight -*/}
    <MonthDropdown />
    <YearDropdown />
    {/*- end highlight -*/}
    <Button slot="next"></Button>
  </header>
  <CalendarGrid>
    {(date) => <CalendarCell date={date} />}
  </CalendarGrid>
</RangeCalendar>

API

<RangeCalendar>
  <Button slot="previous" />
  <Heading />
  <Button slot="next" />
  <CalendarGrid>
    <CalendarGridHeader>
      {day => <CalendarHeaderCell />}
    </CalendarGridHeader>
    <CalendarGridBody>
      {date => <CalendarCell date={date} />}
    </CalendarGridBody>
  </CalendarGrid>
  <Text slot="errorMessage" />
</RangeCalendar>

RangeCalendar

CalendarGrid

CalendarGridHeader

CalendarHeaderCell

CalendarGridBody

CalendarCell