Skip to content

Commit

Permalink
wip: calendar struct
Browse files Browse the repository at this point in the history
  • Loading branch information
pan-kot committed Jan 20, 2025
1 parent e1dc31c commit 6a4dfb1
Show file tree
Hide file tree
Showing 5 changed files with 241 additions and 179 deletions.
8 changes: 4 additions & 4 deletions src/calendar/grid/use-calendar-grid-rows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export default function useCalendarGridRows({
baseDate,
granularity,
locale,
startOfWeek,
startOfWeek: rawStartOfWeek,
}: {
baseDate: Date;
granularity: CalendarProps.Granularity;
Expand All @@ -24,10 +24,10 @@ export default function useCalendarGridRows({
if (isMonthPicker) {
return getCalendarYear(baseDate);
} else {
const firstDayOfWeek = normalizeStartOfWeek(startOfWeek, locale);
return getCalendarMonthWithSixRows(baseDate, { firstDayOfWeek, padWeeks: 'after' });
const startOfWeek = normalizeStartOfWeek(rawStartOfWeek, locale);
return getCalendarMonthWithSixRows(baseDate, { startOfWeek, padDates: 'after' });
}
}, [baseDate, isMonthPicker, startOfWeek, locale]);
}, [baseDate, isMonthPicker, rawStartOfWeek, locale]);

