Files
helix-engage/src/components/application/date-picker/calendar.tsx
2026-03-23 16:41:58 +05:30

104 lines
4.7 KiB
TypeScript

import type { FC, HTMLAttributes, PropsWithChildren } from "react";
import { Fragment, useContext, useState } from "react";
import { faChevronLeft, faChevronRight } from "@fortawesome/pro-duotone-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { type CalendarDate, getLocalTimeZone, today } from "@internationalized/date";
import type { CalendarProps as AriaCalendarProps, DateValue } from "react-aria-components";
import {
Calendar as AriaCalendar,
CalendarContext as AriaCalendarContext,
CalendarGrid as AriaCalendarGrid,
CalendarGridBody as AriaCalendarGridBody,
CalendarGridHeader as AriaCalendarGridHeader,
CalendarHeaderCell as AriaCalendarHeaderCell,
CalendarStateContext as AriaCalendarStateContext,
Heading as AriaHeading,
useSlottedContext,
} from "react-aria-components";
import { Button } from "@/components/base/buttons/button";
import { cx } from "@/utils/cx";
import { CalendarCell } from "./cell";
import { DateInput } from "./date-input";
const ChevronLeft: FC<{ className?: string }> = ({ className }) => <FontAwesomeIcon icon={faChevronLeft} className={className} />;
const ChevronRight: FC<{ className?: string }> = ({ className }) => <FontAwesomeIcon icon={faChevronRight} className={className} />;
export const CalendarContextProvider = ({ children }: PropsWithChildren) => {
const [value, onChange] = useState<DateValue | null>(null);
const [focusedValue, onFocusChange] = useState<DateValue | undefined>();
return <AriaCalendarContext.Provider value={{ value, onChange, focusedValue, onFocusChange }}>{children}</AriaCalendarContext.Provider>;
};
const PresetButton = ({ value, children, ...props }: HTMLAttributes<HTMLButtonElement> & { value: CalendarDate }) => {
const context = useContext(AriaCalendarStateContext);
if (!context) {
throw new Error("Preset must be used within a Calendar component");
}
const handleClick = () => {
context.setValue(value);
context.setFocusedDate(value);
};
return (
<Button
{...props}
// It's important to give `null` explicitly to the `slot` prop
// otherwise the button will throw an error due to not using one of
// the required slots inside the Calendar component.
// Passing `null` will tell the button to not use a slot context.
slot={null}
size="md"
color="secondary"
onClick={handleClick}
>
{children}
</Button>
);
};
interface CalendarProps extends AriaCalendarProps<DateValue> {
/** The dates to highlight. */
highlightedDates?: DateValue[];
}
export const Calendar = ({ highlightedDates, className, ...props }: CalendarProps) => {
const context = useSlottedContext(AriaCalendarContext)!;
const ContextWrapper = context ? Fragment : CalendarContextProvider;
return (
<ContextWrapper>
<AriaCalendar {...props} className={(state) => cx("flex flex-col gap-3", typeof className === "function" ? className(state) : className)}>
<header className="flex items-center justify-between">
<Button slot="previous" iconLeading={ChevronLeft} size="sm" color="tertiary" className="size-8" />
<AriaHeading className="text-sm font-semibold text-fg-secondary" />
<Button slot="next" iconLeading={ChevronRight} size="sm" color="tertiary" className="size-8" />
</header>
<div className="flex gap-3">
<DateInput className="flex-1" />
<PresetButton value={today(getLocalTimeZone())}>Today</PresetButton>
</div>
<AriaCalendarGrid weekdayStyle="short" className="w-max">
<AriaCalendarGridHeader className="border-b-4 border-transparent">
{(day) => (
<AriaCalendarHeaderCell className="p-0">
<div className="flex size-10 items-center justify-center text-sm font-medium text-secondary">{day.slice(0, 2)}</div>
</AriaCalendarHeaderCell>
)}
</AriaCalendarGridHeader>
<AriaCalendarGridBody className="[&_td]:p-0 [&_tr]:border-b-4 [&_tr]:border-transparent [&_tr:last-of-type]:border-none">
{(date) => (
<CalendarCell date={date} isHighlighted={highlightedDates?.some((highlightedDate) => date.compare(highlightedDate) === 0)} />
)}
</AriaCalendarGridBody>
</AriaCalendarGrid>
</AriaCalendar>
</ContextWrapper>
);
};