Skip to content

Commit

Permalink
Merge pull request #57 from timelessco/range-calendar
Browse files Browse the repository at this point in the history
feat: added range calendar
  • Loading branch information
navin-moorthy authored Sep 30, 2020
2 parents 85caa5f + df132c2 commit 7c6442a
Show file tree
Hide file tree
Showing 22 changed files with 866 additions and 121 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"@react-aria/interactions": "3.2.0",
"@react-aria/link": "3.1.1",
"@react-aria/utils": "3.2.1",
"@react-aria/visually-hidden": "^3.2.0",
"@react-stately/toggle": "3.2.0",
"date-fns": "^2.16.1",
"react-use-gesture": "7.0.16",
Expand Down
47 changes: 0 additions & 47 deletions src/calendar-v1/CalendarCell.ts

This file was deleted.

28 changes: 0 additions & 28 deletions src/calendar-v1/__utils.ts

This file was deleted.

File renamed without changes.
File renamed without changes.
96 changes: 96 additions & 0 deletions src/calendar/CalendarCell.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/**
* All credit goes to [React Spectrum](https://github.com/adobe/react-spectrum)
* We improved the Calendar from Aria [useCalendarBase](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-aria/calendar/src/useCalendarBase.ts)
* to work with Reakit System
*/
import { useCallback } from "react";
import { BoxHTMLProps, BoxOptions, useBox } from "reakit";
import { createComponent, createHook } from "reakit-system";
import { getDaysInMonth, isSameDay, isWeekend } from "date-fns";
import { ariaAttr, callAllHandlers, dataAttr } from "@chakra-ui/utils";

import { CALENDAR_CELL_KEYS } from "./__keys";
import { CalendarStateReturn } from "./CalendarState";
import { RangeCalendarStateReturn } from "./RangeCalendarState";

export type CalendarCellOptions = BoxOptions &
Pick<CalendarStateReturn, "dateValue" | "isDisabled" | "currentMonth"> &
Partial<
Pick<RangeCalendarStateReturn, "highlightDate" | "highlightedRange">
> & {
date: Date;
};

export type CalendarCellHTMLProps = BoxHTMLProps;

export type CalendarCellProps = CalendarCellOptions & CalendarCellHTMLProps;

export const useCalendarCell = createHook<
CalendarCellOptions,
CalendarCellHTMLProps
>({
name: "CalendarCell",
compose: useBox,
keys: CALENDAR_CELL_KEYS,

useProps(options, { onMouseEnter: htmlOnMouseEnter, ...htmlProps }) {
const { isDisabled, highlightDate, date } = options;
const onMouseEnter = useCallback(() => {
if (isDisabled) return;

highlightDate?.(date);
}, [date, highlightDate, isDisabled]);

return {
role: "gridcell",
"data-weekend": dataAttr(isWeekend(date)),
onMouseEnter:
"highlightDate" in options
? callAllHandlers(htmlOnMouseEnter, onMouseEnter)
: htmlOnMouseEnter,
...getCalendarCellProps(options),
...htmlProps,
};
},
});

export const CalendarCell = createComponent({
as: "div",
memo: true,
useHook: useCalendarCell,
});

