Calendar
Calendar component for Tile UI.
packages/react/src/components/calendar/calendar.tsx
import React, { useState } from 'react';
import {
addMonths,
calendarStyleKeys,
getCalendarDayModifiers,
getCalendarFirstSelectedDate,
getMonthCaption,
getMonthGrid,
getWeekdayLabels,
selectCalendarDay,
startOfMonth,
} from '../lib/core';
import type { CalendarBaseProps, CalendarDayModifiers, CalendarSelection } from '../lib/core';
import styles from './calendar.module.scss';
export interface CalendarProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onSelect'>, CalendarBaseProps {}
export interface CalendarDayButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
/** 日期 */
date: Date;
/** 修饰状态 */
modifiers: CalendarDayModifiers;
}
/**
* 单个日期按钮(独立导出,供自定义网格使用)
*/
const CalendarDayButton = React.forwardRef<HTMLButtonElement, CalendarDayButtonProps>(({ date, modifiers, className = '', children, ...props }, ref) => {
const classes = [styles[calendarStyleKeys.dayButton], className].filter(Boolean).join(' ');
return (
<button
ref={ref}
type="button"
data-day={date.toLocaleDateString()}
data-selected={modifiers.selected}
data-selected-single={modifiers.selected && !modifiers.rangeStart && !modifiers.rangeEnd && !modifiers.rangeMiddle}
data-range-start={modifiers.rangeStart}
data-range-end={modifiers.rangeEnd}
data-range-middle={modifiers.rangeMiddle}
data-outside={modifiers.outside}
data-disabled={modifiers.disabled}
data-today={modifiers.today}
disabled={modifiers.disabled}
aria-disabled={modifiers.disabled}
className={classes}
{...props}>
{children ?? date.getDate()}
</button>
);
});
CalendarDayButton.displayName = 'CalendarDayButton';
/**
* Calendar 日历组件(单选 / 多选 / 范围选择)
*/
const Calendar = React.forwardRef<HTMLDivElement, CalendarProps>(
({ className = '', mode = 'single', selected, defaultSelected, defaultMonth, showOutsideDays = true, disabled, onSelect, ...props }, ref) => {
const [internalSelected, setInternalSelected] = useState<CalendarSelection>(defaultSelected);
const currentSelected = selected !== undefined ? selected : internalSelected;
const [month, setMonth] = useState<Date>(() => {
if (defaultMonth) {
return startOfMonth(defaultMonth);
}
const firstSelected = getCalendarFirstSelectedDate(defaultSelected);
if (firstSelected) {
return startOfMonth(firstSelected);
}
return startOfMonth(new Date());
});
function handleSelect(day: Date) {
if (disabled?.(day)) {
return;
}
const next = selectCalendarDay(mode, currentSelected, day);
if (selected === undefined) {
setInternalSelected(next);
}
onSelect?.(next);
}
const grid = getMonthGrid(month, showOutsideDays);
const weekdays = getWeekdayLabels();
return (
<div ref={ref} data-slot="calendar" data-mode={mode} className={`${styles[calendarStyleKeys.root]} ${className}`} {...props}>
<div className={styles[calendarStyleKeys.header]}>
<nav className={styles[calendarStyleKeys.nav]}>
<button
type="button"
aria-label="上个月"
className={`${styles[calendarStyleKeys.navButton]} ${styles[calendarStyleKeys.navPrev]}`}
onClick={() => setMonth((prev) => addMonths(prev, -1))}>
<svg
className={styles[calendarStyleKeys.icon]}
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true">
<path d="m15 18-6-6 6-6" />
</svg>
</button>
<button
type="button"
aria-label="下个月"
className={`${styles[calendarStyleKeys.navButton]} ${styles[calendarStyleKeys.navNext]}`}
onClick={() => setMonth((prev) => addMonths(prev, 1))}>
<svg
className={styles[calendarStyleKeys.icon]}
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true">
<path d="m9 18 6-6-6-6" />
</svg>
</button>
</nav>
<div className={styles[calendarStyleKeys.monthCaption]}>{getMonthCaption(month)}</div>
</div>
<div className={styles[calendarStyleKeys.weekdays]}>
{weekdays.map((label) => (
<div key={label} className={styles[calendarStyleKeys.weekday]}>
{label}
</div>
))}
</div>
<div className={styles[calendarStyleKeys.weeks]} role="grid">
{grid.map((week, weekIndex) => (
<div key={weekIndex} role="row" className={styles[calendarStyleKeys.week]}>
{week.map((day, dayIndex) => {
if (!day) {
return <div key={dayIndex} className={styles[calendarStyleKeys.day]} />;
}
const modifiers = getCalendarDayModifiers(day, month, currentSelected, showOutsideDays, disabled);
return (
<div key={dayIndex} role="gridcell" aria-selected={modifiers.selected} className={styles[calendarStyleKeys.day]}>
<CalendarDayButton date={day} modifiers={modifiers} onClick={() => handleSelect(day)} />
</div>
);
})}
</div>
))}
</div>
</div>
);
},
);
Calendar.displayName = 'Calendar';
export { Calendar, CalendarDayButton };
export default Calendar;
- Where it goes
- calendar