From 8624923b99e371de9871324ee4d3c5e4178bcb41 Mon Sep 17 00:00:00 2001 From: Kevin Liu Date: Wed, 18 Dec 2024 11:30:31 -0800 Subject: [PATCH 01/13] DatePicker Draft --- .../src/Calendar/Calendar.module.scss | 7 + easy-ui-react/src/Calendar/CalendarBase.tsx | 4 +- .../src/Calendar/CalendarCell.module.scss | 2 +- easy-ui-react/src/Calendar/CalendarCell.tsx | 4 +- .../src/Calendar/CalendarGrid.module.scss | 2 + easy-ui-react/src/DatePicker/DateField.tsx | 64 ++++++++ .../src/DatePicker/DatePicker.module.scss | 94 ++++++++++++ .../src/DatePicker/DatePicker.stories.tsx | 86 +++++++++++ easy-ui-react/src/DatePicker/DatePicker.tsx | 119 +++++++++++++++ .../src/DatePicker/DatePickerOverlay.tsx | 61 ++++++++ .../src/DatePicker/DatePickerTrigger.tsx | 80 ++++++++++ easy-ui-react/src/DatePicker/index.ts | 1 + .../DateRangePicker/DatePickerQuickSelect.tsx | 31 ++++ .../DateRangePicker.stories.tsx | 143 ++++++++++++++++++ .../src/DateRangePicker/DateRangePicker.tsx | 134 ++++++++++++++++ easy-ui-react/src/DateRangePicker/index.ts | 1 + 16 files changed, 829 insertions(+), 4 deletions(-) create mode 100644 easy-ui-react/src/DatePicker/DateField.tsx create mode 100644 easy-ui-react/src/DatePicker/DatePicker.module.scss create mode 100644 easy-ui-react/src/DatePicker/DatePicker.stories.tsx create mode 100644 easy-ui-react/src/DatePicker/DatePicker.tsx create mode 100644 easy-ui-react/src/DatePicker/DatePickerOverlay.tsx create mode 100644 easy-ui-react/src/DatePicker/DatePickerTrigger.tsx create mode 100644 easy-ui-react/src/DatePicker/index.ts create mode 100644 easy-ui-react/src/DateRangePicker/DatePickerQuickSelect.tsx create mode 100644 easy-ui-react/src/DateRangePicker/DateRangePicker.stories.tsx create mode 100644 easy-ui-react/src/DateRangePicker/DateRangePicker.tsx create mode 100644 easy-ui-react/src/DateRangePicker/index.ts diff --git a/easy-ui-react/src/Calendar/Calendar.module.scss b/easy-ui-react/src/Calendar/Calendar.module.scss index f2cdaf94a..532092bcb 100644 --- a/easy-ui-react/src/Calendar/Calendar.module.scss +++ b/easy-ui-react/src/Calendar/Calendar.module.scss @@ -5,3 +5,10 @@ design-token("color.neutral.200"); border-radius: design-token("shape.border_radius.lg"); } + +.calendarContainer { + display: flex; + flex-direction: column; + gap: design-token('space.1'); + max-width: min-content; +} diff --git a/easy-ui-react/src/Calendar/CalendarBase.tsx b/easy-ui-react/src/Calendar/CalendarBase.tsx index 20ac2d85a..0819a1d69 100644 --- a/easy-ui-react/src/Calendar/CalendarBase.tsx +++ b/easy-ui-react/src/Calendar/CalendarBase.tsx @@ -73,7 +73,7 @@ export function CalendarBase(props: CalendarBaseProps) { ...restProps } = props; return ( - +
)} - +
); } diff --git a/easy-ui-react/src/Calendar/CalendarCell.module.scss b/easy-ui-react/src/Calendar/CalendarCell.module.scss index 850c7d607..bc8afed51 100644 --- a/easy-ui-react/src/Calendar/CalendarCell.module.scss +++ b/easy-ui-react/src/Calendar/CalendarCell.module.scss @@ -97,7 +97,7 @@ content: none; } - @include breakpoint-lg-up { + @include breakpoint-md-up { width: component-token("calendar-cell", "size"); height: component-token("calendar-cell", "size"); } diff --git a/easy-ui-react/src/Calendar/CalendarCell.tsx b/easy-ui-react/src/Calendar/CalendarCell.tsx index 6ec8ae39a..3ed46f6e2 100644 --- a/easy-ui-react/src/Calendar/CalendarCell.tsx +++ b/easy-ui-react/src/Calendar/CalendarCell.tsx @@ -44,7 +44,9 @@ export function CalendarCell({ state, date }: CalendarCellProps) { } if (!state.isInvalid(date)) { if (isRangeCalendar) { - rangeState.setValue(rangeState.highlightedRange); + if (!rangeState.anchorDate) { + rangeState.setValue(rangeState.highlightedRange); + } } else { singleState.setValue(date); } diff --git a/easy-ui-react/src/Calendar/CalendarGrid.module.scss b/easy-ui-react/src/Calendar/CalendarGrid.module.scss index 1c6f73349..bc8cdfd33 100644 --- a/easy-ui-react/src/Calendar/CalendarGrid.module.scss +++ b/easy-ui-react/src/Calendar/CalendarGrid.module.scss @@ -1,6 +1,8 @@ @use "../styles/common" as *; .CalendarGrid { border-collapse: collapse; + z-index: 999; + position: relative; } .CalendarGridHeader { background-color: design-token("color.primary.700"); diff --git a/easy-ui-react/src/DatePicker/DateField.tsx b/easy-ui-react/src/DatePicker/DateField.tsx new file mode 100644 index 000000000..04977f452 --- /dev/null +++ b/easy-ui-react/src/DatePicker/DateField.tsx @@ -0,0 +1,64 @@ +import React from "react"; +import { useDateField, useDateSegment, useLocale } from "react-aria"; +import { + useDateFieldState, + DateFieldState, + DateSegment as DateSegmentType, +} from "react-stately"; +import { createCalendar } from "@internationalized/date"; +import { DateValue } from "@react-types/calendar"; +import { HorizontalStack } from "../HorizontalStack"; +import { classNames } from "../utilities/css"; + +import styles from "./DatePicker.module.scss"; + +type DateFieldFieldProps = { + isDisabled?: boolean; + isReadOnly?: boolean; + isInvalid?: boolean; + isOpen?: boolean; + defaultOpen?: boolean; + value?: DateValue | null; + onChange?: (value: DateValue | null) => void; +}; +export function DateFieldField(props: DateFieldFieldProps) { + const dateFieldRef = React.useRef(null); + const { locale } = useLocale(); + const state = useDateFieldState({ ...props, locale, createCalendar }); + const { fieldProps } = useDateField(props, state, dateFieldRef); + + return ( +
+ + {state.segments.map((segment, i) => ( + + ))} + +
+ ); +} + +type DateSegmentProps = { + segment: DateSegmentType; + state: DateFieldState; +}; + +function DateSegment(props: DateSegmentProps) { + const { segment, state } = props; + const { type } = segment; + const dateSegmentRef = React.useRef(null); + const { segmentProps } = useDateSegment(segment, state, dateSegmentRef); + + return ( +
+ {segment.text} +
+ ); +} diff --git a/easy-ui-react/src/DatePicker/DatePicker.module.scss b/easy-ui-react/src/DatePicker/DatePicker.module.scss new file mode 100644 index 000000000..a2fe731f0 --- /dev/null +++ b/easy-ui-react/src/DatePicker/DatePicker.module.scss @@ -0,0 +1,94 @@ +@use "../styles/common" as *; +@use "../InputField/mixins" as Input; +@use "../Menu/mixins" as Menu; + +.DatePicker { + @include Input.root; +} + +.datePickerTrigger { + display: flex; + align-items: center; + @include Input.input; + + &:hover { + @include Input.hovered; + } + @include Input.iconEndInput; + +} + + +.datePickerTriggerContainer { + @include Input.inputIconContainer; +} + +.datePickerSm .datePickerTrigger { + @include Input.inputSm; + @include Input.iconEndInput; +} + +.errorInput { + @include Input.error; +} + +.underlay { + position: fixed; + inset: 0; +} + +.dialog { + @include Menu.root; + @include Menu.menu; + @include component-token("modal", "max-height", 762px); + @include component-token("modal", "max-width", 788px); + flex: 0 1 auto; + display: flex; + flex-direction: column; + margin: 0 auto; + max-height: component-token("modal", "max-height"); + max-width: component-token("modal", "max-width"); + width: 100%; + overflow: hidden; + background: theme-token("color.neutral.000"); + border-radius: design-token("shape.border_radius.lg"); + box-shadow: design-token("shadow.modal"); + pointer-events: auto; + outline: none; + + padding: design-token("space.5"); +} + +.quickSelection { + @include Menu.menuList; +} + +.option { + @include Menu.itemContentColor; + outline: none; + text-decoration: none; + margin-bottom: design-token("space.2"); + + &:hover { + color: design-token("color.primary.500"); + } +} + +.option :first-child { + cursor: pointer; +} + +.DateSegment { + padding: design-token("space.0-5"); + color: design-token("color.primary.800"); + &[data-placeholder="true"], + &.literalSegment { + color: design-token("color.neutral.500"); + } + &:focus-visible { + outline: none; + background-color: design-token("color.primary.500"); + border-radius: design-token("shape.border_radius.md"); + color: design-token("color.neutral.000"); + } +} diff --git a/easy-ui-react/src/DatePicker/DatePicker.stories.tsx b/easy-ui-react/src/DatePicker/DatePicker.stories.tsx new file mode 100644 index 000000000..c5e21c53f --- /dev/null +++ b/easy-ui-react/src/DatePicker/DatePicker.stories.tsx @@ -0,0 +1,86 @@ +import React from "react"; +import { Meta, StoryObj } from "@storybook/react"; +import { + today, + getLocalTimeZone, + isWeekend, + endOfWeek, +} from "@internationalized/date"; +import { DateValue, MappedDateValue } from "@react-types/calendar"; +import { InputDecorator } from "../utilities/storybook"; +import { DatePicker, DatePickerProps } from "./DatePicker"; +import { useLocale } from "react-aria"; + +type Story = StoryObj; + +const meta: Meta = { + title: "Components/DatePicker/DatePicker", + component: DatePicker, + decorators: [InputDecorator], +}; + +export default meta; + +const Template = (args: DatePickerProps) => ; + +export const Standard: Story = { + render: Template.bind({}), +}; + +export const Sizes: Story = { + render: Template.bind({}), + args: { + size: "sm", + }, +}; + +export const LimitAvailableDates: Story = { + render: Template.bind({}), + args: { + minValue: today(getLocalTimeZone()).subtract({ days: 10 }), + maxValue: today(getLocalTimeZone()), + }, +}; + +export const DatesAvailability: Story = { + render: Template.bind({}), + args: { + isDateUnavailable: (date) => today(getLocalTimeZone()).compare(date) > 0, + }, +}; + +export const ControlledSelection: Story = { + render: () => { + const [date, setDate] = React.useState | null>( + null, + ); + return ; + }, +}; + +export const InvalidSelection: Story = { + render: () => { + const { locale } = useLocale(); + const [date, setDate] = React.useState | null>( + endOfWeek(today(getLocalTimeZone()), locale), + ); + + const isInvalid = date ? isWeekend(date, locale) : false; + + return ( + + ); + }, +}; + +export const Disabled: Story = { + render: Template.bind({}), + args: { + isDisabled: true, + }, +}; diff --git a/easy-ui-react/src/DatePicker/DatePicker.tsx b/easy-ui-react/src/DatePicker/DatePicker.tsx new file mode 100644 index 000000000..fbed0db3e --- /dev/null +++ b/easy-ui-react/src/DatePicker/DatePicker.tsx @@ -0,0 +1,119 @@ +import React, { ReactNode } from "react"; +import { useDatePicker } from "react-aria"; +import { useDatePickerState } from "react-stately"; +import { DatePickerTrigger } from "./DatePickerTrigger"; +import { DatePickerOverlay } from "./DatePickerOverlay"; +import { DatePickerQuickSelect } from "../DateRangePicker/DatePickerQuickSelect"; +import { classNames, variationName } from "../utilities/css"; +import { Calendar } from "../Calendar"; +import { Text } from "../Text"; +import styles from "./DatePicker.module.scss"; +import { DateValue, MappedDateValue } from "@react-types/calendar"; + +export type DatePickerProps = { + /** + * The content to display as the label. + */ + label?: string; + /** + * The default value (uncontrolled). + */ + defaultValue?: DateValue | null; + /** + * The current value (controlled). + */ + value?: DateValue | null; + /** + * Handler that is called when the value changes. + */ + onChange?: (value: MappedDateValue | null) => void; + // onChange?: (value: DateValue | null) => void; + /** + * The minimum allowed date that a user may select. + */ + minValue?: DateValue; + /** + * The maximum allowed date that a user may select. + */ + maxValue?: DateValue; + /** + * Whether the input is disabled. + */ + isDisabled?: boolean; + /** + * Whether the input value is invalid. + */ + isInvalid?: boolean; + /** + * An error message to display when the selected value is invalid. + */ + errorMessage?: ReactNode; + /** + * Callback that is called for each date of the calendar. If + * it returns true, then the date is unavailable. + */ + isDateUnavailable?: (date: DateValue) => boolean; + /** + * The size of the DatePicker. + * @default md + */ + size?: "sm" | "md"; +}; +export function DatePicker(props: DatePickerProps) { + const { label, size = "md", isDisabled, isInvalid, errorMessage } = props; + const datePickerRef = React.useRef(null); + const triggerRef = React.useRef(null); + const state = useDatePickerState(props); + const { + groupProps, + labelProps, + fieldProps, + buttonProps, + dialogProps, + calendarProps, + } = useDatePicker(props, state, datePickerRef); + + const triggerProps = { + triggerRef, + datePickerRef, + buttonProps, + groupProps, + fieldProps, + isDisabled, + size, + isInvalid, + errorMessage, + state, + }; + + const overlayProps = { state, triggerRef, dialogProps }; + const className = classNames( + styles.DatePicker, + size && styles[variationName("datePicker", size)], + ); + return ( +
+ {label && ( + + {label} + + )} + + + + +
+ ); +} + +DatePicker.displayName = "DatePicker"; + +DatePicker.Trigger = DatePickerTrigger; + +DatePicker.Overlay = DatePickerOverlay; + +DatePicker.QuickSelect = DatePickerQuickSelect; diff --git a/easy-ui-react/src/DatePicker/DatePickerOverlay.tsx b/easy-ui-react/src/DatePicker/DatePickerOverlay.tsx new file mode 100644 index 000000000..0cea90d70 --- /dev/null +++ b/easy-ui-react/src/DatePicker/DatePickerOverlay.tsx @@ -0,0 +1,61 @@ +import React, { ReactNode, MutableRefObject } from "react"; +import { + AriaDialogProps, + DismissButton, + Overlay, + usePopover, +} from "react-aria"; + +import { + DEFAULT_PLACEMENT, + OVERLAY_OFFSET, + OVERLAY_PADDING_FROM_CONTAINER, +} from "../Menu/utilities"; +import styles from "./DatePicker.module.scss"; +import { DatePickerState, DateRangePickerState } from "react-stately"; + +type DatePickerOverlayProps = { + children: ReactNode; + triggerRef: MutableRefObject; + dialogProps: AriaDialogProps; + state: DatePickerState | DateRangePickerState; +}; + +export function DatePickerOverlay(props: DatePickerOverlayProps) { + const { state } = props; + if (!state.isOpen) { + return null; + } + + return ; +} + +function DatePickerContent(props: DatePickerOverlayProps) { + const { children, triggerRef, dialogProps, state } = props; + + const popoverRef = React.useRef(null); + + const { popoverProps, underlayProps } = usePopover( + { + containerPadding: OVERLAY_PADDING_FROM_CONTAINER, + offset: OVERLAY_OFFSET, + placement: DEFAULT_PLACEMENT, + popoverRef, + triggerRef, + }, + state, + ); + + return ( + +
+
+ +
+ {children} +
+ +
+ + ); +} diff --git a/easy-ui-react/src/DatePicker/DatePickerTrigger.tsx b/easy-ui-react/src/DatePicker/DatePickerTrigger.tsx new file mode 100644 index 000000000..0442236fa --- /dev/null +++ b/easy-ui-react/src/DatePicker/DatePickerTrigger.tsx @@ -0,0 +1,80 @@ +import React, { MutableRefObject, ReactNode } from "react"; +import CalendarMonth from "@easypost/easy-ui-icons/CalendarMonth"; +import { GroupDOMAttributes } from "@react-types/shared"; +import { AriaDatePickerProps, DateValue, AriaButtonProps } from "react-aria"; +import { InputIcon } from "../InputField/InputIcon"; +import { UnstyledButton } from "../UnstyledButton"; +import { DateFieldField } from "./DateField"; +import { VerticalStack } from "../VerticalStack"; +import { Text } from "../Text"; +import styles from "./DatePicker.module.scss"; +import { classNames } from "../utilities/css"; +import { DatePickerState, DateRangePickerState } from "react-stately"; + +export type DatePickerTriggerProps = { + isDisabled?: boolean; + size?: "sm" | "md"; + triggerRef: MutableRefObject; + datePickerRef: MutableRefObject; + buttonProps: AriaButtonProps; + groupProps: GroupDOMAttributes; + startFieldProps?: AriaDatePickerProps; + endFieldProps?: AriaDatePickerProps; + fieldProps?: AriaDatePickerProps; + isInvalid?: boolean; + errorMessage?: ReactNode; + state: DatePickerState | DateRangePickerState; +}; + +export function DatePickerTrigger(props: DatePickerTriggerProps) { + const { + isDisabled, + size, + triggerRef, + datePickerRef, + buttonProps, + groupProps, + startFieldProps, + endFieldProps, + fieldProps, + isInvalid, + errorMessage, + state, + } = props; + + const className = classNames( + styles.datePickerTrigger, + (isInvalid || state.isInvalid) && styles.errorInput, + ); + + return ( + +
+ + + {endFieldProps && ( + <> + + + + )} + + +
+ {(isInvalid || state.isInvalid) && ( + + {errorMessage} + + )} +
+ ); +} diff --git a/easy-ui-react/src/DatePicker/index.ts b/easy-ui-react/src/DatePicker/index.ts new file mode 100644 index 000000000..2f4cf5f51 --- /dev/null +++ b/easy-ui-react/src/DatePicker/index.ts @@ -0,0 +1 @@ +export * from "./DatePicker"; diff --git a/easy-ui-react/src/DateRangePicker/DatePickerQuickSelect.tsx b/easy-ui-react/src/DateRangePicker/DatePickerQuickSelect.tsx new file mode 100644 index 000000000..78e76cd05 --- /dev/null +++ b/easy-ui-react/src/DateRangePicker/DatePickerQuickSelect.tsx @@ -0,0 +1,31 @@ +import React from "react"; +import { DateRange } from "react-aria"; +import { UnstyledButton } from "../UnstyledButton"; +import { DateRangePickerState } from "react-stately"; +import styles from "../DatePicker/DatePicker.module.scss"; + +export type OptionProps = { + label: string; + dateRange: DateRange; +}; + +export type DatePickerQuickSelectProps = { + quickSelectOptions: OptionProps[]; + state: DateRangePickerState; +}; + +export function DatePickerQuickSelect(props: DatePickerQuickSelectProps) { + const { quickSelectOptions, state } = props; + + return ( +
    + {quickSelectOptions.map(({ label, dateRange }) => ( +
  • + state.setValue(dateRange)}> + {label} + +
  • + ))} +
+ ); +} diff --git a/easy-ui-react/src/DateRangePicker/DateRangePicker.stories.tsx b/easy-ui-react/src/DateRangePicker/DateRangePicker.stories.tsx new file mode 100644 index 000000000..1f2887818 --- /dev/null +++ b/easy-ui-react/src/DateRangePicker/DateRangePicker.stories.tsx @@ -0,0 +1,143 @@ +import React from "react"; +import { Meta, StoryObj } from "@storybook/react"; +import { today, getLocalTimeZone, startOfYear } from "@internationalized/date"; +import { DateRange } from "@react-types/calendar"; +import { InputDecorator } from "../utilities/storybook"; +import { DateRangePicker, DateRangePickerProps } from "./DateRangePicker"; + +type Story = StoryObj; + +const meta: Meta = { + title: "Components/DatePicker/DateRangePicker", + component: DateRangePicker, + decorators: [InputDecorator], +}; + +export default meta; + +const Template = (args: DateRangePickerProps) => ; + +const QUICK_SELECTION_OPTIONS = [ + { + label: "Today", + dateRange: { + start: today(getLocalTimeZone()), + end: today(getLocalTimeZone()), + }, + }, + { + label: "Yesterday", + dateRange: { + start: today(getLocalTimeZone()).subtract({ days: 1 }), + end: today(getLocalTimeZone()).subtract({ days: 1 }), + }, + }, + { + label: "Last 7 days", + dateRange: { + start: today(getLocalTimeZone()).subtract({ days: 6 }), + end: today(getLocalTimeZone()), + }, + }, + { + label: "Last 30 days", + dateRange: { + start: today(getLocalTimeZone()).subtract({ days: 29 }), + end: today(getLocalTimeZone()), + }, + }, + { + label: "Last 6 months", + dateRange: { + start: today(getLocalTimeZone()).subtract({ months: 6 }), + end: today(getLocalTimeZone()), + }, + }, + { + label: "Last 12 months", + dateRange: { + start: today(getLocalTimeZone()).subtract({ months: 12 }), + end: today(getLocalTimeZone()), + }, + }, + { + label: "Year to date", + dateRange: { + start: startOfYear(today(getLocalTimeZone())), + end: today(getLocalTimeZone()), + }, + }, +]; + +export const Standard: Story = { + render: Template.bind({}), +}; + +export const Sizes: Story = { + render: Template.bind({}), + args: { + size: "sm", + }, +}; + +export const LimitAvailableDates: Story = { + render: Template.bind({}), + args: { + minValue: today(getLocalTimeZone()).subtract({ days: 10 }), + maxValue: today(getLocalTimeZone()), + }, +}; + +export const DatesAvailability: Story = { + render: Template.bind({}), + args: { + isDateUnavailable: (date) => today(getLocalTimeZone()).compare(date) > 0, + }, +}; + +export const ControlledSelection: Story = { + render: () => { + const [date, setDate] = React.useState(); + return ; + }, +}; + +export const WithQuickSelection: Story = { + render: () => { + const [date, setDate] = React.useState(); + return ( + + ); + }, +}; + +export const InvalidSelection: Story = { + render: () => { + const [date, setDate] = React.useState({ + start: today(getLocalTimeZone()).subtract({ days: 7 }), + end: today(getLocalTimeZone()), + }); + + const isInvalid = date ? date.end.compare(date.start) >= 7 : false; + + return ( + + ); + }, +}; + +export const Disabled: Story = { + render: Template.bind({}), + args: { + isDisabled: true, + }, +}; diff --git a/easy-ui-react/src/DateRangePicker/DateRangePicker.tsx b/easy-ui-react/src/DateRangePicker/DateRangePicker.tsx new file mode 100644 index 000000000..f096b2ef6 --- /dev/null +++ b/easy-ui-react/src/DateRangePicker/DateRangePicker.tsx @@ -0,0 +1,134 @@ +import React, { ReactNode } from "react"; +import { useDateRangePicker } from "react-aria"; +import { useDateRangePickerState } from "react-stately"; +import { RangeValue } from "@react-types/shared"; +import { DateValue, MappedDateValue } from "@react-types/calendar"; +import { HorizontalStack } from "../HorizontalStack"; +import { DatePicker } from "../DatePicker"; +import { Text } from "../Text"; +import { RangeCalendar } from "../RangeCalendar"; +import { OptionProps } from "./DatePickerQuickSelect"; +import { classNames, variationName } from "../utilities/css"; +import styles from "../DatePicker/DatePicker.module.scss"; + +export type DateRangePickerProps = { + /** + * The content to display as the label. + */ + label?: string; + /** + * The default value (uncontrolled). + */ + defaultValue?: RangeValue | null; + /** + * The current value (controlled). + */ + value?: RangeValue | null; + /** + * Handler that is called when the value changes. + */ + onChange?: (value: RangeValue> | null) => void; + /** + * The minimum allowed date that a user may select. + */ + minValue?: DateValue; + /** + * The maximum allowed date that a user may select. + */ + maxValue?: DateValue; + /** + * Whether the input is disabled. + */ + isDisabled?: boolean; + /** + * Whether the input value is invalid. + */ + isInvalid?: boolean; + /** + * An error message to display when the selected value is invalid. + */ + errorMessage?: ReactNode; + /** + * Callback that is called for each date of the calendar. If + * it returns true, then the date is unavailable. + */ + isDateUnavailable?: (date: DateValue) => boolean; + /** + * The size of the DateRangePicker. + * @default md + */ + size?: "sm" | "md"; + /** + * A list of quick select options that provide users the + * ability to make selection faster. + */ + quickSelectOptions?: OptionProps[]; +}; + +export function DateRangePicker(props: DateRangePickerProps) { + const { + label, + size = "md", + isDisabled, + quickSelectOptions, + isInvalid, + errorMessage, + } = props; + const datePickerRef = React.useRef(null); + const triggerRef = React.useRef(null); + const state = useDateRangePickerState(props); + const { + groupProps, + labelProps, + startFieldProps, + endFieldProps, + buttonProps, + dialogProps, + calendarProps, + } = useDateRangePicker(props, state, datePickerRef); + + const triggerProps = { + triggerRef, + datePickerRef, + buttonProps, + groupProps, + startFieldProps, + endFieldProps, + isDisabled, + size, + isInvalid, + errorMessage: errorMessage || calendarProps.errorMessage, + state, + }; + const overlayProps = { state, triggerRef, dialogProps }; + + const className = classNames( + styles.DatePicker, + size && styles[variationName("datePicker", size)], + ); + return ( +
+ {label && ( + + {label} + + )} + + + + {quickSelectOptions && ( + + )} + + + +
+ ); +} diff --git a/easy-ui-react/src/DateRangePicker/index.ts b/easy-ui-react/src/DateRangePicker/index.ts new file mode 100644 index 000000000..78c1d8662 --- /dev/null +++ b/easy-ui-react/src/DateRangePicker/index.ts @@ -0,0 +1 @@ +export * from "./DateRangePicker"; From 4d5d53d31a7dae5540f14827fe273907a8e43013 Mon Sep 17 00:00:00 2001 From: Kevin Liu Date: Wed, 18 Dec 2024 11:31:54 -0800 Subject: [PATCH 02/13] fix lint --- easy-ui-react/src/Calendar/Calendar.module.scss | 2 +- easy-ui-react/src/Calendar/CalendarBase.tsx | 1 - easy-ui-react/src/DatePicker/DatePicker.module.scss | 2 -- 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/easy-ui-react/src/Calendar/Calendar.module.scss b/easy-ui-react/src/Calendar/Calendar.module.scss index 532092bcb..f528284e2 100644 --- a/easy-ui-react/src/Calendar/Calendar.module.scss +++ b/easy-ui-react/src/Calendar/Calendar.module.scss @@ -9,6 +9,6 @@ .calendarContainer { display: flex; flex-direction: column; - gap: design-token('space.1'); + gap: design-token("space.1"); max-width: min-content; } diff --git a/easy-ui-react/src/Calendar/CalendarBase.tsx b/easy-ui-react/src/Calendar/CalendarBase.tsx index 0819a1d69..9606fb8be 100644 --- a/easy-ui-react/src/Calendar/CalendarBase.tsx +++ b/easy-ui-react/src/Calendar/CalendarBase.tsx @@ -1,6 +1,5 @@ import React, { ReactNode, HTMLAttributes } from "react"; import { AriaButtonProps } from "react-aria"; -import { VerticalStack } from "../VerticalStack"; import { Text } from "../Text"; import { CalendarState, RangeCalendarState } from "@react-stately/calendar"; import { RefObject } from "@react-types/shared"; diff --git a/easy-ui-react/src/DatePicker/DatePicker.module.scss b/easy-ui-react/src/DatePicker/DatePicker.module.scss index a2fe731f0..bc173f4c0 100644 --- a/easy-ui-react/src/DatePicker/DatePicker.module.scss +++ b/easy-ui-react/src/DatePicker/DatePicker.module.scss @@ -15,10 +15,8 @@ @include Input.hovered; } @include Input.iconEndInput; - } - .datePickerTriggerContainer { @include Input.inputIconContainer; } From de27aeb7ab80d0c27d54faf74a563e17f9140eea Mon Sep 17 00:00:00 2001 From: Kevin Liu Date: Wed, 18 Dec 2024 17:06:44 -0800 Subject: [PATCH 03/13] feat: DatePicker and DateRangePicker --- .../src/Calendar/Calendar.module.scss | 7 - easy-ui-react/src/Calendar/CalendarBase.tsx | 5 +- easy-ui-react/src/DatePicker/DatePicker.mdx | 62 +++++++++ .../src/DatePicker/DatePicker.module.scss | 33 ----- .../src/DatePicker/DatePicker.stories.tsx | 13 +- .../src/DatePicker/DatePicker.test.tsx | 111 ++++++++++++++++ easy-ui-react/src/DatePicker/DatePicker.tsx | 23 +++- .../DateRangePicker/DatePickerQuickSelect.tsx | 31 ----- .../src/DateRangePicker/DateRangePicker.mdx | 62 +++++++++ .../DateRangePicker.stories.tsx | 73 ++-------- .../DateRangePicker/DateRangePicker.test.tsx | 125 ++++++++++++++++++ .../src/DateRangePicker/DateRangePicker.tsx | 27 ++-- .../src/RangeCalendar/RangeCalendar.test.tsx | 2 +- 13 files changed, 416 insertions(+), 158 deletions(-) create mode 100644 easy-ui-react/src/DatePicker/DatePicker.mdx create mode 100644 easy-ui-react/src/DatePicker/DatePicker.test.tsx delete mode 100644 easy-ui-react/src/DateRangePicker/DatePickerQuickSelect.tsx create mode 100644 easy-ui-react/src/DateRangePicker/DateRangePicker.mdx create mode 100644 easy-ui-react/src/DateRangePicker/DateRangePicker.test.tsx diff --git a/easy-ui-react/src/Calendar/Calendar.module.scss b/easy-ui-react/src/Calendar/Calendar.module.scss index f528284e2..f2cdaf94a 100644 --- a/easy-ui-react/src/Calendar/Calendar.module.scss +++ b/easy-ui-react/src/Calendar/Calendar.module.scss @@ -5,10 +5,3 @@ design-token("color.neutral.200"); border-radius: design-token("shape.border_radius.lg"); } - -.calendarContainer { - display: flex; - flex-direction: column; - gap: design-token("space.1"); - max-width: min-content; -} diff --git a/easy-ui-react/src/Calendar/CalendarBase.tsx b/easy-ui-react/src/Calendar/CalendarBase.tsx index 9606fb8be..20ac2d85a 100644 --- a/easy-ui-react/src/Calendar/CalendarBase.tsx +++ b/easy-ui-react/src/Calendar/CalendarBase.tsx @@ -1,5 +1,6 @@ import React, { ReactNode, HTMLAttributes } from "react"; import { AriaButtonProps } from "react-aria"; +import { VerticalStack } from "../VerticalStack"; import { Text } from "../Text"; import { CalendarState, RangeCalendarState } from "@react-stately/calendar"; import { RefObject } from "@react-types/shared"; @@ -72,7 +73,7 @@ export function CalendarBase(props: CalendarBaseProps) { ...restProps } = props; return ( -
+
)} -
+
); } diff --git a/easy-ui-react/src/DatePicker/DatePicker.mdx b/easy-ui-react/src/DatePicker/DatePicker.mdx new file mode 100644 index 000000000..950e35c47 --- /dev/null +++ b/easy-ui-react/src/DatePicker/DatePicker.mdx @@ -0,0 +1,62 @@ +import React from "react"; +import { ArgTypes, Canvas, Meta, Controls } from "@storybook/blocks"; +import { DatePicker } from "./DatePicker"; +import * as DatePickerStories from "./DatePicker.stories"; + + + +# DatePicker + +DatePicker combine a DateField and a Calendar popover to allow users to enter or select a date. + + + +## Default value + +A `` component allows users to set default selected date (uncontrolled). + + + +## Sizes + +Use `size="sm"` to render a smaller ``. + + + + + +## Set minimum and maximum value + +Set minimum and maximum allowed date that a user may select or enter. Dates outside the minimum and maximum value are disabled and unreachable. + + + +## Set Date Availability + +Callback that is called for each date of the calendar. If it returns `true`, then the date is unavailable. + + + +## Disabled Calendar + +Use `isDisabled` to disabled the calendar entirely. + + + + + +## Controlled Selection + +For controlled selection, use `onChange` and `value`. Uncontrolled selection is possible with `defaultValue`. See associated code snippet. + + + +## Invalid Selection + +An error message is displayed when the selected value is invalid according to application logic. Select a weekend date to see an invalid selection. + + + +## Properties + + diff --git a/easy-ui-react/src/DatePicker/DatePicker.module.scss b/easy-ui-react/src/DatePicker/DatePicker.module.scss index bc173f4c0..a27e44d46 100644 --- a/easy-ui-react/src/DatePicker/DatePicker.module.scss +++ b/easy-ui-react/src/DatePicker/DatePicker.module.scss @@ -36,44 +36,11 @@ } .dialog { - @include Menu.root; - @include Menu.menu; - @include component-token("modal", "max-height", 762px); - @include component-token("modal", "max-width", 788px); - flex: 0 1 auto; - display: flex; - flex-direction: column; - margin: 0 auto; - max-height: component-token("modal", "max-height"); - max-width: component-token("modal", "max-width"); - width: 100%; - overflow: hidden; background: theme-token("color.neutral.000"); border-radius: design-token("shape.border_radius.lg"); box-shadow: design-token("shadow.modal"); pointer-events: auto; outline: none; - - padding: design-token("space.5"); -} - -.quickSelection { - @include Menu.menuList; -} - -.option { - @include Menu.itemContentColor; - outline: none; - text-decoration: none; - margin-bottom: design-token("space.2"); - - &:hover { - color: design-token("color.primary.500"); - } -} - -.option :first-child { - cursor: pointer; } .DateSegment { diff --git a/easy-ui-react/src/DatePicker/DatePicker.stories.tsx b/easy-ui-react/src/DatePicker/DatePicker.stories.tsx index c5e21c53f..74afb423a 100644 --- a/easy-ui-react/src/DatePicker/DatePicker.stories.tsx +++ b/easy-ui-react/src/DatePicker/DatePicker.stories.tsx @@ -15,6 +15,7 @@ type Story = StoryObj; const meta: Meta = { title: "Components/DatePicker/DatePicker", + args: { "aria-label": "Date picker" }, component: DatePicker, decorators: [InputDecorator], }; @@ -27,6 +28,13 @@ export const Standard: Story = { render: Template.bind({}), }; +export const DefaultValue: Story = { + render: Template.bind({}), + args: { + defaultValue: today(getLocalTimeZone()), + }, +}; + export const Sizes: Story = { render: Template.bind({}), args: { @@ -54,7 +62,9 @@ export const ControlledSelection: Story = { const [date, setDate] = React.useState | null>( null, ); - return ; + return ( + + ); }, }; @@ -69,6 +79,7 @@ export const InvalidSelection: Story = { return ( ", () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + afterEach(() => { + vi.useRealTimers(); + }); + + it("should render a date picker", () => { + render(); + expect( + screen.getByRole("button", { name: /calendar/i }), + ).toBeInTheDocument(); + }); + + it("should open up a popover with calendar", async () => { + const { user } = render(); + await clickElement(user, screen.getByRole("button", { name: /calendar/i })); + const dateObject = startOfMonth(today(getLocalTimeZone())).toDate( + getLocalTimeZone(), + ); + const name = dateObject.toLocaleString("default", { + month: "long", + year: "numeric", + }); + expect(screen.getByRole("heading", { name })).toBeInTheDocument(); + }); + + it("should have default selected value", async () => { + const { user } = render(); + // Date display on the DateField + expect(screen.getByRole("spinbutton", { name: /month/i })).toHaveValue( + month, + ); + expect(screen.getByRole("spinbutton", { name: /day/i })).toHaveValue(4); + expect(screen.getByRole("spinbutton", { name: /year/i })).toHaveValue(year); + // Open popover + await clickElement(user, screen.getByRole("button", { name: /calendar/i })); + // Date selected in Calendar + expect(screen.getByRole("gridcell", { name: "4" })).toHaveAttribute( + "aria-selected", + ); + }); + + it("should have minimum and maximun date that a user may select", async () => { + const { user } = render( + , + ); + await clickElement(user, screen.getByRole("button", { name: /calendar/i })); + expect( + screen.getByRole("button", { name: /Thursday, July 25, 2024/i }), + ).toHaveAttribute("aria-disabled"); + expect( + screen.getByRole("button", { name: /Friday, July 5, 2024/i }), + ).not.toHaveAttribute("aria-disabled"); + }); + + it("should render past date unavailable", async () => { + const setDateUnavailable = (date: DateValue) => + defaultValue.compare(date) > 0; + const { user } = render( + , + ); + await clickElement(user, screen.getByRole("button", { name: /calendar/i })); + expect( + screen.getByRole("heading", { name: /July 2024/i }), + ).toBeInTheDocument(); + expect( + screen.getByRole("button", { name: /Wednesday, July 3, 2024/i }), + ).toHaveAttribute("aria-disabled"); + expect( + screen.getByRole("button", { name: /Friday, July 5, 2024/i }), + ).not.toHaveAttribute("aria-disabled"); + }); + + it("should be disabled", () => { + render(); + expect(screen.getByRole("group")).toHaveAttribute("aria-disabled"); + const dateFields = screen.getAllByRole("spinbutton"); + dateFields.every((field) => expect(field).toHaveAttribute("aria-disabled")); + }); + + it("should show error message when date is invalid", async () => { + render(); + expect(screen.getByText("This date is invalid")).toBeInTheDocument(); + }); +}); diff --git a/easy-ui-react/src/DatePicker/DatePicker.tsx b/easy-ui-react/src/DatePicker/DatePicker.tsx index fbed0db3e..f4a77c5b5 100644 --- a/easy-ui-react/src/DatePicker/DatePicker.tsx +++ b/easy-ui-react/src/DatePicker/DatePicker.tsx @@ -3,14 +3,18 @@ import { useDatePicker } from "react-aria"; import { useDatePickerState } from "react-stately"; import { DatePickerTrigger } from "./DatePickerTrigger"; import { DatePickerOverlay } from "./DatePickerOverlay"; -import { DatePickerQuickSelect } from "../DateRangePicker/DatePickerQuickSelect"; import { classNames, variationName } from "../utilities/css"; import { Calendar } from "../Calendar"; import { Text } from "../Text"; import styles from "./DatePicker.module.scss"; import { DateValue, MappedDateValue } from "@react-types/calendar"; +import { logWarningForMissingAriaLabel } from "../InputField/utilities"; export type DatePickerProps = { + /** + * Accessibility label for input field. + */ + "aria-label"?: string; /** * The content to display as the label. */ @@ -60,7 +64,14 @@ export type DatePickerProps = { size?: "sm" | "md"; }; export function DatePicker(props: DatePickerProps) { - const { label, size = "md", isDisabled, isInvalid, errorMessage } = props; + const { + label, + size = "md", + isDisabled, + isInvalid, + errorMessage, + "aria-label": ariaLabel, + } = props; const datePickerRef = React.useRef(null); const triggerRef = React.useRef(null); const state = useDatePickerState(props); @@ -73,6 +84,8 @@ export function DatePicker(props: DatePickerProps) { calendarProps, } = useDatePicker(props, state, datePickerRef); + logWarningForMissingAriaLabel(label, ariaLabel); + const triggerProps = { triggerRef, datePickerRef, @@ -85,7 +98,6 @@ export function DatePicker(props: DatePickerProps) { errorMessage, state, }; - const overlayProps = { state, triggerRef, dialogProps }; const className = classNames( styles.DatePicker, @@ -104,7 +116,8 @@ export function DatePicker(props: DatePickerProps) { )} - + {/** Set Calendar to always valid to prevent displaying error message under Calendar */} +
); @@ -115,5 +128,3 @@ DatePicker.displayName = "DatePicker"; DatePicker.Trigger = DatePickerTrigger; DatePicker.Overlay = DatePickerOverlay; - -DatePicker.QuickSelect = DatePickerQuickSelect; diff --git a/easy-ui-react/src/DateRangePicker/DatePickerQuickSelect.tsx b/easy-ui-react/src/DateRangePicker/DatePickerQuickSelect.tsx deleted file mode 100644 index 78e76cd05..000000000 --- a/easy-ui-react/src/DateRangePicker/DatePickerQuickSelect.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import React from "react"; -import { DateRange } from "react-aria"; -import { UnstyledButton } from "../UnstyledButton"; -import { DateRangePickerState } from "react-stately"; -import styles from "../DatePicker/DatePicker.module.scss"; - -export type OptionProps = { - label: string; - dateRange: DateRange; -}; - -export type DatePickerQuickSelectProps = { - quickSelectOptions: OptionProps[]; - state: DateRangePickerState; -}; - -export function DatePickerQuickSelect(props: DatePickerQuickSelectProps) { - const { quickSelectOptions, state } = props; - - return ( -
    - {quickSelectOptions.map(({ label, dateRange }) => ( -
  • - state.setValue(dateRange)}> - {label} - -
  • - ))} -
- ); -} diff --git a/easy-ui-react/src/DateRangePicker/DateRangePicker.mdx b/easy-ui-react/src/DateRangePicker/DateRangePicker.mdx new file mode 100644 index 000000000..b18e28fef --- /dev/null +++ b/easy-ui-react/src/DateRangePicker/DateRangePicker.mdx @@ -0,0 +1,62 @@ +import React from "react"; +import { ArgTypes, Canvas, Meta, Controls } from "@storybook/blocks"; +import { DateRangePicker } from "./DateRangePicker"; +import * as DateRangePickerStories from "./DateRangePicker.stories"; + + + +# DateRangePicker + +DateRangePicker combine DateFields and a Calendar popover to allow users to enter or select a range of dates. + + + +## Default values + +A `` component allows users to set default selected date (uncontrolled). + + + +## Sizes + +Use `size="sm"` to render a smaller ``. + + + + + +## Set minimum and maximum value + +Set minimum and maximum allowed date that a user may select or enter. Dates outside the minimum and maximum value are disabled and unreachable. + + + +## Set Date Availability + +Callback that is called for each date of the calendar. If it returns `true`, then the date is unavailable. + + + +## Disabled Calendar + +Use `isDisabled` to disabled the calendar entirely. + + + + + +## Controlled Selection + +For controlled selection, use `onChange` and `value`. Uncontrolled selection is possible with `defaultValue`. See associated code snippet. + + + +## Invalid Selection + +An error message is displayed when the selected value is invalid according to application logic. Select more than 7 days to see the error message. + + + +## Properties + + diff --git a/easy-ui-react/src/DateRangePicker/DateRangePicker.stories.tsx b/easy-ui-react/src/DateRangePicker/DateRangePicker.stories.tsx index 1f2887818..5d3a3e509 100644 --- a/easy-ui-react/src/DateRangePicker/DateRangePicker.stories.tsx +++ b/easy-ui-react/src/DateRangePicker/DateRangePicker.stories.tsx @@ -1,6 +1,6 @@ import React from "react"; import { Meta, StoryObj } from "@storybook/react"; -import { today, getLocalTimeZone, startOfYear } from "@internationalized/date"; +import { today, getLocalTimeZone } from "@internationalized/date"; import { DateRange } from "@react-types/calendar"; import { InputDecorator } from "../utilities/storybook"; import { DateRangePicker, DateRangePickerProps } from "./DateRangePicker"; @@ -10,6 +10,7 @@ type Story = StoryObj; const meta: Meta = { title: "Components/DatePicker/DateRangePicker", component: DateRangePicker, + args: { "aria-label": "Range date picker" }, decorators: [InputDecorator], }; @@ -17,60 +18,18 @@ export default meta; const Template = (args: DateRangePickerProps) => ; -const QUICK_SELECTION_OPTIONS = [ - { - label: "Today", - dateRange: { - start: today(getLocalTimeZone()), - end: today(getLocalTimeZone()), - }, - }, - { - label: "Yesterday", - dateRange: { - start: today(getLocalTimeZone()).subtract({ days: 1 }), - end: today(getLocalTimeZone()).subtract({ days: 1 }), - }, - }, - { - label: "Last 7 days", - dateRange: { - start: today(getLocalTimeZone()).subtract({ days: 6 }), - end: today(getLocalTimeZone()), - }, - }, - { - label: "Last 30 days", - dateRange: { - start: today(getLocalTimeZone()).subtract({ days: 29 }), - end: today(getLocalTimeZone()), - }, - }, - { - label: "Last 6 months", - dateRange: { - start: today(getLocalTimeZone()).subtract({ months: 6 }), - end: today(getLocalTimeZone()), - }, - }, - { - label: "Last 12 months", - dateRange: { - start: today(getLocalTimeZone()).subtract({ months: 12 }), - end: today(getLocalTimeZone()), - }, - }, - { - label: "Year to date", - dateRange: { - start: startOfYear(today(getLocalTimeZone())), +export const Standard: Story = { + render: Template.bind({}), +}; + +export const DefaultValue: Story = { + render: Template.bind({}), + args: { + defaultValue: { + start: today(getLocalTimeZone()).subtract({ days: 7 }), end: today(getLocalTimeZone()), }, }, -]; - -export const Standard: Story = { - render: Template.bind({}), }; export const Sizes: Story = { @@ -96,20 +55,13 @@ export const DatesAvailability: Story = { }; export const ControlledSelection: Story = { - render: () => { - const [date, setDate] = React.useState(); - return ; - }, -}; - -export const WithQuickSelection: Story = { render: () => { const [date, setDate] = React.useState(); return ( ); }, @@ -126,6 +78,7 @@ export const InvalidSelection: Story = { return ( ", () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + afterEach(() => { + vi.useRealTimers(); + }); + + it("should render a date range picker", () => { + render(); + expect( + screen.getByRole("button", { name: /calendar/i }), + ).toBeInTheDocument(); + }); + + it("should open up a popover with calendar", async () => { + const { user } = render(); + await clickElement(user, screen.getByRole("button", { name: /calendar/i })); + const dateObject = startOfMonth(today(getLocalTimeZone())).toDate( + getLocalTimeZone(), + ); + const name = dateObject.toLocaleString("default", { + month: "long", + year: "numeric", + }); + expect(screen.getByRole("heading", { name })).toBeInTheDocument(); + }); + + it("should have default selected value", async () => { + const { user } = render(); + // Dates display on the DateField + expect( + screen.getByRole("spinbutton", { name: /month, start date/i }), + ).toHaveValue(month); + expect( + screen.getByRole("spinbutton", { name: /day, start date/i }), + ).toHaveValue(4); + expect( + screen.getByRole("spinbutton", { name: /year, start date/i }), + ).toHaveValue(year); + expect( + screen.getByRole("spinbutton", { name: /month, end date/i }), + ).toHaveValue(month); + expect( + screen.getByRole("spinbutton", { name: /day, end date/i }), + ).toHaveValue(10); + expect( + screen.getByRole("spinbutton", { name: /year, end date/i }), + ).toHaveValue(year); + // Open popover + await clickElement(user, screen.getByRole("button", { name: /calendar/i })); + // Dates selected in Calendar + expect(screen.getAllByRole("gridcell", { selected: true })).toHaveLength(7); + }); + + it("should have minimum and maximun date that a user may select", async () => { + const { user } = render( + , + ); + await clickElement(user, screen.getByRole("button", { name: /calendar/i })); + expect( + screen.getByRole("button", { name: /Thursday, July 25, 2024/i }), + ).toHaveAttribute("aria-disabled"); + expect( + screen.getByRole("button", { name: /Friday, July 5, 2024/i }), + ).not.toHaveAttribute("aria-disabled"); + }); + + it("should render past date unavailable", async () => { + const setDateUnavailable = (date: DateValue) => + defaultValue.start.compare(date) > 0; + const { user } = render( + , + ); + await clickElement(user, screen.getByRole("button", { name: /calendar/i })); + expect( + screen.getByRole("heading", { name: /July 2024/i }), + ).toBeInTheDocument(); + expect( + screen.getByRole("button", { name: /Wednesday, July 3, 2024/i }), + ).toHaveAttribute("aria-disabled"); + expect( + screen.getByRole("button", { name: /Friday, July 5, 2024/i }), + ).not.toHaveAttribute("aria-disabled"); + }); + + it("should be disabled", () => { + render(); + expect(screen.getByRole("group")).toHaveAttribute("aria-disabled"); + const dateFields = screen.getAllByRole("spinbutton"); + dateFields.every((field) => expect(field).toHaveAttribute("aria-disabled")); + }); + + it("should show error message when date is invalid", async () => { + render(); + expect(screen.getByText("This date is invalid")).toBeInTheDocument(); + }); +}); diff --git a/easy-ui-react/src/DateRangePicker/DateRangePicker.tsx b/easy-ui-react/src/DateRangePicker/DateRangePicker.tsx index f096b2ef6..efc9439dc 100644 --- a/easy-ui-react/src/DateRangePicker/DateRangePicker.tsx +++ b/easy-ui-react/src/DateRangePicker/DateRangePicker.tsx @@ -3,15 +3,18 @@ import { useDateRangePicker } from "react-aria"; import { useDateRangePickerState } from "react-stately"; import { RangeValue } from "@react-types/shared"; import { DateValue, MappedDateValue } from "@react-types/calendar"; -import { HorizontalStack } from "../HorizontalStack"; import { DatePicker } from "../DatePicker"; import { Text } from "../Text"; import { RangeCalendar } from "../RangeCalendar"; -import { OptionProps } from "./DatePickerQuickSelect"; import { classNames, variationName } from "../utilities/css"; import styles from "../DatePicker/DatePicker.module.scss"; +import { logWarningForMissingAriaLabel } from "../InputField/utilities"; export type DateRangePickerProps = { + /** + * Accessibility label for input field. + */ + "aria-label"?: string; /** * The content to display as the label. */ @@ -58,11 +61,6 @@ export type DateRangePickerProps = { * @default md */ size?: "sm" | "md"; - /** - * A list of quick select options that provide users the - * ability to make selection faster. - */ - quickSelectOptions?: OptionProps[]; }; export function DateRangePicker(props: DateRangePickerProps) { @@ -70,9 +68,9 @@ export function DateRangePicker(props: DateRangePickerProps) { label, size = "md", isDisabled, - quickSelectOptions, isInvalid, errorMessage, + "aria-label": ariaLabel, } = props; const datePickerRef = React.useRef(null); const triggerRef = React.useRef(null); @@ -87,6 +85,8 @@ export function DateRangePicker(props: DateRangePickerProps) { calendarProps, } = useDateRangePicker(props, state, datePickerRef); + logWarningForMissingAriaLabel(label, ariaLabel); + const triggerProps = { triggerRef, datePickerRef, @@ -119,15 +119,8 @@ export function DateRangePicker(props: DateRangePickerProps) { )} - - {quickSelectOptions && ( - - )} - - + {/** Set Calendar to always valid to prevent displaying error message under Calendar */} +
); diff --git a/easy-ui-react/src/RangeCalendar/RangeCalendar.test.tsx b/easy-ui-react/src/RangeCalendar/RangeCalendar.test.tsx index ddc57045e..63dcacaa0 100644 --- a/easy-ui-react/src/RangeCalendar/RangeCalendar.test.tsx +++ b/easy-ui-react/src/RangeCalendar/RangeCalendar.test.tsx @@ -109,6 +109,6 @@ describe("", () => { }); }); -async function clickElement(user: UserEvent, el: HTMLElement) { +export async function clickElement(user: UserEvent, el: HTMLElement) { await userClick(user, el); } From 385f210b8e3435deb6445bca685474620a076b91 Mon Sep 17 00:00:00 2001 From: Kevin Liu Date: Wed, 18 Dec 2024 17:08:26 -0800 Subject: [PATCH 04/13] style update --- easy-ui-react/src/Calendar/CalendarCell.module.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/easy-ui-react/src/Calendar/CalendarCell.module.scss b/easy-ui-react/src/Calendar/CalendarCell.module.scss index bc8afed51..7c256d349 100644 --- a/easy-ui-react/src/Calendar/CalendarCell.module.scss +++ b/easy-ui-react/src/Calendar/CalendarCell.module.scss @@ -14,7 +14,7 @@ component-token("calendar-cell", "mobile-size") + design-token("space.0-5") ) / 2 ); - @include breakpoint-lg-up { + @include breakpoint-md-up { left: calc( (component-token("calendar-cell", "size") + design-token("space.1")) / 2 ); @@ -27,7 +27,7 @@ @include rangeSelection; } - @include breakpoint-lg-up { + @include breakpoint-md-up { padding: design-token("space.1.5") design-token("space.1"); } } From 8980ce25aef981947b8ea1d904d3b5e82a7a9521 Mon Sep 17 00:00:00 2001 From: Kevin Liu Date: Thu, 19 Dec 2024 10:56:56 -0800 Subject: [PATCH 05/13] fix calendar styles and refactor --- .../src/Calendar/CalendarCell.module.scss | 6 +- easy-ui-react/src/Calendar/CalendarGrid.tsx | 2 +- .../src/Calendar/CalendarHeader.module.scss | 3 +- .../src/DatePicker/DatePicker.stories.tsx | 2 +- easy-ui-react/src/DatePicker/DatePicker.tsx | 48 ++++------ .../src/DatePicker/DatePickerBase.tsx | 92 +++++++++++++++++++ .../src/DatePicker/DatePickerOverlay.tsx | 3 +- .../src/DatePicker/DatePickerTrigger.tsx | 4 +- .../src/DateRangePicker/DateRangePicker.tsx | 44 +++------ 9 files changed, 130 insertions(+), 74 deletions(-) create mode 100644 easy-ui-react/src/DatePicker/DatePickerBase.tsx diff --git a/easy-ui-react/src/Calendar/CalendarCell.module.scss b/easy-ui-react/src/Calendar/CalendarCell.module.scss index 7c256d349..2e0c8b98d 100644 --- a/easy-ui-react/src/Calendar/CalendarCell.module.scss +++ b/easy-ui-react/src/Calendar/CalendarCell.module.scss @@ -22,18 +22,18 @@ } .CellContainer { - padding: design-token("space.0-5"); + padding: design-token("space.0-5") design-token("space.0-5"); .rangeSelectionStart:before { @include rangeSelection; } @include breakpoint-md-up { - padding: design-token("space.1.5") design-token("space.1"); + padding: 10px design-token("space.1"); } } .CalendarCell { - @include component-token("calendar-cell", "size", design-token("space.4-5")); + @include component-token("calendar-cell", "size", design-token("space.4")); @include component-token( "calendar-cell", "mobile-size", diff --git a/easy-ui-react/src/Calendar/CalendarGrid.tsx b/easy-ui-react/src/Calendar/CalendarGrid.tsx index 85552b067..5211cc29a 100644 --- a/easy-ui-react/src/Calendar/CalendarGrid.tsx +++ b/easy-ui-react/src/Calendar/CalendarGrid.tsx @@ -27,7 +27,7 @@ export function CalendarGrid({ state, ...props }: CalendarGridProps) { {weekDays.map((day, index) => ( - + {day} diff --git a/easy-ui-react/src/Calendar/CalendarHeader.module.scss b/easy-ui-react/src/Calendar/CalendarHeader.module.scss index cfaf18c51..35316c730 100644 --- a/easy-ui-react/src/Calendar/CalendarHeader.module.scss +++ b/easy-ui-react/src/Calendar/CalendarHeader.module.scss @@ -1,14 +1,13 @@ @use "../styles/common" as *; .CalendarHeader { - @include component-token("calendar-header", "padding", 6px); @include component-token("calendar-header", "line-height", 19.5px); @include component-token("calendar-header", "border-radius", 7px); width: 100%; background-color: design-token("color.primary.800"); border-top-left-radius: component-token("calendar-header", "border-radius"); border-top-right-radius: component-token("calendar-header", "border-radius"); - padding: component-token("calendar-header", "padding"); + padding: design-token("space.1"); span { line-height: component-token("calendar-header", "line-height"); } diff --git a/easy-ui-react/src/DatePicker/DatePicker.stories.tsx b/easy-ui-react/src/DatePicker/DatePicker.stories.tsx index 74afb423a..9922cca6c 100644 --- a/easy-ui-react/src/DatePicker/DatePicker.stories.tsx +++ b/easy-ui-react/src/DatePicker/DatePicker.stories.tsx @@ -1,5 +1,6 @@ import React from "react"; import { Meta, StoryObj } from "@storybook/react"; +import { useLocale } from "react-aria"; import { today, getLocalTimeZone, @@ -9,7 +10,6 @@ import { import { DateValue, MappedDateValue } from "@react-types/calendar"; import { InputDecorator } from "../utilities/storybook"; import { DatePicker, DatePickerProps } from "./DatePicker"; -import { useLocale } from "react-aria"; type Story = StoryObj; diff --git a/easy-ui-react/src/DatePicker/DatePicker.tsx b/easy-ui-react/src/DatePicker/DatePicker.tsx index f4a77c5b5..7943fed52 100644 --- a/easy-ui-react/src/DatePicker/DatePicker.tsx +++ b/easy-ui-react/src/DatePicker/DatePicker.tsx @@ -1,14 +1,11 @@ import React, { ReactNode } from "react"; import { useDatePicker } from "react-aria"; import { useDatePickerState } from "react-stately"; +import { DateValue, MappedDateValue } from "@react-types/calendar"; import { DatePickerTrigger } from "./DatePickerTrigger"; +import { DatePickerBase } from "./DatePickerBase"; import { DatePickerOverlay } from "./DatePickerOverlay"; -import { classNames, variationName } from "../utilities/css"; import { Calendar } from "../Calendar"; -import { Text } from "../Text"; -import styles from "./DatePicker.module.scss"; -import { DateValue, MappedDateValue } from "@react-types/calendar"; -import { logWarningForMissingAriaLabel } from "../InputField/utilities"; export type DatePickerProps = { /** @@ -73,7 +70,6 @@ export function DatePicker(props: DatePickerProps) { "aria-label": ariaLabel, } = props; const datePickerRef = React.useRef(null); - const triggerRef = React.useRef(null); const state = useDatePickerState(props); const { groupProps, @@ -84,10 +80,7 @@ export function DatePicker(props: DatePickerProps) { calendarProps, } = useDatePicker(props, state, datePickerRef); - logWarningForMissingAriaLabel(label, ariaLabel); - const triggerProps = { - triggerRef, datePickerRef, buttonProps, groupProps, @@ -95,31 +88,22 @@ export function DatePicker(props: DatePickerProps) { isDisabled, size, isInvalid, - errorMessage, - state, + errorMessage: errorMessage || calendarProps.errorMessage, }; - const overlayProps = { state, triggerRef, dialogProps }; - const className = classNames( - styles.DatePicker, - size && styles[variationName("datePicker", size)], - ); + const overlayProps = { dialogProps }; + return ( -
- {label && ( - - {label} - - )} - - - {/** Set Calendar to always valid to prevent displaying error message under Calendar */} - - -
+ + {/** When DatePicker is invalid, error message display under both DatePicker and Calendar. Set calendar to valid prevent error message displaying twice */} + + ); } diff --git a/easy-ui-react/src/DatePicker/DatePickerBase.tsx b/easy-ui-react/src/DatePicker/DatePickerBase.tsx new file mode 100644 index 000000000..eb2becc93 --- /dev/null +++ b/easy-ui-react/src/DatePicker/DatePickerBase.tsx @@ -0,0 +1,92 @@ +import React, { DOMAttributes, MutableRefObject, ReactNode } from "react"; +import { + AriaButtonProps, + AriaDatePickerProps, + AriaDialogProps, +} from "react-aria"; +import { DatePickerState, DateRangePickerState } from "react-stately"; +import { DateValue } from "@react-types/calendar"; +import { FocusableElement, GroupDOMAttributes } from "@react-types/shared"; +import { DatePicker } from "./DatePicker"; +import { Text } from "../Text"; +import { logWarningForMissingAriaLabel } from "../InputField/utilities"; +import { classNames, variationName } from "../utilities/css"; +import styles from "./DatePicker.module.scss"; + +type TriggerProps = { + datePickerRef: MutableRefObject; + buttonProps: AriaButtonProps; + groupProps: GroupDOMAttributes; + startFieldProps?: AriaDatePickerProps; + endFieldProps?: AriaDatePickerProps; + fieldProps?: AriaDatePickerProps; + isDisabled?: boolean; + size?: "sm" | "md"; + isInvalid?: boolean; + errorMessage?: ReactNode; +}; + +type OverlayProps = { + dialogProps: AriaDialogProps; +}; + +export type DatePickerProps = { + /** + * Accessibility label for input field. + */ + "aria-label"?: string; + /** + * The content to display as the label. + */ + label?: string; + triggerProps: TriggerProps; + overlayProps: OverlayProps; + labelProps: DOMAttributes; + children: ReactNode; + state: DatePickerState | DateRangePickerState; +}; +export function DatePickerBase(props: DatePickerProps) { + const { + label, + "aria-label": ariaLabel, + triggerProps, + overlayProps, + labelProps, + children, + state, + } = props; + const { size } = triggerProps; + const triggerRef = React.useRef(null); + + logWarningForMissingAriaLabel(label, ariaLabel); + + const className = classNames( + styles.DatePicker, + size && styles[variationName("datePicker", size)], + ); + return ( +
+ {label && ( + + {label} + + )} + + + {children} + +
+ ); +} diff --git a/easy-ui-react/src/DatePicker/DatePickerOverlay.tsx b/easy-ui-react/src/DatePicker/DatePickerOverlay.tsx index 0cea90d70..a65e13155 100644 --- a/easy-ui-react/src/DatePicker/DatePickerOverlay.tsx +++ b/easy-ui-react/src/DatePicker/DatePickerOverlay.tsx @@ -5,14 +5,13 @@ import { Overlay, usePopover, } from "react-aria"; - +import { DatePickerState, DateRangePickerState } from "react-stately"; import { DEFAULT_PLACEMENT, OVERLAY_OFFSET, OVERLAY_PADDING_FROM_CONTAINER, } from "../Menu/utilities"; import styles from "./DatePicker.module.scss"; -import { DatePickerState, DateRangePickerState } from "react-stately"; type DatePickerOverlayProps = { children: ReactNode; diff --git a/easy-ui-react/src/DatePicker/DatePickerTrigger.tsx b/easy-ui-react/src/DatePicker/DatePickerTrigger.tsx index 0442236fa..c44f0496e 100644 --- a/easy-ui-react/src/DatePicker/DatePickerTrigger.tsx +++ b/easy-ui-react/src/DatePicker/DatePickerTrigger.tsx @@ -2,14 +2,14 @@ import React, { MutableRefObject, ReactNode } from "react"; import CalendarMonth from "@easypost/easy-ui-icons/CalendarMonth"; import { GroupDOMAttributes } from "@react-types/shared"; import { AriaDatePickerProps, DateValue, AriaButtonProps } from "react-aria"; +import { DatePickerState, DateRangePickerState } from "react-stately"; import { InputIcon } from "../InputField/InputIcon"; import { UnstyledButton } from "../UnstyledButton"; import { DateFieldField } from "./DateField"; import { VerticalStack } from "../VerticalStack"; import { Text } from "../Text"; -import styles from "./DatePicker.module.scss"; import { classNames } from "../utilities/css"; -import { DatePickerState, DateRangePickerState } from "react-stately"; +import styles from "./DatePicker.module.scss"; export type DatePickerTriggerProps = { isDisabled?: boolean; diff --git a/easy-ui-react/src/DateRangePicker/DateRangePicker.tsx b/easy-ui-react/src/DateRangePicker/DateRangePicker.tsx index efc9439dc..cad14f8fd 100644 --- a/easy-ui-react/src/DateRangePicker/DateRangePicker.tsx +++ b/easy-ui-react/src/DateRangePicker/DateRangePicker.tsx @@ -3,12 +3,8 @@ import { useDateRangePicker } from "react-aria"; import { useDateRangePickerState } from "react-stately"; import { RangeValue } from "@react-types/shared"; import { DateValue, MappedDateValue } from "@react-types/calendar"; -import { DatePicker } from "../DatePicker"; -import { Text } from "../Text"; import { RangeCalendar } from "../RangeCalendar"; -import { classNames, variationName } from "../utilities/css"; -import styles from "../DatePicker/DatePicker.module.scss"; -import { logWarningForMissingAriaLabel } from "../InputField/utilities"; +import { DatePickerBase } from "../DatePicker/DatePickerBase"; export type DateRangePickerProps = { /** @@ -73,7 +69,6 @@ export function DateRangePicker(props: DateRangePickerProps) { "aria-label": ariaLabel, } = props; const datePickerRef = React.useRef(null); - const triggerRef = React.useRef(null); const state = useDateRangePickerState(props); const { groupProps, @@ -85,10 +80,7 @@ export function DateRangePicker(props: DateRangePickerProps) { calendarProps, } = useDateRangePicker(props, state, datePickerRef); - logWarningForMissingAriaLabel(label, ariaLabel); - const triggerProps = { - triggerRef, datePickerRef, buttonProps, groupProps, @@ -98,30 +90,20 @@ export function DateRangePicker(props: DateRangePickerProps) { size, isInvalid, errorMessage: errorMessage || calendarProps.errorMessage, - state, }; - const overlayProps = { state, triggerRef, dialogProps }; + const overlayProps = { dialogProps }; - const className = classNames( - styles.DatePicker, - size && styles[variationName("datePicker", size)], - ); return ( -
- {label && ( - - {label} - - )} - - - {/** Set Calendar to always valid to prevent displaying error message under Calendar */} - - -
+ + {/** When DatePicker is invalid, error message display under both DatePicker and Calendar. Set calendar to valid prevent error message displaying twice */} + + ); } From 0797a19c0789fad46856d682147b94963d618348 Mon Sep 17 00:00:00 2001 From: Kevin Liu Date: Thu, 19 Dec 2024 11:34:47 -0800 Subject: [PATCH 06/13] update spec and add changeset --- .changeset/popular-mayflies-rescue.md | 5 + documentation/specs/DatePicker.md | 204 +++++++++++++++--- .../src/Calendar/CalendarCell.module.scss | 2 +- easy-ui-react/src/DatePicker/DatePicker.mdx | 4 +- easy-ui-react/src/DatePicker/DatePicker.tsx | 6 - .../src/DatePicker/DatePickerBase.tsx | 9 +- .../src/DateRangePicker/DateRangePicker.mdx | 4 +- 7 files changed, 187 insertions(+), 47 deletions(-) create mode 100644 .changeset/popular-mayflies-rescue.md diff --git a/.changeset/popular-mayflies-rescue.md b/.changeset/popular-mayflies-rescue.md new file mode 100644 index 000000000..ed94a5c1e --- /dev/null +++ b/.changeset/popular-mayflies-rescue.md @@ -0,0 +1,5 @@ +--- +"@easypost/easy-ui": minor +--- + +feat: Create DatePicker and DaterangePicker diff --git a/documentation/specs/DatePicker.md b/documentation/specs/DatePicker.md index 26c026e5d..2db01e71a 100644 --- a/documentation/specs/DatePicker.md +++ b/documentation/specs/DatePicker.md @@ -11,6 +11,7 @@ DatePicker combine a date field and a calendar popver to allow users to enter or ### Features - Supports setting dates availability +- Support minimum and maximum allowed dates - Supports being controlled ### Prior Art @@ -23,56 +24,122 @@ DatePicker combine a date field and a calendar popver to allow users to enter or ## Design -`DatePicker` will use `useDatePicker` and `useDateRangePicker` from `React Aria` to helps achieve accessible date picker. +The `DatePicker` will utilize `useDatePicker` and `useDateRangePicker` from `React Aria` to ensure an accessible date picker experience. -A `DatePicker` composes several other components to product a composite element that can be used to enter dates with keyboard, or select them on a calendar. The component consist of `DatePicker` wrapper, `DatePicker.Trigger` to open a `DatePicker.Overlay` containing a `Calendar` and `DateField` for selecting and inputing dates. +The component includes a `DatePickerBase` that determines whether it's a `DatePicker` or a `DateRangePicker`, depending on the state is passed into. + +`DatePickerTiger` features a `DateField` that enables users to input dates, along with a calendar icon that opens the `DatePickerOverlay`, allowing users to select dates from the calendar component. ### API ```ts -type DatePickerProps = { +export type DatePickerProps = { + /** + * Accessibility label for input field. + */ + "aria-label"?: string; /** * The content to display as the label. */ - label: ReactNode; + label?: string; + /** + * The default value (uncontrolled). + */ + defaultValue?: DateValue | null; + /** + * The current value (controlled). + */ + value?: DateValue | null; + /** + * Handler that is called when the value changes. + */ + onChange?: (value: MappedDateValue | null) => void; + // onChange?: (value: DateValue | null) => void; /** * The minimum allowed date that a user may select. */ - minValue: DateValue; + minValue?: DateValue; /** * The maximum allowed date that a user may select. */ - maxValue: DateValue; + maxValue?: DateValue; /** - * Callback that is called for each date of the calendar. If it returns - * true, then the date is unavailable. + * Whether the input is disabled. */ - isDateUnavailable: (date: DateValue) => boolean; + isDisabled?: boolean; /** - * A placeholder date that influences the format of the placeholder shown - * when no value is selected. Defaults to today's date at midnight. + * Whether the input value is invalid. */ - placeholderValue: DateValue; + isInvalid?: boolean; /** - * Whether the input is disabled. + * An error message to display when the selected value is invalid. + */ + errorMessage?: ReactNode; + /** + * Callback that is called for each date of the calendar. If + * it returns true, then the date is unavailable. + */ + isDateUnavailable?: (date: DateValue) => boolean; + /** + * The size of the DatePicker. + * @default md + */ + size?: "sm" | "md"; +}; +``` + +```ts +export type DateRangePickerProps = { + /** + * Accessibility label for input field. + */ + "aria-label"?: string; + /** + * The content to display as the label. + */ + label?: string; + /** + * The default value (uncontrolled). + */ + defaultValue?: RangeValue | null; + /** + * The current value (controlled). + */ + value?: RangeValue | null; + /** + * Handler that is called when the value changes. + */ + onChange?: (value: RangeValue> | null) => void; + /** + * The minimum allowed date that a user may select. */ - isDisabled: boolean; + minValue?: DateValue; /** - * Whether the input can be selected but not changed by the user. + * The maximum allowed date that a user may select. */ - isReadOnly: boolean; + maxValue?: DateValue; /** - * Whether user input is required on the input before form submission. + * Whether the input is disabled. */ - isRequired: boolean; + isDisabled?: boolean; /** * Whether the input value is invalid. */ - isInvalid: boolean; + isInvalid?: boolean; /** - * An error message for the field. + * An error message to display when the selected value is invalid. */ - errorMessage: ReactNode; + errorMessage?: ReactNode; + /** + * Callback that is called for each date of the calendar. If + * it returns true, then the date is unavailable. + */ + isDateUnavailable?: (date: DateValue) => boolean; + /** + * The size of the DateRangePicker. + * @default md + */ + size?: "sm" | "md"; }; ``` @@ -82,20 +149,93 @@ _Standalone_: ```tsx import { DatePicker } from "@easypost/easy-ui/DatePicker"; -import { Calendar } from "@easypost/easy-ui/Calendar"; -import { DateField } from "@easypost/easy-ui/DateField"; function PageWithDatePicker() { + return ; +} +``` + +_Default value:_ + +```tsx +import { DatePicker } from "@easypost/easy-ui/DatePicker"; + +function PageWithDatePicker() { + return ; +} +``` + +_Disabled:_ + +```tsx +import { DatePicker } from "@easypost/easy-ui/DatePicker"; + +function PageWithDatePicker() { + return ; +} +``` + +_Minimum and maximum allowed dates:_ + +```tsx +import { DatePicker } from "@easypost/easy-ui/DatePicker"; + +function PageWithDatePicker() { + return ( + + ); +} +``` + +_Dates availabilty:_ + +```tsx +import { DatePicker } from "@easypost/easy-ui/DatePicker"; + +function PageWithDatePicker() { + return ( + today(getLocalTimeZone()).compare(date) > 0} + /> + ); +} +``` + +_Controlled:_ + +```tsx +import { DatePicker } from "@easypost/easy-ui/DatePicker"; + +function PageWithDatePicker() { + const [date, setDate] = React.useState | null>( + null, + ); + + return ; +} +``` + +_Invalid:_ + +```tsx +import { DatePicker } from "@easypost/easy-ui/DatePicker"; + +function PageWithDatePicker() { + const { locale } = useLocale(); + const [date, setDate] = React.useState | null>( + endOfWeek(today(getLocalTimeZone()), locale), + ); + return ( - - - - - - - - - + ); } ``` diff --git a/easy-ui-react/src/Calendar/CalendarCell.module.scss b/easy-ui-react/src/Calendar/CalendarCell.module.scss index 2e0c8b98d..92627a101 100644 --- a/easy-ui-react/src/Calendar/CalendarCell.module.scss +++ b/easy-ui-react/src/Calendar/CalendarCell.module.scss @@ -22,7 +22,7 @@ } .CellContainer { - padding: design-token("space.0-5") design-token("space.0-5"); + padding: design-token("space.0-5"); .rangeSelectionStart:before { @include rangeSelection; } diff --git a/easy-ui-react/src/DatePicker/DatePicker.mdx b/easy-ui-react/src/DatePicker/DatePicker.mdx index 950e35c47..f301c6009 100644 --- a/easy-ui-react/src/DatePicker/DatePicker.mdx +++ b/easy-ui-react/src/DatePicker/DatePicker.mdx @@ -37,9 +37,9 @@ Callback that is called for each date of the calendar. If it returns `true`, the -## Disabled Calendar +## Disabled DatePicker -Use `isDisabled` to disabled the calendar entirely. +Use `isDisabled` to disabled the DatePicker entirely. diff --git a/easy-ui-react/src/DatePicker/DatePicker.tsx b/easy-ui-react/src/DatePicker/DatePicker.tsx index 7943fed52..1d63688b7 100644 --- a/easy-ui-react/src/DatePicker/DatePicker.tsx +++ b/easy-ui-react/src/DatePicker/DatePicker.tsx @@ -2,9 +2,7 @@ import React, { ReactNode } from "react"; import { useDatePicker } from "react-aria"; import { useDatePickerState } from "react-stately"; import { DateValue, MappedDateValue } from "@react-types/calendar"; -import { DatePickerTrigger } from "./DatePickerTrigger"; import { DatePickerBase } from "./DatePickerBase"; -import { DatePickerOverlay } from "./DatePickerOverlay"; import { Calendar } from "../Calendar"; export type DatePickerProps = { @@ -108,7 +106,3 @@ export function DatePicker(props: DatePickerProps) { } DatePicker.displayName = "DatePicker"; - -DatePicker.Trigger = DatePickerTrigger; - -DatePicker.Overlay = DatePickerOverlay; diff --git a/easy-ui-react/src/DatePicker/DatePickerBase.tsx b/easy-ui-react/src/DatePicker/DatePickerBase.tsx index eb2becc93..c11f04a30 100644 --- a/easy-ui-react/src/DatePicker/DatePickerBase.tsx +++ b/easy-ui-react/src/DatePicker/DatePickerBase.tsx @@ -7,7 +7,8 @@ import { import { DatePickerState, DateRangePickerState } from "react-stately"; import { DateValue } from "@react-types/calendar"; import { FocusableElement, GroupDOMAttributes } from "@react-types/shared"; -import { DatePicker } from "./DatePicker"; +import { DatePickerTrigger } from "./DatePickerTrigger"; +import { DatePickerOverlay } from "./DatePickerOverlay"; import { Text } from "../Text"; import { logWarningForMissingAriaLabel } from "../InputField/utilities"; import { classNames, variationName } from "../utilities/css"; @@ -75,18 +76,18 @@ export function DatePickerBase(props: DatePickerProps) { {label}
)} - - {children} - +
); } diff --git a/easy-ui-react/src/DateRangePicker/DateRangePicker.mdx b/easy-ui-react/src/DateRangePicker/DateRangePicker.mdx index b18e28fef..7beba22f2 100644 --- a/easy-ui-react/src/DateRangePicker/DateRangePicker.mdx +++ b/easy-ui-react/src/DateRangePicker/DateRangePicker.mdx @@ -37,9 +37,9 @@ Callback that is called for each date of the calendar. If it returns `true`, the -## Disabled Calendar +## Disabled DateRangePicker -Use `isDisabled` to disabled the calendar entirely. +Use `isDisabled` to disabled the DateRangePicker entirely. From 794a46798defa84be10be0fcab3791ac915e8193 Mon Sep 17 00:00:00 2001 From: Kevin Liu Date: Thu, 19 Dec 2024 11:38:26 -0800 Subject: [PATCH 07/13] remove comments --- easy-ui-react/src/DatePicker/DatePickerBase.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/easy-ui-react/src/DatePicker/DatePickerBase.tsx b/easy-ui-react/src/DatePicker/DatePickerBase.tsx index c11f04a30..afe793b0b 100644 --- a/easy-ui-react/src/DatePicker/DatePickerBase.tsx +++ b/easy-ui-react/src/DatePicker/DatePickerBase.tsx @@ -32,13 +32,7 @@ type OverlayProps = { }; export type DatePickerProps = { - /** - * Accessibility label for input field. - */ "aria-label"?: string; - /** - * The content to display as the label. - */ label?: string; triggerProps: TriggerProps; overlayProps: OverlayProps; From 8b1820c6826f053f2d3920c68a9bd81264cec2f5 Mon Sep 17 00:00:00 2001 From: Kevin Liu Date: Thu, 19 Dec 2024 11:51:32 -0800 Subject: [PATCH 08/13] tsdoc --- easy-ui-react/src/DatePicker/DatePicker.mdx | 2 +- .../src/DatePicker/DatePicker.stories.tsx | 2 +- easy-ui-react/src/DatePicker/DatePicker.tsx | 33 +++++++++++++++++++ .../src/DateRangePicker/DateRangePicker.mdx | 2 +- .../DateRangePicker.stories.tsx | 2 +- .../src/DateRangePicker/DateRangePicker.tsx | 32 ++++++++++++++++++ 6 files changed, 69 insertions(+), 4 deletions(-) diff --git a/easy-ui-react/src/DatePicker/DatePicker.mdx b/easy-ui-react/src/DatePicker/DatePicker.mdx index f301c6009..ac7c4a4e0 100644 --- a/easy-ui-react/src/DatePicker/DatePicker.mdx +++ b/easy-ui-react/src/DatePicker/DatePicker.mdx @@ -9,7 +9,7 @@ import * as DatePickerStories from "./DatePicker.stories"; DatePicker combine a DateField and a Calendar popover to allow users to enter or select a date. - + ## Default value diff --git a/easy-ui-react/src/DatePicker/DatePicker.stories.tsx b/easy-ui-react/src/DatePicker/DatePicker.stories.tsx index 9922cca6c..ea580e189 100644 --- a/easy-ui-react/src/DatePicker/DatePicker.stories.tsx +++ b/easy-ui-react/src/DatePicker/DatePicker.stories.tsx @@ -24,7 +24,7 @@ export default meta; const Template = (args: DatePickerProps) => ; -export const Standard: Story = { +export const Standalone: Story = { render: Template.bind({}), }; diff --git a/easy-ui-react/src/DatePicker/DatePicker.tsx b/easy-ui-react/src/DatePicker/DatePicker.tsx index 1d63688b7..fb919e85d 100644 --- a/easy-ui-react/src/DatePicker/DatePicker.tsx +++ b/easy-ui-react/src/DatePicker/DatePicker.tsx @@ -58,6 +58,39 @@ export type DatePickerProps = { */ size?: "sm" | "md"; }; + +/** + * A `DatePicker` has a `DateField` and a calendar popover to + * allow users to enter or select a date. + * + * @remarks + * Use a DatePciker when you want to provide a view that allows + * the users to select a date. + * + * @example + * _Standalone:_ + * ```tsx + * import { DatePicker } from "@easypost/easy-ui/DatePicker"; + * + * function PageWithDatePicker() { + * return ; + * } + * ``` + * + * @example + * _Controlled:_ + * ```tsx + * import { DatePicker } from "@easypost/easy-ui/DatePicker"; + * + * function PageWithDatePicker() { + * const [date, setDate] = React.useState(null); + * return ( + * + * ); + * } + * ``` + */ export function DatePicker(props: DatePickerProps) { const { label, diff --git a/easy-ui-react/src/DateRangePicker/DateRangePicker.mdx b/easy-ui-react/src/DateRangePicker/DateRangePicker.mdx index 7beba22f2..8de4a9d2d 100644 --- a/easy-ui-react/src/DateRangePicker/DateRangePicker.mdx +++ b/easy-ui-react/src/DateRangePicker/DateRangePicker.mdx @@ -9,7 +9,7 @@ import * as DateRangePickerStories from "./DateRangePicker.stories"; DateRangePicker combine DateFields and a Calendar popover to allow users to enter or select a range of dates. - + ## Default values diff --git a/easy-ui-react/src/DateRangePicker/DateRangePicker.stories.tsx b/easy-ui-react/src/DateRangePicker/DateRangePicker.stories.tsx index 5d3a3e509..3ddf04fb7 100644 --- a/easy-ui-react/src/DateRangePicker/DateRangePicker.stories.tsx +++ b/easy-ui-react/src/DateRangePicker/DateRangePicker.stories.tsx @@ -18,7 +18,7 @@ export default meta; const Template = (args: DateRangePickerProps) => ; -export const Standard: Story = { +export const Standalone: Story = { render: Template.bind({}), }; diff --git a/easy-ui-react/src/DateRangePicker/DateRangePicker.tsx b/easy-ui-react/src/DateRangePicker/DateRangePicker.tsx index cad14f8fd..0908a9f42 100644 --- a/easy-ui-react/src/DateRangePicker/DateRangePicker.tsx +++ b/easy-ui-react/src/DateRangePicker/DateRangePicker.tsx @@ -59,6 +59,38 @@ export type DateRangePickerProps = { size?: "sm" | "md"; }; +/** + * A `DateRangePicker` has a `DateField` and a calendar popover + * to allow users to enter or select a date. + * + * @remarks + * Use a DateRangePicker when you want to provide a view that + * allows the users to select a date. + * + * @example + * _Standalone:_ + * ```tsx + * import { DateRangePicker } from "@easypost/easy-ui/DateRangePicker"; + * + * function PageWithDateRangePicker() { + * return ; + * } + * ``` + * + * @example + * _Controlled:_ + * ```tsx + * import { DateRangePicker } from "@easypost/easy-ui/DateRangePicker"; + * + * function PageWithDateRangePicker() { + * const [date, setDate] = React.useState(null); + * return ( + * + * ); + * } + * ``` + */ export function DateRangePicker(props: DateRangePickerProps) { const { label, From 6b3db430f52ede32efbb830325c7a2b0aeb08948 Mon Sep 17 00:00:00 2001 From: Kevin Liu Date: Fri, 20 Dec 2024 10:10:11 -0800 Subject: [PATCH 09/13] address feedback --- easy-ui-react/src/Calendar/CalendarGrid.module.scss | 2 -- easy-ui-react/src/DatePicker/DatePicker.module.scss | 9 ++++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/easy-ui-react/src/Calendar/CalendarGrid.module.scss b/easy-ui-react/src/Calendar/CalendarGrid.module.scss index bc8cdfd33..1c6f73349 100644 --- a/easy-ui-react/src/Calendar/CalendarGrid.module.scss +++ b/easy-ui-react/src/Calendar/CalendarGrid.module.scss @@ -1,8 +1,6 @@ @use "../styles/common" as *; .CalendarGrid { border-collapse: collapse; - z-index: 999; - position: relative; } .CalendarGridHeader { background-color: design-token("color.primary.700"); diff --git a/easy-ui-react/src/DatePicker/DatePicker.module.scss b/easy-ui-react/src/DatePicker/DatePicker.module.scss index a27e44d46..b585bf558 100644 --- a/easy-ui-react/src/DatePicker/DatePicker.module.scss +++ b/easy-ui-react/src/DatePicker/DatePicker.module.scss @@ -41,10 +41,13 @@ box-shadow: design-token("shadow.modal"); pointer-events: auto; outline: none; + position: relative; + z-index: design-token('z-index.input_icon'); } .DateSegment { - padding: design-token("space.0-5"); + @include font-style("body1"); + padding: 0 design-token("space.0-5"); color: design-token("color.primary.800"); &[data-placeholder="true"], &.literalSegment { @@ -57,3 +60,7 @@ color: design-token("color.neutral.000"); } } + +.datePickerSm .DateSegment { + @include font-style("body2"); +} From 151c5cbe26df446c13d1d2a6c6715137617a0423 Mon Sep 17 00:00:00 2001 From: Kevin Liu Date: Fri, 20 Dec 2024 10:30:38 -0800 Subject: [PATCH 10/13] fix lint --- easy-ui-react/src/DatePicker/DatePicker.module.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/easy-ui-react/src/DatePicker/DatePicker.module.scss b/easy-ui-react/src/DatePicker/DatePicker.module.scss index b585bf558..c98f4ec09 100644 --- a/easy-ui-react/src/DatePicker/DatePicker.module.scss +++ b/easy-ui-react/src/DatePicker/DatePicker.module.scss @@ -42,7 +42,7 @@ pointer-events: auto; outline: none; position: relative; - z-index: design-token('z-index.input_icon'); + z-index: design-token("z-index.input_icon"); } .DateSegment { From 8e90335c7cfea54f96c21eb50c05a669c7ccaa5c Mon Sep 17 00:00:00 2001 From: Kevin Liu Date: Fri, 20 Dec 2024 11:16:33 -0800 Subject: [PATCH 11/13] vertical align icon --- easy-ui-react/src/DatePicker/DatePicker.module.scss | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/easy-ui-react/src/DatePicker/DatePicker.module.scss b/easy-ui-react/src/DatePicker/DatePicker.module.scss index c98f4ec09..9129d0d92 100644 --- a/easy-ui-react/src/DatePicker/DatePicker.module.scss +++ b/easy-ui-react/src/DatePicker/DatePicker.module.scss @@ -26,6 +26,14 @@ @include Input.iconEndInput; } +.datePickerTrigger > :last-child { + top: calc(design-token("space.1-5") - design-token("shape.border_width.1")); +} + +.datePickerSm .datePickerTrigger > :last-child { + top: calc(design-token("space.1") - design-token("shape.border_width.1")); +} + .errorInput { @include Input.error; } @@ -42,7 +50,7 @@ pointer-events: auto; outline: none; position: relative; - z-index: design-token("z-index.input_icon"); + z-index: 1; } .DateSegment { From 6a4fcf7f747dc26c7a07e34a5e14cd7950ab9b4e Mon Sep 17 00:00:00 2001 From: Kevin Liu Date: Fri, 20 Dec 2024 11:37:06 -0800 Subject: [PATCH 12/13] Update icon style --- easy-ui-react/src/DatePicker/DatePicker.module.scss | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/easy-ui-react/src/DatePicker/DatePicker.module.scss b/easy-ui-react/src/DatePicker/DatePicker.module.scss index 9129d0d92..4cdd3f5b3 100644 --- a/easy-ui-react/src/DatePicker/DatePicker.module.scss +++ b/easy-ui-react/src/DatePicker/DatePicker.module.scss @@ -27,7 +27,13 @@ } .datePickerTrigger > :last-child { + color: design-token("color.primary.500"); + cursor: pointer; top: calc(design-token("space.1-5") - design-token("shape.border_width.1")); + + &:hover { + color: design-token("color.primary.600"); + } } .datePickerSm .datePickerTrigger > :last-child { From 1c9528f9c786ce078cee92b49ca14b0c49c73078 Mon Sep 17 00:00:00 2001 From: Kevin Liu Date: Fri, 20 Dec 2024 12:27:50 -0800 Subject: [PATCH 13/13] update icon color --- easy-ui-react/src/DatePicker/DatePicker.module.scss | 11 +++++++---- easy-ui-react/src/DatePicker/DatePickerTrigger.tsx | 1 + 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/easy-ui-react/src/DatePicker/DatePicker.module.scss b/easy-ui-react/src/DatePicker/DatePicker.module.scss index 4cdd3f5b3..ad6a56bd7 100644 --- a/easy-ui-react/src/DatePicker/DatePicker.module.scss +++ b/easy-ui-react/src/DatePicker/DatePicker.module.scss @@ -26,16 +26,19 @@ @include Input.iconEndInput; } -.datePickerTrigger > :last-child { - color: design-token("color.primary.500"); +.datePickerTrigger:not(.disabled) > :last-child { + color: design-token("color.neutral.700"); cursor: pointer; - top: calc(design-token("space.1-5") - design-token("shape.border_width.1")); &:hover { - color: design-token("color.primary.600"); + color: design-token("color.neutral.800"); } } +.datePickerTrigger > :last-child { + top: calc(design-token("space.1-5") - design-token("shape.border_width.1")); +} + .datePickerSm .datePickerTrigger > :last-child { top: calc(design-token("space.1") - design-token("shape.border_width.1")); } diff --git a/easy-ui-react/src/DatePicker/DatePickerTrigger.tsx b/easy-ui-react/src/DatePicker/DatePickerTrigger.tsx index c44f0496e..1770f0853 100644 --- a/easy-ui-react/src/DatePicker/DatePickerTrigger.tsx +++ b/easy-ui-react/src/DatePicker/DatePickerTrigger.tsx @@ -45,6 +45,7 @@ export function DatePickerTrigger(props: DatePickerTriggerProps) { const className = classNames( styles.datePickerTrigger, (isInvalid || state.isInvalid) && styles.errorInput, + isDisabled && styles.disabled, ); return (