nextui/packages/components/calendar/stories/calendar.stories.tsx
WK 0d217e466f
refactor: optimization (#5362)
* chore(deps): bump RA versions

* chore(deps): bump RA versions

* chore(deps): bump RA versions

* chore: changeset

* chore(deps): remove unnecessary dependencies

* fix(calendar): typing issue

* refactor(system): remove unused SupportedCalendars

* refactor(system): move I18nProviderProps to type

* refactor: use `spectrumCalendarProps<DateValue>["createCalendar"]`

* feat: add consistent-type-imports

* fix: eslint

* chore: add changeset

* refactor: remove unused deps
2025-06-09 14:17:44 +08:00

429 lines
10 KiB
TypeScript

import type {Meta} from "@storybook/react";
import type {CalendarProps, DateValue} from "../src";
import React from "react";
import {calendar} from "@heroui/theme";
import {
today,
parseDate,
getLocalTimeZone,
isWeekend,
startOfWeek,
startOfMonth,
} from "@internationalized/date";
import {I18nProvider, useLocale} from "@react-aria/i18n";
import {Button, ButtonGroup} from "@heroui/button";
import {Radio, RadioGroup} from "@heroui/radio";
import {cn} from "@heroui/theme";
import {HeroUIProvider} from "@heroui/system";
import {Calendar} from "../src";
export default {
title: "Components/Calendar",
component: Calendar,
parameters: {
layout: "centered",
},
argTypes: {
visibleMonths: {
control: {type: "number", min: 1, max: 3},
},
color: {
control: {
type: "select",
},
options: ["foreground", "primary", "secondary", "success", "warning", "danger"],
},
weekdayStyle: {
control: {
type: "select",
},
options: ["narrow", "short", "long"],
},
disableAnimation: {
control: {
type: "boolean",
},
},
firstDayOfWeek: {
control: "select",
options: [undefined, "sun", "mon", "tue", "wed", "thu", "fri", "sat"],
},
},
} as Meta<typeof Calendar>;
const defaultProps = {
...calendar.defaultVariants,
visibleMonths: 1,
};
const Template = (args: CalendarProps) => <Calendar {...args} />;
const ControlledTemplate = (args: CalendarProps) => {
let [value, setValue] = React.useState<DateValue | null>(parseDate("2024-03-07"));
return (
<div className="flex flex-wrap gap-4">
<div className="flex flex-col items-center gap-4">
<p className="text-small text-default-600">Date (uncontrolled)</p>
<Calendar
aria-label="Date (uncontrolled)"
defaultValue={parseDate("2024-03-07")}
{...args}
/>
</div>
<div className="flex flex-col items-center gap-4">
<p className="text-small text-default-600">Date (controlled)</p>
<Calendar
aria-label="Date (controlled)"
value={value}
onChange={setValue}
{...args}
color="secondary"
/>
</div>
</div>
);
};
const UnavailableDatesTemplate = (args: CalendarProps) => {
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})],
];
let {locale} = useLocale();
let isDateUnavailable = (date) =>
isWeekend(date, locale) ||
disabledRanges.some(
(interval) => date.compare(interval[0]) >= 0 && date.compare(interval[1]) <= 0,
);
return (
<Calendar
aria-label="Appointment date"
isDateUnavailable={isDateUnavailable}
minValue={today(getLocalTimeZone())}
{...args}
/>
);
};
const ControlledFocusedValueTemplate = (args: CalendarProps) => {
let defaultDate = today(getLocalTimeZone());
let [focusedDate, setFocusedDate] = React.useState<DateValue | null>(defaultDate);
return (
<div className="flex flex-col gap-4">
<Calendar
focusedValue={focusedDate}
value={defaultDate}
onFocusChange={setFocusedDate}
{...args}
/>
<Button
className="max-w-fit"
color="primary"
variant="flat"
onPress={() => setFocusedDate(defaultDate)}
>
Reset focused date
</Button>
</div>
);
};
const InvalidDateTemplate = (args: CalendarProps) => {
let [date, setDate] = React.useState<DateValue>(today(getLocalTimeZone()));
let {locale} = useLocale();
let isInvalid = isWeekend(date, locale);
return (
<Calendar
{...args}
aria-label="Appointment date"
errorMessage={isInvalid ? "We are closed on weekends" : undefined}
isInvalid={isInvalid}
value={date}
onChange={setDate}
/>
);
};
const InternationalCalendarsTemplate = (args: CalendarProps) => {
return (
<div className="flex flex-col gap-4">
<I18nProvider locale="zh-CN-u-ca-chinese">
<Calendar aria-label="Appointment date" {...args} />
</I18nProvider>
</div>
);
};
const PresetsTemplate = (args: CalendarProps) => {
let defaultDate = today(getLocalTimeZone());
let [value, setValue] = React.useState<DateValue | null>(defaultDate);
let {locale} = useLocale();
let now = today(getLocalTimeZone());
let nextWeek = startOfWeek(now.add({weeks: 1}), locale);
let nextMonth = startOfMonth(now.add({months: 1}));
const CustomRadio = (props) => {
const {children, ...otherProps} = props;
return (
<Radio
{...otherProps}
classNames={{
base: cn(
"flex-none m-0 h-8 bg-content1 hover:bg-content2 items-center justify-between",
"cursor-pointer rounded-full border-2 border-default-200/60",
"data-[selected=true]:border-primary",
),
label: "text-tiny text-default-500",
labelWrapper: "px-1 m-0",
wrapper: "hidden",
}}
>
{children}
</Radio>
);
};
return (
<div className="flex flex-col gap-4">
<Calendar
bottomContent={
<RadioGroup
aria-label="Date precision"
classNames={{
base: "w-full pb-2",
wrapper: "-my-2.5 py-2.5 px-3 gap-1 flex-nowrap max-w-[280px] overflow-scroll",
}}
defaultValue="exact_dates"
orientation="horizontal"
>
<CustomRadio value="exact_dates">Exact dates</CustomRadio>
<CustomRadio value="1_day">1 day</CustomRadio>
<CustomRadio value="2_days">2 days</CustomRadio>
<CustomRadio value="3_days">3 days</CustomRadio>
<CustomRadio value="7_days">7 days</CustomRadio>
<CustomRadio value="14_days">14 days</CustomRadio>
</RadioGroup>
}
classNames={{
content: "w-full",
}}
focusedValue={value}
nextButtonProps={{
variant: "bordered",
}}
prevButtonProps={{
variant: "bordered",
}}
topContent={
<ButtonGroup
fullWidth
className="px-3 pb-2 pt-3 bg-content1 [&>button]:text-default-500 [&>button]:border-default-200/60"
radius="full"
size="sm"
variant="bordered"
>
<Button onPress={() => setValue(now)}>Today</Button>
<Button onPress={() => setValue(nextWeek)}>Next week</Button>
<Button onPress={() => setValue(nextMonth)}>Next month</Button>
</ButtonGroup>
}
value={value}
onChange={setValue}
onFocusChange={setValue}
{...args}
/>
</div>
);
};
const CalendarWidthTemplate = (args: CalendarProps) => {
return (
<div className="flex gap-4">
<div className="flex flex-col items-center gap-4">
<p className="text-small text-default-600">calendarWidth: 300</p>
<Calendar {...args} calendarWidth={300} />
</div>
<div className="flex flex-col items-center gap-4">
<p className="text-small text-default-600">calendarWidth: 300px</p>
<Calendar {...args} calendarWidth="300px" />
</div>
<div className="flex flex-col items-center gap-4">
<p className="text-small text-default-600">calendarWidth: 30em</p>
<Calendar {...args} calendarWidth="30em" />
</div>
</div>
);
};
const ReducedMotionTemplate = (args: CalendarProps) => {
return (
<div className="flex gap-4">
<div className="flex flex-col items-center gap-4">
<p className="text-small text-default-600">reducedMotion: never</p>
<HeroUIProvider reducedMotion="never">
<Calendar {...args} />
</HeroUIProvider>
</div>
<div className="flex flex-col items-center gap-4">
<p className="text-small text-default-600">reducedMotion: always</p>
<HeroUIProvider reducedMotion="always">
<Calendar {...args} />
</HeroUIProvider>
</div>
<div className="flex flex-col items-center gap-4">
<p className="text-small text-default-600">reducedMotion: user</p>
<HeroUIProvider reducedMotion="user">
<Calendar {...args} />
</HeroUIProvider>
</div>
</div>
);
};
export const Default = {
render: Template,
args: {
...defaultProps,
},
};
export const Disabled = {
render: Template,
args: {
...defaultProps,
isDisabled: true,
},
};
export const ReadOnly = {
render: Template,
args: {
...defaultProps,
value: today(getLocalTimeZone()),
isReadOnly: true,
},
};
export const Controlled = {
render: ControlledTemplate,
args: {
...defaultProps,
},
};
export const MinDateValue = {
render: Template,
args: {
...defaultProps,
defaultValue: today(getLocalTimeZone()),
minValue: today(getLocalTimeZone()),
},
};
export const MaxDateValue = {
render: Template,
args: {
...defaultProps,
defaultValue: today(getLocalTimeZone()),
maxValue: today(getLocalTimeZone()),
},
};
export const UnavailableDates = {
render: UnavailableDatesTemplate,
args: {
...defaultProps,
defaultValue: today(getLocalTimeZone()),
unavailableDates: [today(getLocalTimeZone())],
},
};
export const ControlledFocusedValue = {
render: ControlledFocusedValueTemplate,
args: {
...defaultProps,
},
};
export const InvalidDate = {
render: InvalidDateTemplate,
args: {
...defaultProps,
},
};
export const WithMonthAndYearPickers = {
render: Template,
args: {
...defaultProps,
showMonthAndYearPickers: true,
},
};
export const InternationalCalendars = {
render: InternationalCalendarsTemplate,
args: {
...defaultProps,
showMonthAndYearPickers: true,
},
};
export const VisibleMonths = {
render: Template,
args: {
...defaultProps,
visibleMonths: 3,
},
};
export const PageBehavior = {
render: Template,
args: {
...defaultProps,
visibleMonths: 3,
pageBehavior: "single",
},
};
export const Presets = {
render: PresetsTemplate,
args: {
...defaultProps,
},
};
export const CalendarWidth = {
render: CalendarWidthTemplate,
args: {
...defaultProps,
},
};
export const ReducedMotion = {
render: ReducedMotionTemplate,
args: {
...defaultProps,
},
};
export const FirstDayOfWeek = {
render: Template,
args: {
...defaultProps,
firstDayOfWeek: "mon",
},
};