mirror of
https://github.com/nextui-org/nextui.git
synced 2025-12-08 19:26:11 +00:00
* fix(date-range-picker): ensure label props are reflected in aria labels * docs: added missing label to date range picker * chore: add changesets * Update .changeset/gentle-pigs-admire.md Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
582 lines
14 KiB
TypeScript
582 lines
14 KiB
TypeScript
import React from "react";
|
|
import {Meta} from "@storybook/react";
|
|
import {dateInput} from "@nextui-org/theme";
|
|
import {
|
|
endOfMonth,
|
|
endOfWeek,
|
|
getLocalTimeZone,
|
|
isWeekend,
|
|
parseAbsoluteToLocal,
|
|
parseDate,
|
|
parseZonedDateTime,
|
|
startOfMonth,
|
|
startOfWeek,
|
|
today,
|
|
} from "@internationalized/date";
|
|
import {RangeValue} from "@react-types/shared";
|
|
import {DateValue} from "@react-types/datepicker";
|
|
import {I18nProvider, useDateFormatter, useLocale} from "@react-aria/i18n";
|
|
import {Button, ButtonGroup} from "@nextui-org/button";
|
|
import {Radio, RadioGroup} from "@nextui-org/radio";
|
|
import {cn} from "@nextui-org/system";
|
|
|
|
import {DateRangePicker, DateRangePickerProps} from "../src";
|
|
|
|
export default {
|
|
title: "Components/DateRangePicker",
|
|
component: DateRangePicker,
|
|
argTypes: {
|
|
variant: {
|
|
control: {
|
|
type: "select",
|
|
},
|
|
options: ["flat", "faded", "bordered", "underlined"],
|
|
},
|
|
color: {
|
|
control: {
|
|
type: "select",
|
|
},
|
|
options: ["default", "primary", "secondary", "success", "warning", "danger"],
|
|
},
|
|
radius: {
|
|
control: {
|
|
type: "select",
|
|
},
|
|
options: ["none", "sm", "md", "lg", "full"],
|
|
},
|
|
size: {
|
|
control: {
|
|
type: "select",
|
|
},
|
|
options: ["sm", "md", "lg"],
|
|
},
|
|
labelPlacement: {
|
|
control: {
|
|
type: "select",
|
|
},
|
|
options: ["inside", "outside", "outside-left"],
|
|
},
|
|
isDisabled: {
|
|
control: {
|
|
type: "boolean",
|
|
},
|
|
},
|
|
},
|
|
decorators: [
|
|
(Story) => (
|
|
<div className="flex items-center justify-start">
|
|
<Story />
|
|
</div>
|
|
),
|
|
],
|
|
} as Meta<typeof DateRangePicker>;
|
|
|
|
const defaultProps = {
|
|
label: "Stay duration",
|
|
...dateInput.defaultVariants,
|
|
};
|
|
|
|
const Template = (args: DateRangePickerProps) => <DateRangePicker {...args} />;
|
|
|
|
const LabelPlacementTemplate = (args: DateRangePickerProps) => (
|
|
<div className="w-full max-w-xl flex flex-col items-start gap-4">
|
|
<DateRangePicker {...args} description="inside" />
|
|
<DateRangePicker {...args} description="outside" labelPlacement="outside" />
|
|
<DateRangePicker {...args} description="outside-left" labelPlacement="outside-left" />
|
|
</div>
|
|
);
|
|
|
|
const ControlledTemplate = (args: DateRangePickerProps) => {
|
|
const [value, setValue] = React.useState<RangeValue<DateValue>>({
|
|
start: parseDate("2024-04-01"),
|
|
end: parseDate("2024-04-08"),
|
|
});
|
|
|
|
let formatter = useDateFormatter({dateStyle: "long"});
|
|
|
|
return (
|
|
<div className="flex flex-row gap-2">
|
|
<div className="w-full flex flex-col gap-y-2">
|
|
<DateRangePicker
|
|
{...args}
|
|
label="Date range (controlled)"
|
|
value={value}
|
|
onChange={setValue}
|
|
/>
|
|
<p className="text-default-500 text-sm">
|
|
Selected date:{" "}
|
|
{value
|
|
? formatter.formatRange(
|
|
value.start.toDate(getLocalTimeZone()),
|
|
value.end.toDate(getLocalTimeZone()),
|
|
)
|
|
: "--"}
|
|
</p>
|
|
</div>
|
|
<DateRangePicker
|
|
{...args}
|
|
defaultValue={{
|
|
start: parseDate("2024-04-01"),
|
|
end: parseDate("2024-04-08"),
|
|
}}
|
|
label="Date range (uncontrolled)"
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const TimeZonesTemplate = (args: DateRangePickerProps) => (
|
|
<div className="w-full max-w-xl flex flex-col items-start gap-4">
|
|
<DateRangePicker
|
|
{...args}
|
|
className="max-w-xs"
|
|
defaultValue={{
|
|
start: parseZonedDateTime("2024-04-01T00:45[America/Los_Angeles]"),
|
|
end: parseZonedDateTime("2024-04-14T11:15[America/Los_Angeles]"),
|
|
}}
|
|
labelPlacement="outside"
|
|
/>
|
|
<DateRangePicker
|
|
// {...args}
|
|
aria-label="Event date"
|
|
className="max-w-xs"
|
|
defaultValue={{
|
|
start: parseAbsoluteToLocal("2024-04-01T07:45:00Z"),
|
|
end: parseAbsoluteToLocal("2024-04-14T19:15:00Z"),
|
|
}}
|
|
labelPlacement="outside"
|
|
/>
|
|
</div>
|
|
);
|
|
|
|
const GranularityTemplate = (args: DateRangePickerProps) => {
|
|
let [date, setDate] = React.useState<RangeValue<DateValue>>({
|
|
start: parseAbsoluteToLocal("2024-04-01T18:45:22Z"),
|
|
end: parseAbsoluteToLocal("2024-04-08T19:15:22Z"),
|
|
});
|
|
|
|
return (
|
|
<div className="w-full max-w-xl flex flex-col items-start gap-4">
|
|
<DateRangePicker
|
|
{...args}
|
|
fullWidth
|
|
granularity="second"
|
|
label="Date and time range"
|
|
value={date}
|
|
onChange={setDate}
|
|
/>
|
|
<DateRangePicker
|
|
{...args}
|
|
fullWidth
|
|
granularity="day"
|
|
label="Date range"
|
|
value={date}
|
|
onChange={setDate}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const InternationalCalendarsTemplate = (args: DateRangePickerProps) => {
|
|
let [date, setDate] = React.useState<RangeValue<DateValue>>({
|
|
start: parseAbsoluteToLocal("2021-04-01T18:45:22Z"),
|
|
end: parseAbsoluteToLocal("2021-04-14T19:15:22Z"),
|
|
});
|
|
|
|
return (
|
|
<div className="flex flex-col gap-4">
|
|
<I18nProvider locale="hi-IN-u-ca-indian">
|
|
<DateRangePicker {...args} label="Appointment date" value={date} onChange={setDate} />
|
|
</I18nProvider>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const UnavailableDatesTemplate = (args: DateRangePickerProps) => {
|
|
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 (
|
|
<DateRangePicker
|
|
aria-label="Appointment date"
|
|
isDateUnavailable={(date) =>
|
|
disabledRanges.some(
|
|
(interval) => date.compare(interval[0]) >= 0 && date.compare(interval[1]) <= 0,
|
|
)
|
|
}
|
|
minValue={today(getLocalTimeZone())}
|
|
validate={(value) =>
|
|
disabledRanges.some(
|
|
(interval) =>
|
|
value && value.end.compare(interval[0]) >= 0 && value.start.compare(interval[1]) <= 0,
|
|
)
|
|
? "Selected date range may not include unavailable dates."
|
|
: null
|
|
}
|
|
{...args}
|
|
/>
|
|
);
|
|
};
|
|
|
|
const NonContiguousRangesTemplate = (args: DateRangePickerProps) => {
|
|
let {locale} = useLocale();
|
|
|
|
return (
|
|
<DateRangePicker
|
|
{...args}
|
|
allowsNonContiguousRanges
|
|
isDateUnavailable={(date) => isWeekend(date, locale)}
|
|
label="Time off request"
|
|
minValue={today(getLocalTimeZone())}
|
|
visibleMonths={2}
|
|
/>
|
|
);
|
|
};
|
|
|
|
const PresetsTemplate = (args: DateRangePickerProps) => {
|
|
let defaultDate = {
|
|
start: today(getLocalTimeZone()),
|
|
end: today(getLocalTimeZone()).add({days: 7}),
|
|
};
|
|
|
|
const [value, setValue] = React.useState<RangeValue<DateValue>>(defaultDate);
|
|
|
|
let {locale} = useLocale();
|
|
let formatter = useDateFormatter({dateStyle: "full"});
|
|
|
|
let now = today(getLocalTimeZone());
|
|
let nextWeek = {
|
|
start: startOfWeek(now.add({weeks: 1}), locale),
|
|
end: endOfWeek(now.add({weeks: 1}), locale),
|
|
};
|
|
let nextMonth = {
|
|
start: startOfMonth(now.add({months: 1})),
|
|
end: endOfMonth(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 w-full max-w-sm">
|
|
<DateRangePicker
|
|
CalendarBottomContent={
|
|
<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-[w-[calc(var(--visible-months)_*_var(--calendar-width))]] 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>
|
|
}
|
|
CalendarTopContent={
|
|
<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({
|
|
start: now,
|
|
end: now.add({days: 7}),
|
|
})
|
|
}
|
|
>
|
|
This week
|
|
</Button>
|
|
<Button onPress={() => setValue(nextWeek)}>Next week</Button>
|
|
<Button onPress={() => setValue(nextMonth)}>Next month</Button>
|
|
</ButtonGroup>
|
|
}
|
|
calendarProps={{
|
|
focusedValue: value.start,
|
|
onFocusChange: (val) => setValue({...value, start: val}),
|
|
nextButtonProps: {
|
|
variant: "bordered",
|
|
},
|
|
prevButtonProps: {
|
|
variant: "bordered",
|
|
},
|
|
}}
|
|
value={value}
|
|
onChange={setValue}
|
|
{...args}
|
|
label="Event date"
|
|
/>
|
|
<p className="text-default-500 text-sm">
|
|
Selected date:{" "}
|
|
{value
|
|
? formatter.formatRange(
|
|
value.start.toDate(getLocalTimeZone()),
|
|
value.end.toDate(getLocalTimeZone()),
|
|
)
|
|
: "--"}
|
|
</p>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export const Default = {
|
|
render: Template,
|
|
args: {
|
|
...defaultProps,
|
|
},
|
|
};
|
|
|
|
export const VisibleMonths = {
|
|
render: Template,
|
|
args: {
|
|
...defaultProps,
|
|
visibleMonths: 2,
|
|
},
|
|
};
|
|
|
|
export const LabelPlacement = {
|
|
render: LabelPlacementTemplate,
|
|
|
|
args: {
|
|
...defaultProps,
|
|
},
|
|
};
|
|
|
|
export const WithTimeField = {
|
|
render: Template,
|
|
args: {
|
|
...defaultProps,
|
|
label: "Event duration",
|
|
hideTimeZone: true,
|
|
visibleMonths: 2,
|
|
defaultValue: {
|
|
start: parseZonedDateTime("2024-04-01T00:45[America/Los_Angeles]"),
|
|
end: parseZonedDateTime("2024-04-08T11:15[America/Los_Angeles]"),
|
|
},
|
|
},
|
|
};
|
|
|
|
export const Controlled = {
|
|
render: ControlledTemplate,
|
|
args: {
|
|
...defaultProps,
|
|
},
|
|
};
|
|
|
|
export const Required = {
|
|
render: Template,
|
|
args: {
|
|
...defaultProps,
|
|
isRequired: true,
|
|
},
|
|
};
|
|
|
|
export const Disabled = {
|
|
render: Template,
|
|
args: {
|
|
...defaultProps,
|
|
isDisabled: true,
|
|
defaultValue: {
|
|
start: parseDate("2024-04-01"),
|
|
end: parseDate("2024-04-08"),
|
|
},
|
|
},
|
|
};
|
|
|
|
export const ReadOnly = {
|
|
render: Template,
|
|
args: {
|
|
...defaultProps,
|
|
isReadOnly: true,
|
|
defaultValue: {
|
|
start: parseDate("2024-04-01"),
|
|
end: parseDate("2024-04-08"),
|
|
},
|
|
},
|
|
};
|
|
|
|
export const WithoutLabel = {
|
|
render: Template,
|
|
|
|
args: {
|
|
...defaultProps,
|
|
label: null,
|
|
"aria-label": "Stay duration",
|
|
},
|
|
};
|
|
|
|
export const WithDescription = {
|
|
render: Template,
|
|
|
|
args: {
|
|
...defaultProps,
|
|
description: "Please enter your stay duration",
|
|
},
|
|
};
|
|
|
|
export const SelectorIcon = {
|
|
render: Template,
|
|
|
|
args: {
|
|
...defaultProps,
|
|
selectorIcon: (
|
|
<svg height="1em" viewBox="0 0 24 24" width="1em">
|
|
<g
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth="2"
|
|
>
|
|
<path d="M8 2v4m8-4v4" />
|
|
<rect height="18" rx="2" width="18" x="3" y="4" />
|
|
<path d="M3 10h18M8 14h.01M12 14h.01M16 14h.01M8 18h.01M12 18h.01M16 18h.01" />
|
|
</g>
|
|
</svg>
|
|
),
|
|
},
|
|
};
|
|
|
|
export const WithErrorMessage = {
|
|
render: Template,
|
|
|
|
args: {
|
|
...defaultProps,
|
|
errorMessage: "Please enter your stay duration",
|
|
},
|
|
};
|
|
|
|
export const IsInvalid = {
|
|
render: Template,
|
|
|
|
args: {
|
|
...defaultProps,
|
|
variant: "bordered",
|
|
isInvalid: true,
|
|
defaultValue: {
|
|
start: parseDate("2024-04-01"),
|
|
end: parseDate("2024-04-08"),
|
|
},
|
|
errorMessage: "Please enter a valid date",
|
|
},
|
|
};
|
|
|
|
export const TimeZones = {
|
|
render: TimeZonesTemplate,
|
|
|
|
args: {
|
|
...defaultProps,
|
|
label: "Event date",
|
|
defaultValue: parseZonedDateTime("2022-11-07T00:45[America/Los_Angeles]"),
|
|
},
|
|
};
|
|
|
|
export const Granularity = {
|
|
render: GranularityTemplate,
|
|
|
|
args: {
|
|
...defaultProps,
|
|
visibleMonths: 2,
|
|
},
|
|
};
|
|
|
|
export const InternationalCalendars = {
|
|
render: InternationalCalendarsTemplate,
|
|
|
|
args: {
|
|
...defaultProps,
|
|
hideTimeZone: true,
|
|
},
|
|
};
|
|
|
|
export const MinDateValue = {
|
|
render: Template,
|
|
|
|
args: {
|
|
...defaultProps,
|
|
minValue: today(getLocalTimeZone()),
|
|
defaultValue: {
|
|
start: today(getLocalTimeZone()).subtract({days: 1}),
|
|
end: parseDate("2024-04-08"),
|
|
},
|
|
},
|
|
};
|
|
|
|
export const MaxDateValue = {
|
|
render: Template,
|
|
|
|
args: {
|
|
...defaultProps,
|
|
maxValue: today(getLocalTimeZone()),
|
|
defaultValue: {
|
|
start: parseDate("2024-04-01"),
|
|
end: today(getLocalTimeZone()).add({days: 1}),
|
|
},
|
|
},
|
|
};
|
|
|
|
export const UnavailableDates = {
|
|
render: UnavailableDatesTemplate,
|
|
args: {
|
|
...defaultProps,
|
|
},
|
|
};
|
|
|
|
export const PageBehavior = {
|
|
render: Template,
|
|
args: {
|
|
...defaultProps,
|
|
visibleMonths: 2,
|
|
pageBehavior: "single",
|
|
},
|
|
};
|
|
|
|
export const NonContiguous = {
|
|
render: NonContiguousRangesTemplate,
|
|
args: {
|
|
...defaultProps,
|
|
},
|
|
};
|
|
|
|
export const Presets = {
|
|
render: PresetsTemplate,
|
|
args: {
|
|
...defaultProps,
|
|
visibleMonths: 2,
|
|
},
|
|
};
|