mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 12:55:00 +00:00
223 lines
6.2 KiB
TypeScript
223 lines
6.2 KiB
TypeScript
import { ArrowLeftSmallIcon, ArrowRightSmallIcon } from '@blocksuite/icons/rc';
|
|
import clsx from 'clsx';
|
|
import dayjs from 'dayjs';
|
|
import type { ForwardedRef, HTMLAttributes } from 'react';
|
|
import {
|
|
memo,
|
|
useCallback,
|
|
useEffect,
|
|
useImperativeHandle,
|
|
useLayoutEffect,
|
|
useMemo,
|
|
useRef,
|
|
useState,
|
|
} from 'react';
|
|
|
|
import { observeResize } from '../../utils';
|
|
import { IconButton } from '../button';
|
|
import * as styles from './week-date-picker.css';
|
|
|
|
export interface WeekDatePickerHandle {
|
|
/** control cursor manually */
|
|
setCursor?: (cursor: dayjs.Dayjs) => void;
|
|
}
|
|
|
|
export interface WeekDatePickerProps
|
|
extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange'> {
|
|
value?: string;
|
|
onChange?: (value: string) => void;
|
|
handleRef?: ForwardedRef<WeekDatePickerHandle>;
|
|
}
|
|
|
|
// TODO(catsjuice): i18n
|
|
const weekDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
// const weekDays = ['日', '一', '二', '三', '四', '五', '六'];
|
|
const format = 'YYYY-MM-DD';
|
|
|
|
export const WeekDatePicker = memo(function WeekDatePicker({
|
|
value,
|
|
onChange,
|
|
className,
|
|
handleRef,
|
|
...attrs
|
|
}: WeekDatePickerProps) {
|
|
const weekRef = useRef<HTMLDivElement | null>(null);
|
|
|
|
const [cursor, setCursor] = useState(dayjs(value));
|
|
const [dense, setDense] = useState(false);
|
|
const [viewPortSize, setViewPortSize] = useState(7);
|
|
|
|
useImperativeHandle(handleRef, () => ({
|
|
setCursor,
|
|
}));
|
|
|
|
const range = useMemo(() => {
|
|
if (viewPortSize === 7) return [0, 7];
|
|
const cursorIndex = cursor.day();
|
|
let start = Math.max(0, cursorIndex - Math.floor(viewPortSize / 2));
|
|
const end = Math.min(7, start + viewPortSize);
|
|
if (end === 7) start = 7 - viewPortSize;
|
|
return [start, end];
|
|
}, [cursor, viewPortSize]);
|
|
const allDays = useMemo(
|
|
() =>
|
|
Array.from({ length: 7 }).map((_, index) =>
|
|
cursor.startOf('week').add(index, 'day').startOf('day')
|
|
),
|
|
[cursor]
|
|
);
|
|
const displayDays = useMemo(() => allDays.slice(...range), [allDays, range]);
|
|
|
|
const onNext = useCallback(() => {
|
|
const viewPortSize = displayDays.length;
|
|
if (viewPortSize === 7) setCursor(c => c.add(1, 'week'));
|
|
else {
|
|
setCursor(c => {
|
|
// last day of week is visible, move to next weeks
|
|
if (
|
|
displayDays[displayDays.length - 1].isSame(c.endOf('week'), 'day')
|
|
) {
|
|
return c
|
|
.add(1, 'week')
|
|
.startOf('week')
|
|
.add(Math.floor(viewPortSize / 2), 'day');
|
|
}
|
|
let nextDay = c.add(viewPortSize, 'day');
|
|
if (!nextDay.isSame(c, 'week')) nextDay = c.endOf('week');
|
|
return nextDay;
|
|
});
|
|
}
|
|
}, [displayDays]);
|
|
const onPrev = useCallback(() => {
|
|
const viewPortSize = displayDays.length;
|
|
if (viewPortSize === 7) setCursor(c => c.add(-1, 'week'));
|
|
else
|
|
setCursor(c => {
|
|
// first day of week is visible, move to prev weeks
|
|
if (displayDays[0].isSame(c.startOf('week'), 'day')) {
|
|
return c
|
|
.subtract(1, 'week')
|
|
.endOf('week')
|
|
.subtract(Math.floor(viewPortSize / 2) - 1, 'day');
|
|
}
|
|
let prevDay = c.add(-viewPortSize, 'day');
|
|
if (!prevDay.isSame(c, 'week')) prevDay = c.startOf('week');
|
|
return prevDay;
|
|
});
|
|
}, [displayDays]);
|
|
const onDayClick = useCallback(
|
|
(day: dayjs.Dayjs) => {
|
|
onChange?.(day.format(format));
|
|
},
|
|
[onChange]
|
|
);
|
|
|
|
// Observe weekRef to update viewPortSize
|
|
useEffect(() => {
|
|
const el = weekRef.current;
|
|
if (!el) return;
|
|
|
|
return observeResize(el, ({ contentRect: rect }) => {
|
|
const width = rect.width;
|
|
if (!width) return;
|
|
|
|
const minWidth = 30;
|
|
const gap = 4;
|
|
const viewPortCount = Math.floor(width / (minWidth + gap));
|
|
setViewPortSize(Math.max(1, Math.min(viewPortCount, 7)));
|
|
setDense(width < 300);
|
|
});
|
|
}, []);
|
|
|
|
// when value changes, reset cursor
|
|
useEffect(() => {
|
|
value && setCursor(dayjs(value));
|
|
}, [value]);
|
|
|
|
const focusCursorCell = useCallback(() => {
|
|
if (!weekRef.current) return;
|
|
const cursorCell = weekRef.current.querySelector(
|
|
'button[tabIndex="0"]'
|
|
) as HTMLButtonElement;
|
|
cursorCell?.focus();
|
|
}, []);
|
|
|
|
useLayoutEffect(() => {
|
|
if (!weekRef.current) return;
|
|
const el = weekRef.current;
|
|
const onKeyDown = (e: KeyboardEvent) => {
|
|
if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') return;
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
setCursor(cursor => cursor.add(e.key === 'ArrowLeft' ? -1 : 1, 'day'));
|
|
setTimeout(focusCursorCell);
|
|
};
|
|
|
|
el.addEventListener('keydown', onKeyDown);
|
|
|
|
return () => {
|
|
el.removeEventListener('keydown', onKeyDown);
|
|
};
|
|
}, [focusCursorCell, onNext, onPrev]);
|
|
|
|
return (
|
|
<div className={clsx(styles.weekDatePicker, className)} {...attrs}>
|
|
<IconButton onClick={onPrev}>
|
|
<ArrowLeftSmallIcon />
|
|
</IconButton>
|
|
|
|
<div ref={weekRef} className={styles.weekDatePickerContent}>
|
|
{displayDays.map(day => (
|
|
<Cell
|
|
key={day.toISOString()}
|
|
dense={dense}
|
|
value={value}
|
|
day={day}
|
|
cursor={cursor}
|
|
onClick={onDayClick}
|
|
/>
|
|
))}
|
|
</div>
|
|
|
|
<IconButton onClick={onNext}>
|
|
<ArrowRightSmallIcon />
|
|
</IconButton>
|
|
</div>
|
|
);
|
|
});
|
|
|
|
interface CellProps {
|
|
dense: boolean;
|
|
day: dayjs.Dayjs;
|
|
cursor: dayjs.Dayjs;
|
|
value?: string;
|
|
onClick: (day: dayjs.Dayjs) => void;
|
|
}
|
|
const Cell = ({ day, dense, value, cursor, onClick }: CellProps) => {
|
|
const isActive = day.format(format) === value;
|
|
const isCurrentMonth = day.month() === cursor.month();
|
|
const isToday = day.isSame(dayjs(), 'day');
|
|
|
|
const dayIndex = day.day();
|
|
const label = weekDays[dayIndex];
|
|
|
|
return (
|
|
<button
|
|
tabIndex={cursor.isSame(day, 'day') ? 0 : -1}
|
|
aria-label={day.format(format)}
|
|
data-active={isActive}
|
|
data-curr-month={isCurrentMonth}
|
|
data-today={isToday}
|
|
data-value={day.format(format)}
|
|
key={day.toISOString()}
|
|
className={styles.dayCell}
|
|
onClick={() => onClick(day)}
|
|
>
|
|
<div className={styles.dayCellWeek}>
|
|
{dense ? label.slice(0, 1) : label}
|
|
</div>
|
|
<div className={styles.dayCellDate}>{day.format('D')}</div>
|
|
</button>
|
|
);
|
|
};
|