return rows;
}
4 changes: 2 additions & 2 deletions src/date-range-picker/calendar/grids/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ export const Grids = ({
<InternalSpaceBetween size="xs" direction="horizontal">
{!isSingleGrid && (
<MonthlyGrid
dateType="start"
padDates="before"
className={styles['first-grid']}
baseDate={addMonths(baseDate, -1)}
selectedEndDate={selectedEndDate}
Expand All @@ -196,7 +196,7 @@ export const Grids = ({
/>
)}
<MonthlyGrid
dateType="end"
padDates="after"
className={styles['second-grid']}
baseDate={baseDate}
selectedEndDate={selectedEndDate}
Expand Down
261 changes: 107 additions & 154 deletions src/date-range-picker/calendar/grids/monthly-grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,12 @@
// SPDX-License-Identifier: Apache-2.0
import React, { useMemo } from 'react';
import clsx from 'clsx';
import {
addDays,
addWeeks,
getDaysInMonth,
isAfter,
isBefore,
isLastDayOfMonth,
isSameDay,
isSameMonth,
isToday,
} from 'date-fns';
import { isAfter, isBefore, isLastDayOfMonth, isSameDay, isSameMonth, isToday } from 'date-fns';

import { getDateLabel, renderDayName } from '../../../calendar/utils/intl';
import ScreenreaderOnly from '../../../internal/components/screenreader-only/index.js';
import { formatDate } from '../../../internal/utils/date-time';
import { getCalendarMonthWithSixRows } from '../../../internal/utils/date-time/calendar';
import { MonthCalendar } from '../../../internal/utils/date-time/calendar';
import { DateRangePickerProps, DayIndex } from '../../interfaces';
import { GridCell } from './grid-cell';

Expand All @@ -40,7 +30,7 @@ import styles from './styles.css.js';
*/

interface GridProps {
dateType: 'start' | 'end';
padDates: 'before' | 'after';
baseDate: Date;
selectedStartDate: Date | null;
selectedEndDate: Date | null;
Expand All @@ -67,7 +57,7 @@ interface GridProps {
}

export function MonthlyGrid({
dateType,
padDates,
baseDate,
selectedStartDate,
selectedEndDate,
Expand All @@ -91,36 +81,23 @@ export function MonthlyGrid({

className,
}: GridProps) {
const baseDateTime = baseDate?.getTime();
// `baseDateTime` is used as a more stable replacement for baseDate
const weeks = useMemo<Date[][]>(
() =>
getCalendarMonthWithSixRows(baseDate, {
firstDayOfWeek: startOfWeek,
padWeeks: dateType === 'start' ? 'before' : 'after',
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[baseDateTime, startOfWeek, dateType]
);
// Week indices are only added for weeks with at least some days belonging to the base date month.
const weeksWithIndices = weeks.reduce(
(acc, week, index) => {
const isWeekBelongsToMonth = week.some(date => isSameMonth(date, baseDate));
if (isWeekBelongsToMonth) {
acc.push({ week, testIndex: (acc[index - 1]?.testIndex ?? 0) + 1 });
} else {
acc.push({ week });
}
return acc;
const baseDateTime = baseDate?.getTime();
const calendar = useMemo(
() => {
const startDate = rangeStartDate ?? rangeEndDate;
const endDate = rangeEndDate ?? rangeStartDate;
const selection = startDate && endDate ? ([startDate, endDate] as [Date, Date]) : null;
return new MonthCalendar({ padDates, startOfWeek, baseDate, selection });
},
[] as Array<{ week: Date[]; testIndex?: number }>
// eslint-disable-next-line react-hooks/exhaustive-deps
[padDates, startOfWeek, baseDateTime, rangeStartDate, rangeEndDate]
);
const weekdays = weeks[0].map(date => date.getDay());
return (
<table role="grid" aria-labelledby={ariaLabelledby} className={clsx(styles.grid, className)}>
<thead>
<tr>
{weekdays.map(dayIndex => (
{calendar.weekdays.map(dayIndex => (
<th key={dayIndex} scope="col" className={clsx(styles['grid-cell'], styles['day-header'])}>
<span aria-hidden="true">{renderDayName(locale, dayIndex, 'short')}</span>
<ScreenreaderOnly>{renderDayName(locale, dayIndex, 'long')}</ScreenreaderOnly>
Expand All @@ -129,140 +106,116 @@ export function MonthlyGrid({
</tr>
</thead>
<tbody onKeyDown={onGridKeyDownHandler}>
{weeksWithIndices.map(({ week, testIndex }, weekIndex) => {
const isWeekBelongsToMonth = week.some(date => isSameMonth(date, baseDate));
{calendar.weeks.map(({ days, testIndex }, weekIndex) => {
const isWeekBelongsToMonth = days.some(({ date }) => isSameMonth(date, baseDate));
return (
<tr
key={weekIndex}
className={clsx(styles.week, isWeekBelongsToMonth && testStyles['calendar-week'])}
data-awsui-weekindex={testIndex}
>
{week.map((date, dateIndex) => {
const isStartDate = !!selectedStartDate && isSameDay(date, selectedStartDate);
const isEndDate = !!selectedEndDate && isSameDay(date, selectedEndDate);
const isSelected = isStartDate || isEndDate;
const isRangeStartDate = !!rangeStartDate && isSameDay(date, rangeStartDate);
const isRangeEndDate = !!rangeEndDate && isSameDay(date, rangeEndDate);

const isFocused = !!focusedDate && isSameDay(date, focusedDate);
{days.map(
(
{ date, isVisible, isInRange, isSelectionTop, isSelectionBottom, isSelectionLeft, isSelectionRight },
dateIndex
) => {
const isStartDate = !!selectedStartDate && isSameDay(date, selectedStartDate);
const isEndDate = !!selectedEndDate && isSameDay(date, selectedEndDate);
const isSelected = isStartDate || isEndDate;
const isFocused = !!focusedDate && isSameDay(date, focusedDate);
const onlyOneSelected =
!!rangeStartDate && !!rangeEndDate
? isSameDay(rangeStartDate, rangeEndDate)
: !selectedStartDate || !selectedEndDate;

const isDateBelongsToMonth = isSameMonth(date, baseDate);
const isEnabled = (!isDateEnabled || isDateEnabled(date)) && isDateBelongsToMonth;
const disabledReason = dateDisabledReason(date);
const isDisabledWithReason = !isEnabled && !!disabledReason;
const isFocusable = isFocused && (isEnabled || isDisabledWithReason);

const baseClasses = {
[styles.day]: true,
[testStyles['calendar-date']]: isDateBelongsToMonth,
[styles['grid-cell']]: true,
[styles['in-first-row']]: weekIndex === 0,
[styles['in-first-column']]: dateIndex === 0,
};

if (!isVisible) {
return (
<td
key={`${weekIndex}:${dateIndex}`}
ref={isFocused ? focusedDateRef : undefined}
className={clsx(baseClasses, {
[styles['in-previous-month']]: isBefore(date, baseDate),
[styles['last-day-of-month']]: isLastDayOfMonth(date),
[styles['in-next-month']]: isAfter(date, baseDate),
})}
></td>
);
}

const handlers: React.HTMLAttributes<HTMLDivElement> = {};
if (isEnabled) {
handlers.onClick = () => onSelectDate(date);
handlers.onFocus = () => onFocusedDateChange(date);
}

// Can't be focused.
let tabIndex = undefined;
if (isFocusable && (isEnabled || isDisabledWithReason)) {
// Next focus target.
tabIndex = 0;
} else if (isEnabled || isDisabledWithReason) {
// Can be focused programmatically.
tabIndex = -1;
}

// Screen-reader announcement for the focused day.
let dayAnnouncement = getDateLabel(locale, date, 'short');
if (isToday(date)) {
dayAnnouncement += '. ' + todayAriaLabel;
}

const dateIsInRange = isStartDate || isEndDate || isInRange(date, rangeStartDate, rangeEndDate);
const inRangeStartWeek =
rangeStartDate && isInRange(date, rangeStartDate, addDays(addWeeks(rangeStartDate, 1), -1));
const inRangeEndWeek =
rangeEndDate && isInRange(date, rangeEndDate, addDays(addWeeks(rangeEndDate, -1), 1));
const onlyOneSelected =
!!rangeStartDate && !!rangeEndDate
? isSameDay(rangeStartDate, rangeEndDate)
: !selectedStartDate || !selectedEndDate;

const isDateBelongsToMonth = isSameMonth(date, baseDate);
const isEnabled = (!isDateEnabled || isDateEnabled(date)) && isDateBelongsToMonth;
const disabledReason = dateDisabledReason(date);
const isDisabledWithReason = !isEnabled && !!disabledReason;
const isFocusable = isFocused && (isEnabled || isDisabledWithReason);

const baseClasses = {
[styles.day]: true,
[testStyles['calendar-date']]: isDateBelongsToMonth,
[styles['grid-cell']]: true,
[styles['in-first-row']]: weekIndex === 0,
[styles['in-first-column']]: dateIndex === 0,
};

if (
!isDateBelongsToMonth &&
!(dateType === 'start' && weekIndex <= 1) &&
!(dateType === 'end' && weekIndex >= 4)
) {
return (
<td
key={`${weekIndex}:${dateIndex}`}
<GridCell
ref={isFocused ? focusedDateRef : undefined}
key={`${weekIndex}:${dateIndex}`}
className={clsx(baseClasses, {
[styles['in-previous-month']]: isBefore(date, baseDate),
[styles['last-day-of-month']]: isLastDayOfMonth(date),
[styles['in-next-month']]: isAfter(date, baseDate),
[styles['in-visible-calendar']]: true,
[styles.enabled]: isEnabled,
[styles.selected]: isSelected,
[styles['start-date']]: isStartDate,
[styles['end-date']]: isEndDate,
[styles['no-range']]: isSelected && onlyOneSelected,
[styles['in-range']]: isInRange,
[styles['in-range-border-block-start']]: isSelectionTop,
[styles['in-range-border-block-end']]: isSelectionBottom,
[styles['in-range-border-inline-start']]: isSelectionLeft,
[styles['in-range-border-inline-end']]: isSelectionRight,
[styles.today]: isToday(date),
})}
></td>
aria-selected={isEnabled ? isSelected || isInRange : undefined}
aria-current={isToday(date) ? 'date' : undefined}
data-date={formatDate(date)}
aria-disabled={!isEnabled}
tabIndex={tabIndex}
disabledReason={isDisabledWithReason ? disabledReason : undefined}
{...handlers}
>
<span className={styles['day-inner']} aria-hidden="true">
{date.getDate()}
</span>
<ScreenreaderOnly>{dayAnnouncement}</ScreenreaderOnly>
</GridCell>
);
}

const handlers: React.HTMLAttributes<HTMLDivElement> = {};
if (isEnabled) {
handlers.onClick = () => onSelectDate(date);
handlers.onFocus = () => onFocusedDateChange(date);
}

// Can't be focused.
let tabIndex = undefined;
if (isFocusable && (isEnabled || isDisabledWithReason)) {
// Next focus target.
tabIndex = 0;
} else if (isEnabled || isDisabledWithReason) {
// Can be focused programmatically.
tabIndex = -1;
}

// Screen-reader announcement for the focused day.
let dayAnnouncement = getDateLabel(locale, date, 'short');
if (isToday(date)) {
dayAnnouncement += '. ' + todayAriaLabel;
}

return (
<GridCell
ref={isFocused ? focusedDateRef : undefined}
key={`${weekIndex}:${dateIndex}`}
className={clsx(baseClasses, {
[styles['in-current-month']]: true,
[styles.enabled]: isEnabled,
[styles.selected]: isSelected && isDateBelongsToMonth,
[styles['start-date']]: isStartDate,
[styles['end-date']]: isEndDate,
[styles['range-start-date']]: isRangeStartDate,
[styles['range-end-date']]: isRangeEndDate,
[styles['no-range']]: isSelected && onlyOneSelected,
[styles['in-range']]: dateIsInRange && isDateBelongsToMonth,
[styles['in-range-border-block-start']]: !!inRangeStartWeek || date.getDate() <= 7,
[styles['in-range-border-block-end']]:
!!inRangeEndWeek || date.getDate() > getDaysInMonth(date) - 7,
[styles['in-range-border-inline-start']]:
dateIndex === 0 || date.getDate() === 1 || isRangeStartDate,
[styles['in-range-border-inline-end']]:
dateIndex === week.length - 1 || isLastDayOfMonth(date) || isRangeEndDate,
[styles.today]: isToday(date),
})}
aria-selected={isEnabled ? isSelected || dateIsInRange : undefined}
aria-current={isToday(date) ? 'date' : undefined}
data-date={formatDate(date)}
aria-disabled={!isEnabled}
tabIndex={tabIndex}
disabledReason={isDisabledWithReason ? disabledReason : undefined}
{...handlers}
>
<span className={styles['day-inner']} aria-hidden="true">
{date.getDate()}
</span>
<ScreenreaderOnly>{dayAnnouncement}</ScreenreaderOnly>
</GridCell>
);
})}
)}
</tr>
);
})}
</tbody>
</table>
);
}

function isInRange(date: Date, dateOne: Date | null, dateTwo: Date | null) {
if (!dateOne || !dateTwo || isSameDay(dateOne, dateTwo)) {
return false;
}

const inRange =
(isAfter(date, dateOne) && isBefore(date, dateTwo)) || (isAfter(date, dateTwo) && isBefore(date, dateOne));

return inRange || isSameDay(date, dateOne) || isSameDay(date, dateTwo);
}
Loading

0 comments on commit 6a4dfb1

Please sign in to comment.