Skip to content

Commit

Permalink
feat(datepicker): ✨ add controllable date picker
Browse files Browse the repository at this point in the history
  • Loading branch information
navin-moorthy committed Oct 5, 2020
1 parent 0c284a0 commit 7b70b22
Show file tree
Hide file tree
Showing 15 changed files with 365 additions and 238 deletions.
24 changes: 9 additions & 15 deletions src/calendar/stories/Calendar.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,16 @@ import { Meta } from "@storybook/react";
import { addDays, addWeeks, subWeeks } from "date-fns";

import "./index.css";
import { DateValue } from "../index";
import { CalendarComponent } from "./CalendarComponent";
import { CalendarStateInitialProps, useCalendarState, DateValue } from "..";

export default {
title: "Component/Calendar",
} as Meta;

const CalendarComp: React.FC<CalendarStateInitialProps> = props => {
const state = useCalendarState(props);

return <CalendarComponent {...state} />;
};

export const Default = () => <CalendarComp />;
export const Default = () => <CalendarComponent />;
export const DefaultValue = () => (
<CalendarComp defaultValue={new Date(2001, 0, 1)} />
<CalendarComponent defaultValue={new Date(2001, 0, 1)} />
);
export const ControlledValue = () => {
const [value, setValue] = React.useState<DateValue>(addDays(new Date(), 1));
Expand All @@ -30,27 +24,27 @@ export const ControlledValue = () => {
onChange={e => setValue(new Date(e.target.value))}
value={(value as Date).toISOString().slice(0, 10)}
/>
<CalendarComp value={value} onChange={setValue} />
<CalendarComponent value={value} onChange={setValue} />
</div>
);
};
export const MinMaxDate = () => (
<CalendarComp minValue={new Date()} maxValue={addWeeks(new Date(), 1)} />
<CalendarComponent minValue={new Date()} maxValue={addWeeks(new Date(), 1)} />
);
export const MinMaxDefaultDate = () => (
<CalendarComp
<CalendarComponent
defaultValue={new Date()}
minValue={subWeeks(new Date(), 1)}
maxValue={addWeeks(new Date(), 1)}
/>
);
export const isDisabled = () => (
<CalendarComp defaultValue={addDays(new Date(), 1)} isDisabled />
<CalendarComponent defaultValue={addDays(new Date(), 1)} isDisabled />
);
export const isReadOnly = () => (
<CalendarComp defaultValue={addDays(new Date(), 1)} isReadOnly />
<CalendarComponent defaultValue={addDays(new Date(), 1)} isReadOnly />
);
export const autoFocus = () => (
// eslint-disable-next-line jsx-a11y/no-autofocus
<CalendarComp defaultValue={addDays(new Date(), 1)} autoFocus />
<CalendarComponent defaultValue={addDays(new Date(), 1)} autoFocus />
);
12 changes: 8 additions & 4 deletions src/calendar/stories/CalendarComponent.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
import React from "react";

import "./index.css";
import {
Calendar as CalendarWrapper,
CalendarButton,
CalendarCell,
CalendarCellButton,
CalendarGrid,
CalendarHeader,
CalendarStateReturn,
CalendarWeekTitle,
} from "..";
import "./index.css";
CalendarStateInitialProps,
useCalendarState,
} from "../index";

