Skip to content

Commit

Permalink
add calendar range to handle multi view calendars
Browse files Browse the repository at this point in the history
  • Loading branch information
steppy452 committed Feb 15, 2024
1 parent 4d6d2bd commit 0ca0801
Show file tree
Hide file tree
Showing 7 changed files with 445 additions and 17 deletions.
15 changes: 14 additions & 1 deletion src/form/Calendar/Calendar.module.css
Original file line number Diff line number Diff line change
@@ -1,9 +1,22 @@
.container {
overflow: hidden;

.header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--spacing-sm);
}
overflow: hidden;

.multiviewLabel {
display: flex;
flex-grow: 1;
justify-content: space-around;
gap: var(--spacing-lg);
}

.calendars {
display: flex;
gap: var(--spacing-lg);
}
}
25 changes: 24 additions & 1 deletion src/form/Calendar/Calendar.story.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useState } from 'react';
import { useState } from 'react';
import { Card } from '../../layout/Card';
import { Calendar } from './Calendar';
import { CalendarRange } from './CalendarRange';
import { add, addMonths, sub } from 'date-fns';
import { Divider, Stack } from '../../layout';

Expand Down Expand Up @@ -112,3 +113,25 @@ export const Range = () => {
</Card>
);
};

export const Multiview = () => {
const [range, setRange] = useState<[Date, Date]>();

return (
<Card>
<CalendarRange
value={range}
onChange={val => setRange(val as [Date, Date])}
numMonths={3}
enableDayOfWeek
isRange
/>
<Divider />
<Stack inline={false} justifyContent="center">
{range
? `${range[0]?.toLocaleDateString()}-${range[1]?.toLocaleDateString()}`
: 'No date selected'}
</Stack>
</Card>
);
};
5 changes: 5 additions & 0 deletions src/form/Calendar/Calendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ export interface CalendarProps {
*/
dateFormat?: string;

/**
* Whether to display day of week labels
*/
enableDayOfWeek?: boolean;

/**
* Whether to animate the calendar.
*/
Expand Down
30 changes: 27 additions & 3 deletions src/form/Calendar/CalendarDays/CalendarDays.module.css
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
.week {
display: flex;
display: grid;
grid-template-columns: repeat(7, 1fr);

.day {
flex: 1;
transition: border 100ms ease-in-out;
padding: var(--calendar-spacing);
text-align: center;

&:not(.selected).outside {
&:not(.range).outside {
opacity: 0.6;
}

Expand All @@ -17,12 +19,34 @@

&.startRangeDate {
border-top-left-radius: var(--button-border-radius);
}

&.roundStartDateBottom {
border-bottom-left-radius: var(--button-border-radius);
}

&.endRangeDate {
border-top-right-radius: var(--button-border-radius);
border-bottom-right-radius: var(--button-border-radius);
}

&.roundEndDateTop {
border-top-right-radius: var(--button-border-radius);
}

&.hideDay {
visibility: hidden !important;
}
}
}

.weekLabels {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: var(--spacing-sm);
width: 100%;
margin: var(--spacing-md) 0 var(--spacing-sm);

.dayOfWeek {
text-align: center;
}
}
113 changes: 101 additions & 12 deletions src/form/Calendar/CalendarDays/CalendarDays.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { FC, useCallback, useMemo, useState } from 'react';
import { FC, useCallback, useMemo, useState } from 'react';
import classNames from 'classnames';
import {
addDays,
Expand All @@ -7,7 +7,8 @@ import {
isSameDay,
set,
max as maxDate,
min as minDate
min as minDate,
isSameMonth
} from 'date-fns';
import { Button } from '../../../elements/Button';
import { getWeeks } from '../utils';
Expand All @@ -26,6 +27,11 @@ export interface CalendarDaysProps {
*/
current?: Date | [Date, Date];

/**
* The currently hovered date.
*/
hover?: Date | null;

/**
* The minimum selectable date for the calendar, as a Date object.
*/
Expand All @@ -41,6 +47,21 @@ export interface CalendarDaysProps {
*/
disabled?: boolean;

/**
* Whether to display days of previous month
*/
hidePrevMonthDays?: boolean;

/**
* Whether to display days of next month
*/
hideNextMonthDays?: boolean;

/**
* Whether to display day of week labels
*/
enableDayOfWeek?: boolean;

/**
* Whether the calendar is a range picker.
*/
Expand All @@ -65,6 +86,11 @@ export interface CalendarDaysProps {
* A callback function that is called when a day is selected.
*/
onChange: (date: Date) => void;

/**
* A callback function that is called when a day is hovered.
*/
onHover?: (date: Date | null) => void;
}

const ZERO_TIME = {
Expand All @@ -74,50 +100,64 @@ const ZERO_TIME = {
milliseconds: 0
};

const DAY_OF_WEEK_LABELS = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];

export const CalendarDays: FC<CalendarDaysProps> = ({
value = new Date(),
current = new Date(),
hover = null,
isRange,
disabled,
min: minLimit,
max,
animated,
xAnimation = 0,
onChange
enableDayOfWeek,
hidePrevMonthDays,
hideNextMonthDays,
onChange,
onHover
}) => {
const [hoveringDate, setHoveringDate] = useState<Date | null>(null);
const [hoveringDate, setHoveringDate] = useState<Date | null>(hover);
const weeks = useMemo(() => getWeeks(value), [value]);
const maxLimit = useMemo(() => (max === 'now' ? new Date() : max), [max]);

const renderDay = useCallback(
(day, ii) => {
// Determine if the day should be shown or not
const hideDay =
(day.isPreviousMonth && hidePrevMonthDays) ||
(day.isNextMonth && hideNextMonthDays);

// Determine if the day is disabled
const isDisabled =
disabled ||
(minLimit && isBefore(day.date, minLimit)) ||
(maxLimit && isAfter(day.date, maxLimit));

// Determine that date is in selected range
const currentHover = hover || hoveringDate;
const isSelectionStarted =
Array.isArray(current) && isSameDay(...current);
const prevDayRangeStart = set(
addDays(
hoveringDate && isSelectionStarted
? minDate([current?.[0], hoveringDate])
currentHover && isSelectionStarted
? minDate([current?.[0], currentHover])
: current?.[0],
-1
),
ZERO_TIME
);
const nextDayRangeEnd = set(
addDays(
hoveringDate && isSelectionStarted
? maxDate([current?.[1], hoveringDate])
currentHover && isSelectionStarted
? maxDate([current?.[1], currentHover])
: current?.[1],
1
),
ZERO_TIME
);

const isSelected = Array.isArray(current)
? isAfter(day.date, prevDayRangeStart) &&
isBefore(day.date, nextDayRangeEnd)
Expand All @@ -131,12 +171,36 @@ export const CalendarDays: FC<CalendarDaysProps> = ({
Array.isArray(current) &&
isSameDay(addDays(nextDayRangeEnd, -1), day.date);

// Determine styling of range start and end dates
const hasNoRange = isStartRangeDate && isEndRangeDate;
const nextWeek = addDays(day.date, 7);
const nextWeekInRange =
isStartRangeDate && isBefore(nextWeek, nextDayRangeEnd);
const rangeConnectsBottom =
!nextWeekInRange &&
(isSameMonth(day.date, nextWeek) || !hideNextMonthDays);

const prevWeek = addDays(day.date, -7);
const prevWeekInRange =
isEndRangeDate && isAfter(prevWeek, prevDayRangeStart);
const rangeConnectsTop =
!prevWeekInRange &&
(isSameMonth(day.date, prevWeek) || !hidePrevMonthDays);

// Determine the color variant of the button
const colorVariant = isSelected ? 'primary' : 'default';

// Determine the button variant
const buttonVariant = isSelected ? 'filled' : 'text';

const handleHover = (value: Date | null) => {
if (onHover) {
onHover(value);
} else {
setHoveringDate(value);
}
};

return (
<Button
key={`day-${ii}`}
Expand All @@ -145,10 +209,16 @@ export const CalendarDays: FC<CalendarDaysProps> = ({
[css.today]: day.isToday,
[css.range]: isRange && isSelected,
[css.startRangeDate]: isRange && isStartRangeDate,
[css.endRangeDate]: isRange && isEndRangeDate
[css.roundStartDateBottom]:
(isRange && isStartRangeDate && rangeConnectsBottom) ||
hasNoRange,
[css.endRangeDate]: isRange && isEndRangeDate,
[css.roundEndDateTop]:
(isRange && isEndRangeDate && rangeConnectsTop) || hasNoRange,
[css.hideDay]: hideDay
})}
onMouseEnter={() => setHoveringDate(day.date)}
onMouseLeave={() => setHoveringDate(null)}
onMouseEnter={() => handleHover(day.date)}
onMouseLeave={() => handleHover(null)}
variant={buttonVariant}
color={colorVariant}
disableMargins
Expand All @@ -160,7 +230,17 @@ export const CalendarDays: FC<CalendarDaysProps> = ({
</Button>
);
},
[disabled, minLimit, maxLimit, hoveringDate, current, isRange, onChange]
[
disabled,
minLimit,
maxLimit,
hoveringDate,
current,
hover,
isRange,
onChange,
onHover
]
);

return (
Expand All @@ -174,6 +254,15 @@ export const CalendarDays: FC<CalendarDaysProps> = ({
opacity: { duration: 0.2, type: animated ? 'tween' : false }
}}
>
{enableDayOfWeek && (
<div className={css.weekLabels}>
{DAY_OF_WEEK_LABELS.map(day => (
<div key={`day-${day}`} className={css.dayOfWeek}>
{day}
</div>
))}
</div>
)}
{weeks.map((week, i) => (
<div key={`week-${i}`} className={css.week}>
{week.map(renderDay)}
Expand Down
Loading

0 comments on commit 0ca0801

Please sign in to comment.