const getCalendarCellProps = (options: CalendarCellOptions) => {
const { date, dateValue, highlightedRange, currentMonth } = options;

if ("highlightDate" in options) {
const isSelected = highlightedRange
? date >= highlightedRange.start && date <= highlightedRange.end
: false;

const isRangeStart = isSelected && date.getDate() === 1;
const isRangeEnd =
isSelected && date.getDate() === getDaysInMonth(currentMonth);
const isSelectionStart = highlightedRange
? isSameDay(date, highlightedRange.start)
: false;
const isSelectionEnd = highlightedRange
? isSameDay(date, highlightedRange.end)
: false;

return {
"aria-selected": ariaAttr(isSelected),
"data-is-range-selection": dataAttr(isSelected),
"data-is-range-end": dataAttr(isRangeEnd),
"data-is-range-start": dataAttr(isRangeStart),
"data-is-selection-end": dataAttr(isSelectionEnd),
"data-is-selection-start": dataAttr(isSelectionStart),
};
}

const isSelected = dateValue ? isSameDay(date, dateValue) : false;

return {
"aria-selected": ariaAttr(isSelected),
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { ButtonHTMLProps, ButtonOptions, useButton } from "reakit";
import { isInvalid } from "./__utils";
import { CALENDAR_CELL_BUTTON_KEYS } from "./__keys";
import { CalendarStateReturn } from "./CalendarState";
import { RangeCalendarStateReturn } from "./RangeCalendarState";

export type CalendarCellButtonOptions = ButtonOptions &
Pick<
Expand All @@ -27,7 +28,8 @@ export type CalendarCellButtonOptions = ButtonOptions &
| "maxDate"
| "dateValue"
| "isFocused"
> & {
> &
Partial<Pick<RangeCalendarStateReturn, "anchorDate">> & {
date: Date;
};

Expand Down Expand Up @@ -69,7 +71,9 @@ export const useCalendarCellButton = createHook<
disabled,
dateValue,
selectDate,
anchorDate,
focusedDate,
isDisabled,
setFocusedDate,
isFocused: isFocusedOption,
} = options;
Expand Down Expand Up @@ -124,6 +128,25 @@ export const useCalendarCellButton = createHook<
ariaLabel = `${ariaLabel} selected`;
}

// When a cell is focused and this is a range calendar, add a prompt to help
// screenreader users know that they are in a range selection mode.
if (anchorDate && isFocused && !isDisabled) {
let rangeSelectionPrompt = "";

// If selection has started add "click to finish selecting range"
if (anchorDate) {
rangeSelectionPrompt = "click to finish selecting range";
// Otherwise, add "click to start selecting range" prompt
} else {
rangeSelectionPrompt = "click to start selecting range";
}

// Append to aria-label
if (rangeSelectionPrompt) {
ariaLabel = `${ariaLabel} (${rangeSelectionPrompt})`;
}
}

return {
children: useDateFormatter({ day: "numeric" }).format(date),
tabIndex,
Expand Down
32 changes: 29 additions & 3 deletions src/calendar-v1/CalendarGrid.ts → src/calendar/CalendarGrid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@
* We improved the Calendar from Stately [useWeekStart](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-stately/calendar/src/useWeekStart.ts)
* to work with Reakit System
*/
import { chain } from "@react-aria/utils";
import { createOnKeyDown, useForkRef } from "reakit-utils";
import { KeyboardEvent, useRef } from "react";
import { BoxHTMLProps, BoxOptions, useBox } from "reakit";
import { createOnKeyDown, useForkRef } from "reakit-utils";
import { createComponent, createHook } from "reakit-system";
import { ariaAttr, callAllHandlers } from "@chakra-ui/utils";

import { CALENDAR_GRID_KEYS } from "./__keys";
import { CalendarStateReturn } from "./CalendarState";
import { RangeCalendarStateReturn } from "./RangeCalendarState";

export type CalendarGridOptions = BoxOptions &
Pick<
Expand All @@ -30,7 +32,8 @@ export type CalendarGridOptions = BoxOptions &
| "focusPreviousDay"
| "focusNextWeek"
| "focusPreviousWeek"
>;
> &
Partial<Pick<RangeCalendarStateReturn, "setAnchorDate">>;

export type CalendarGridHTMLProps = BoxHTMLProps;

Expand Down Expand Up @@ -70,6 +73,7 @@ export const useCalendarGrid = createHook<
focusNextWeek,
focusPreviousWeek,
calendarId,
setAnchorDate,
} = options;
const ref = useRef<HTMLElement>(null);

Expand Down Expand Up @@ -98,15 +102,37 @@ export const useCalendarGrid = createHook<
},
});

let rangeCalendarProps = {};

if ("highlightDate" in options) {
const onRangeKeyDown = (e: KeyboardEvent) => {
switch (e.key) {
case "Escape":
// Cancel the selection.
setAnchorDate?.(null);
break;
}
};

rangeCalendarProps = {
"aria-multiselectable": true,
onKeyDown: callAllHandlers(
htmlOnKeyDown,
chain(onKeyDown, onRangeKeyDown),
),
};
}

return {
ref: useForkRef(ref, htmlRef),
role: "grid",
"aria-labelledby": calendarId,
"aria-readonly": ariaAttr(isReadOnly),
"aria-disabled": ariaAttr(isDisabled),
onKeyDown,
onKeyDown: callAllHandlers(htmlOnKeyDown, onKeyDown),
onFocus: callAllHandlers(htmlOnFocus, () => setFocused(true)),
onBlur: callAllHandlers(htmlOnBlur, () => setFocused(false)),
...rangeCalendarProps,
...htmlProps,
};
},
Expand Down
File renamed without changes.
Loading

0 comments on commit 7c6442a

Please sign in to comment.