export const CalendarComponent: React.FC<CalendarStateInitialProps> = props => {
const state = useCalendarState(props);

export const CalendarComponent: React.FC<CalendarStateReturn> = state => {
return (
<CalendarWrapper {...state} className="calendar">
<div className="header">
Expand Down
1 change: 0 additions & 1 deletion src/calendar/stories/index.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
.calendar {
margin-top: 1em;
max-width: 320px;
position: relative;
}
Expand Down
65 changes: 62 additions & 3 deletions src/datepicker/DatePicker.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,81 @@
import { BoxHTMLProps, BoxOptions, useBox } from "reakit";
import { createComponent, createHook } from "reakit-system";
import { ariaAttr, callAllHandlers } from "@chakra-ui/utils";

import { DATE_PICKER_KEYS } from "./__keys";
import { DatePickerStateReturn } from "./DatePickerState";

export type DatePickerOptions = BoxOptions;
export type DatePickerOptions = BoxOptions &
Pick<
DatePickerStateReturn,
| "visible"
| "validationState"
| "isDisabled"
| "isReadOnly"
| "isRequired"
| "show"
| "pickerId"
| "dialogId"
>;

export type DatePickerHTMLProps = BoxHTMLProps;

export type DatePickerProps = DatePickerOptions & DatePickerHTMLProps;

const isTouch = Boolean(
"ontouchstart" in window ||
window.navigator.maxTouchPoints > 0 ||
window.navigator.msMaxTouchPoints > 0,
);

export const useDatePicker = createHook<DatePickerOptions, DatePickerHTMLProps>(
{
name: "DatePicker",
compose: useBox,
keys: DATE_PICKER_KEYS,

useProps(options, htmlProps) {
return htmlProps;
useProps(
options,
{ onKeyDown: htmlOnKeyDown, onClick: htmlOnClick, ...htmlProps },
) {
const {
visible,
validationState,
isDisabled,
isReadOnly,
isRequired,
show,
pickerId,
dialogId,
} = options;

// Open the popover on alt + arrow down
const onKeyDown = (e: React.KeyboardEvent) => {
if (e.altKey && e.key === "ArrowDown") {
e.preventDefault();
e.stopPropagation();
show;
}
};

const onClick = (e: React.MouseEvent) => {
if (isTouch) show();
};

return {
id: pickerId,
role: "combobox",
"aria-haspopup": "dialog",
"aria-expanded": visible,
"aria-owns": visible ? dialogId : undefined,
"aria-invalid": ariaAttr(validationState === "invalid"),
"aria-disabled": ariaAttr(isDisabled),
"aria-readonly": ariaAttr(isReadOnly),
"aria-required": ariaAttr(isRequired),
onKeyDown: callAllHandlers(htmlOnKeyDown, onKeyDown),
onClick: callAllHandlers(htmlOnClick, onClick),
...htmlProps,
};
},
},
);
Expand Down
8 changes: 5 additions & 3 deletions src/datepicker/DatePickerContent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import { createComponent, createHook } from "reakit-system";
import { PopoverHTMLProps, PopoverOptions, usePopover } from "reakit";

import { DATE_PICKER_CONTENT_KEYS } from "./__keys";
import { DatePickerStateReturn } from "./DatePickerState";

export type DatePickerContentOptions = PopoverOptions;
export type DatePickerContentOptions = PopoverOptions &
Pick<DatePickerStateReturn, "dialogId">;

export type DatePickerContentHTMLProps = PopoverHTMLProps;

Expand All @@ -18,8 +20,8 @@ export const useDatePickerContent = createHook<
compose: usePopover,
keys: DATE_PICKER_CONTENT_KEYS,

useProps(options, htmlProps) {
return htmlProps;
useProps({ dialogId }, htmlProps) {
return { id: dialogId, ...htmlProps };
},
});

Expand Down
57 changes: 21 additions & 36 deletions src/datepicker/DatePickerFieldState.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
/**
* All credit goes to [React Spectrum](https://github.com/adobe/react-spectrum)
* We improved the Calendar from Stately [useCalendarState](https://github.com/adobe/react-spectrum/tree/main/packages/%40react-stately/calendar)
* We improved the Calendar from Stately [useDatePickerFieldState](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-stately/datepicker/src/useDatePickerFieldState.ts)
* to work with Reakit System
*/

import { useMemo, useState } from "react";
import { useDateFormatter } from "@react-aria/i18n";
import { useControlledState } from "@react-stately/utils";
import { useControllableState } from "@chakra-ui/hooks";

import { DatePickerProps } from "./index.d";
import { DatePickerStateInitialProps } from "./index.d";
import { add, setSegment, convertValue, getSegmentLimits } from "./__utils";

export interface IDateSegment {
Expand All @@ -20,19 +20,6 @@ export interface IDateSegment {
isPlaceholder: boolean;
}

export interface DatePickerFieldState {
value: Date;
setValue: (value: Date) => void;
segments: IDateSegment[];
dateFormatter: Intl.DateTimeFormat;
increment: (type: Intl.DateTimeFormatPartTypes) => void;
decrement: (type: Intl.DateTimeFormatPartTypes) => void;
incrementPage: (type: Intl.DateTimeFormatPartTypes) => void;
decrementPage: (type: Intl.DateTimeFormatPartTypes) => void;
setSegment: (type: Intl.DateTimeFormatPartTypes, value: number) => void;
confirmPlaceholder: (type: Intl.DateTimeFormatPartTypes) => void;
}

const EDITABLE_SEGMENTS = {
year: true,
month: true,
Expand All @@ -57,9 +44,7 @@ const TYPE_MAPPING = {
dayperiod: "dayPeriod",
};

export function useDatePickerFieldState(
props: DatePickerProps,
): DatePickerFieldState {
export function useDatePickerFieldState(props: DatePickerStateInitialProps) {
const [validSegments, setValidSegments] = useState(
props.value || props.defaultValue ? { ...EDITABLE_SEGMENTS } : {},
);
Expand Down Expand Up @@ -92,14 +77,15 @@ export function useDatePickerFieldState(
convertValue(props.placeholderDate) ||
new Date(new Date().getFullYear(), 0, 1),
);
const [date, setDate] = useControlledState<Date>(
// @ts-ignore
props.value === null
? convertValue(placeholderDate)
: convertValue(props.value),
convertValue(props.defaultValue),
props.onChange,
);
const [date, setDate] = useControllableState<Date>({
value:
props.value == null
? convertValue(placeholderDate)
: convertValue(props.value),
defaultValue: convertValue(props.defaultValue),
onChange: props.onChange,
shouldUpdate: (prev, next) => prev !== next,
});

// If all segments are valid, use the date from state, otherwise use the placeholder date.
const value =
Expand Down Expand Up @@ -128,34 +114,33 @@ export function useDatePickerFieldState(
) => {
validSegments[type] = true;
setValidSegments({ ...validSegments });
// @ts-ignore
setValue(add(value, type, amount, resolvedOptions));
};

return {
value,
setValue,
fieldValue: value,
setFieldValue: setValue,
segments,
dateFormatter,
increment(part) {
increment(part: Intl.DateTimeFormatPartTypes) {
adjustSegment(part, 1);
},
decrement(part) {
decrement(part: Intl.DateTimeFormatPartTypes) {
adjustSegment(part, -1);
},
incrementPage(part) {
incrementPage(part: Intl.DateTimeFormatPartTypes) {
adjustSegment(part, PAGE_STEP[part] || 1);
},
decrementPage(part) {
decrementPage(part: Intl.DateTimeFormatPartTypes) {
adjustSegment(part, -(PAGE_STEP[part] || 1));
},
setSegment(part, v) {
setSegment(part: Intl.DateTimeFormatPartTypes, v: number) {
validSegments[part] = true;
setValidSegments({ ...validSegments });
// @ts-ignore
setValue(setSegment(value, part, v, resolvedOptions));
},
confirmPlaceholder(part) {
confirmPlaceholder(part: Intl.DateTimeFormatPartTypes) {
validSegments[part] = true;
setValidSegments({ ...validSegments });
setValue(new Date(value));
Expand Down
Loading

0 comments on commit 7b70b22

Please sign in to comment.