mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-22 19:53:48 +08:00
feat(component): optimize week-date-picker keyboard navigation (#5684)
This commit is contained in:
@@ -8,6 +8,8 @@ import {
|
|||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useImperativeHandle,
|
useImperativeHandle,
|
||||||
|
useLayoutEffect,
|
||||||
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
@@ -42,49 +44,67 @@ export const WeekDatePicker = memo(function WeekDatePicker({
|
|||||||
const weekRef = useRef<HTMLDivElement | null>(null);
|
const weekRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
const [cursor, setCursor] = useState(dayjs(value));
|
const [cursor, setCursor] = useState(dayjs(value));
|
||||||
const [range, setRange] = useState([0, 7]);
|
|
||||||
const [dense, setDense] = useState(false);
|
const [dense, setDense] = useState(false);
|
||||||
const [allDays, setAllDays] = useState<dayjs.Dayjs[]>([]);
|
|
||||||
const [viewPortSize, setViewPortSize] = useState(7);
|
const [viewPortSize, setViewPortSize] = useState(7);
|
||||||
|
|
||||||
useImperativeHandle(handleRef, () => ({
|
useImperativeHandle(handleRef, () => ({
|
||||||
setCursor,
|
setCursor,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const displayDays = allDays.slice(...range);
|
const range = useMemo(() => {
|
||||||
|
if (viewPortSize === 7) return [0, 7];
|
||||||
const updateRange = useCallback(
|
const cursorIndex = cursor.day();
|
||||||
(newRange: [number, number]) => {
|
let start = Math.max(0, cursorIndex - Math.floor(viewPortSize / 2));
|
||||||
if (range && newRange[0] === range[0] && newRange[1] === range[1]) return;
|
const end = Math.min(7, start + viewPortSize);
|
||||||
setRange(newRange);
|
if (end === 7) start = 7 - viewPortSize;
|
||||||
},
|
return [start, end];
|
||||||
[range]
|
}, [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 onNext = useCallback(() => {
|
||||||
if (viewPortSize === 7) {
|
const viewPortSize = displayDays.length;
|
||||||
setCursor(cursor.add(1, 'week'));
|
if (viewPortSize === 7) setCursor(c => c.add(1, 'week'));
|
||||||
} else if (range[1] === 7) {
|
else {
|
||||||
setCursor(cursor.add(1, 'week'));
|
setCursor(c => {
|
||||||
updateRange([0, viewPortSize]);
|
// last day of week is visible, move to next weeks
|
||||||
} else {
|
if (
|
||||||
const end = Math.min(range[1] + viewPortSize, 7);
|
displayDays[displayDays.length - 1].isSame(c.endOf('week'), 'day')
|
||||||
const start = Math.min(range[0] + viewPortSize, end - viewPortSize);
|
) {
|
||||||
updateRange([start, end]);
|
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;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [cursor, range, updateRange, viewPortSize]);
|
}, [displayDays]);
|
||||||
const onPrev = useCallback(() => {
|
const onPrev = useCallback(() => {
|
||||||
if (viewPortSize === 7) {
|
const viewPortSize = displayDays.length;
|
||||||
setCursor(cursor.add(-1, 'week'));
|
if (viewPortSize === 7) setCursor(c => c.add(-1, 'week'));
|
||||||
} else if (range[0] === 0) {
|
else
|
||||||
setCursor(cursor.add(-1, 'week'));
|
setCursor(c => {
|
||||||
updateRange([7 - viewPortSize, 7]);
|
// first day of week is visible, move to prev weeks
|
||||||
} else {
|
if (displayDays[0].isSame(c.startOf('week'), 'day')) {
|
||||||
const start = Math.max(range[0] - viewPortSize, 0);
|
return c
|
||||||
const end = Math.max(range[1] - viewPortSize, start + viewPortSize);
|
.subtract(1, 'week')
|
||||||
updateRange([start, end]);
|
.endOf('week')
|
||||||
}
|
.subtract(Math.floor(viewPortSize / 2) - 1, 'day');
|
||||||
}, [cursor, range, updateRange, viewPortSize]);
|
}
|
||||||
|
let prevDay = c.add(-viewPortSize, 'day');
|
||||||
|
if (!prevDay.isSame(c, 'week')) prevDay = c.startOf('week');
|
||||||
|
return prevDay;
|
||||||
|
});
|
||||||
|
}, [displayDays]);
|
||||||
const onDayClick = useCallback(
|
const onDayClick = useCallback(
|
||||||
(day: dayjs.Dayjs) => {
|
(day: dayjs.Dayjs) => {
|
||||||
onChange?.(day.format(format));
|
onChange?.(day.format(format));
|
||||||
@@ -115,77 +135,36 @@ export const WeekDatePicker = memo(function WeekDatePicker({
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// update allDays when cursor changes
|
|
||||||
useEffect(() => {
|
|
||||||
const firstDay = dayjs(cursor).startOf('week');
|
|
||||||
if (allDays[0] && firstDay.isSame(allDays[0], 'day')) return;
|
|
||||||
|
|
||||||
setAllDays(
|
|
||||||
Array.from({ length: 7 }).map((_, index) =>
|
|
||||||
firstDay.add(index, 'day').startOf('day')
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}, [allDays, cursor]);
|
|
||||||
|
|
||||||
// when viewPortSize changes, reset range
|
|
||||||
useEffect(() => {
|
|
||||||
if (viewPortSize >= 7) updateRange([0, 7]);
|
|
||||||
else {
|
|
||||||
const end = Math.min(7, range[0] + viewPortSize);
|
|
||||||
const start = Math.max(0, end - viewPortSize);
|
|
||||||
updateRange([start, end]);
|
|
||||||
}
|
|
||||||
}, [range, updateRange, viewPortSize]);
|
|
||||||
|
|
||||||
// when value changes, reset cursor
|
// when value changes, reset cursor
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
value && setCursor(dayjs(value));
|
value && setCursor(dayjs(value));
|
||||||
}, [value]);
|
}, [value]);
|
||||||
|
|
||||||
// TODO: keyboard navigation
|
const focusCursorCell = useCallback(() => {
|
||||||
useEffect(() => {
|
if (!weekRef.current) return;
|
||||||
|
const cursorCell = weekRef.current.querySelector(
|
||||||
|
'button[tabIndex="0"]'
|
||||||
|
) as HTMLButtonElement;
|
||||||
|
cursorCell?.focus();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
if (!weekRef.current) return;
|
if (!weekRef.current) return;
|
||||||
const el = weekRef.current;
|
const el = weekRef.current;
|
||||||
const onKeyDown = (e: KeyboardEvent) => {
|
const onKeyDown = (e: KeyboardEvent) => {
|
||||||
if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') return;
|
if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
setCursor(cursor => cursor.add(e.key === 'ArrowLeft' ? -1 : 1, 'day'));
|
||||||
const focused = document.activeElement as HTMLElement;
|
setTimeout(focusCursorCell);
|
||||||
if (!focused) return el.querySelector('button')?.focus();
|
|
||||||
const day = dayjs(focused.dataset.value);
|
|
||||||
if (
|
|
||||||
(day.day() === 0 && e.key === 'ArrowLeft') ||
|
|
||||||
(e.key === 'ArrowLeft' && !focused.previousElementSibling)
|
|
||||||
) {
|
|
||||||
onPrev();
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
el.querySelector('button')?.focus();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
(day.day() === 6 && e.key === 'ArrowRight') ||
|
|
||||||
(e.key === 'ArrowRight' && !focused.nextElementSibling)
|
|
||||||
) {
|
|
||||||
onNext();
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
(el.querySelector('button:last-child') as HTMLElement)?.focus();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.key === 'ArrowLeft' && focused.previousElementSibling) {
|
|
||||||
(focused.previousElementSibling as HTMLElement).focus();
|
|
||||||
}
|
|
||||||
if (e.key === 'ArrowRight' && focused.nextElementSibling) {
|
|
||||||
(focused.nextElementSibling as HTMLElement).focus();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
el.addEventListener('keydown', onKeyDown);
|
el.addEventListener('keydown', onKeyDown);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
el.removeEventListener('keydown', onKeyDown);
|
el.removeEventListener('keydown', onKeyDown);
|
||||||
};
|
};
|
||||||
}, [onNext, onPrev]);
|
}, [focusCursorCell, onNext, onPrev]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx(styles.weekDatePicker, className)} {...attrs}>
|
<div className={clsx(styles.weekDatePicker, className)} {...attrs}>
|
||||||
@@ -230,7 +209,7 @@ const Cell = ({ day, dense, value, cursor, onClick }: CellProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
tabIndex={0}
|
tabIndex={cursor.isSame(day, 'day') ? 0 : -1}
|
||||||
aria-label={day.format(format)}
|
aria-label={day.format(format)}
|
||||||
data-active={isActive}
|
data-active={isActive}
|
||||||
data-curr-month={isCurrentMonth}
|
data-curr-month={isCurrentMonth}
|
||||||
|
|||||||
Reference in New Issue
Block a user