From 0564641a3feaeb504d5bb54670aa7a257085d137 Mon Sep 17 00:00:00 2001 From: "Malik, Junaid" Date: Mon, 8 Jan 2024 11:08:41 +0800 Subject: [PATCH] #1074 add DatePicker & DateRangePicker (first-cut) - first-cut since we still need enhancements along the way around theme, handling closing calendar on click away, better focus management, etc. --- .../src/calendar/useSelection.ts | 2 +- .../src/date-picker/DatePicker.css | 23 ++++ .../src/date-picker/DatePicker.tsx | 107 ++++++++++++++++++ .../src/date-picker/DateRangePicker.tsx | 99 ++++++++++++++++ .../vuu-ui-controls/src/date-picker/index.ts | 2 + .../src/date-picker/input/DatePickerInput.css | 13 +++ .../src/date-picker/input/DatePickerInput.tsx | 36 ++++++ .../input/DateRangePickerInput.css | 24 ++++ .../input/DateRangePickerInput.tsx | 35 ++++++ .../src/date-picker/input/types.ts | 10 ++ .../internal/CalendarIconButton.tsx | 14 +++ vuu-ui/packages/vuu-ui-controls/src/index.ts | 1 + .../UiControls/DatePicker.examples.tsx | 42 +++++++ .../showcase/src/examples/UiControls/index.ts | 1 + 14 files changed, 408 insertions(+), 1 deletion(-) create mode 100644 vuu-ui/packages/vuu-ui-controls/src/date-picker/DatePicker.css create mode 100644 vuu-ui/packages/vuu-ui-controls/src/date-picker/DatePicker.tsx create mode 100644 vuu-ui/packages/vuu-ui-controls/src/date-picker/DateRangePicker.tsx create mode 100644 vuu-ui/packages/vuu-ui-controls/src/date-picker/index.ts create mode 100644 vuu-ui/packages/vuu-ui-controls/src/date-picker/input/DatePickerInput.css create mode 100644 vuu-ui/packages/vuu-ui-controls/src/date-picker/input/DatePickerInput.tsx create mode 100644 vuu-ui/packages/vuu-ui-controls/src/date-picker/input/DateRangePickerInput.css create mode 100644 vuu-ui/packages/vuu-ui-controls/src/date-picker/input/DateRangePickerInput.tsx create mode 100644 vuu-ui/packages/vuu-ui-controls/src/date-picker/input/types.ts create mode 100644 vuu-ui/packages/vuu-ui-controls/src/date-picker/internal/CalendarIconButton.tsx create mode 100644 vuu-ui/showcase/src/examples/UiControls/DatePicker.examples.tsx diff --git a/vuu-ui/packages/vuu-ui-controls/src/calendar/useSelection.ts b/vuu-ui/packages/vuu-ui-controls/src/calendar/useSelection.ts index 06c43b35e3..9c5f9b0945 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/calendar/useSelection.ts +++ b/vuu-ui/packages/vuu-ui-controls/src/calendar/useSelection.ts @@ -22,7 +22,7 @@ interface BaseUseSelectionCalendarProps { type SingleSelectionValueType = DateValue; type MultiSelectionValueType = DateValue[]; -type RangeSelectionValueType = { +export type RangeSelectionValueType = { startDate?: DateValue; endDate?: DateValue; }; diff --git a/vuu-ui/packages/vuu-ui-controls/src/date-picker/DatePicker.css b/vuu-ui/packages/vuu-ui-controls/src/date-picker/DatePicker.css new file mode 100644 index 0000000000..f350c70ca9 --- /dev/null +++ b/vuu-ui/packages/vuu-ui-controls/src/date-picker/DatePicker.css @@ -0,0 +1,23 @@ +.vuuPortal { + z-index: calc(var(--salt-zIndex-flyover) + 1); +} + +.saltIcon { + display: inline-block; +} + +.vuuDatePicker { + display: flex; + flex-direction: row; + justify-content: space-between; + gap: 2px; + padding: 0 2px; +} + +.vuuDatePicker > button { + padding: 3px; +} + +.vuuDatePickerTooltip { + padding: 0; +} diff --git a/vuu-ui/packages/vuu-ui-controls/src/date-picker/DatePicker.tsx b/vuu-ui/packages/vuu-ui-controls/src/date-picker/DatePicker.tsx new file mode 100644 index 0000000000..a012a8ae33 --- /dev/null +++ b/vuu-ui/packages/vuu-ui-controls/src/date-picker/DatePicker.tsx @@ -0,0 +1,107 @@ +import { useCallback, useMemo, useState } from "react"; +import { DateValue, today, getLocalTimeZone } from "@internationalized/date"; +import { clsx } from "clsx"; +import { Tooltip } from "@salt-ds/core"; +import { Calendar, CalendarProps } from "../calendar/Calendar"; +import { DatePickerInput } from "./input/DatePickerInput"; +import { PickerSelectionType } from "./input/types"; +import { CalendarIconButton } from "./internal/CalendarIconButton"; + +import "./DatePicker.css"; + +const baseClass = "saltInput saltInput-primary vuuDatePicker"; + +export interface BaseDatePickerProps + extends Pick< + CalendarProps, + | "hideOutOfRangeDates" + | "isDayUnselectable" + | "minDate" + | "maxDate" + | "hideYearDropdown" + > { + onSelectedDateChange: (selected: T) => void; + selectedDate: T | undefined; + closeOnSelection?: boolean; + className?: string; +} + +export const DatePicker = (props: BaseDatePickerProps) => { + const { selectedDate, onSelectedDateChange, className } = props; + const [visibleMonth, setVisibleMonth] = useState( + selectedDate + ); + + const handleInputChange = useCallback( + (d: DateValue) => { + onSelectedDateChange(d); + setVisibleMonth(d); + }, + [onSelectedDateChange] + ); + + return ( +
+ + +
+ ); +}; + +interface DatePickerTooltipProps extends BaseDatePickerProps { + visibleMonth: DateValue | undefined; + onVisibleMonthChange: (d: DateValue) => void; +} + +const DatePickerTooltip = (props: DatePickerTooltipProps) => { + const { + closeOnSelection, + onSelectedDateChange, + onVisibleMonthChange, + ...rest + } = props; + const [isOpen, setIsOpen] = useState(false); + + const handleDateSelection = useCallback( + (_: React.SyntheticEvent, d: DateValue) => { + onSelectedDateChange(d); + if (closeOnSelection) setIsOpen(false); + }, + [onSelectedDateChange, closeOnSelection] + ); + + const handleVisibleMonthChange = useCallback( + (_: React.SyntheticEvent, d: DateValue) => { + onVisibleMonthChange(d); + }, + [onVisibleMonthChange] + ); + + const defaultSelectedDate = useMemo(() => today(getLocalTimeZone()), []); + + return ( + + } + className="vuuDatePickerTooltip" + disableHoverListener + hideArrow + hideIcon + placement="bottom-end" + open={isOpen} + > + setIsOpen((o) => !o)} /> + + ); +}; diff --git a/vuu-ui/packages/vuu-ui-controls/src/date-picker/DateRangePicker.tsx b/vuu-ui/packages/vuu-ui-controls/src/date-picker/DateRangePicker.tsx new file mode 100644 index 0000000000..babf266eea --- /dev/null +++ b/vuu-ui/packages/vuu-ui-controls/src/date-picker/DateRangePicker.tsx @@ -0,0 +1,99 @@ +import { Tooltip } from "@salt-ds/core"; +import { useCallback, useMemo, useState } from "react"; +import { Calendar } from "../calendar/Calendar"; +import { DateValue, today, getLocalTimeZone } from "@internationalized/date"; +import { clsx } from "clsx"; +import { DateRangePickerInput } from "./input/DateRangePickerInput"; +import { BaseDatePickerProps } from "./DatePicker"; +import { RangeSelectionValueType } from "../calendar"; +import { CalendarIconButton } from "./internal/CalendarIconButton"; + +import "./DatePicker.css"; + +const baseClass = "saltInput saltInput-primary vuuDatePicker"; + +export const DateRangePicker = ( + props: BaseDatePickerProps +) => { + const { selectedDate, onSelectedDateChange, className } = props; + const [visibleMonth, setVisibleMonth] = useState( + selectedDate?.startDate + ); + + const handleInputChange = useCallback( + (r: RangeSelectionValueType) => { + onSelectedDateChange(r); + setVisibleMonth(r.endDate ?? r.startDate); + }, + [onSelectedDateChange] + ); + + return ( +
+ + +
+ ); +}; + +interface DateRangePickerTooltipProps + extends BaseDatePickerProps { + visibleMonth: DateValue | undefined; + onVisibleMonthChange: (d: DateValue) => void; +} + +const DateRangePickerTooltip = (props: DateRangePickerTooltipProps) => { + const { + onVisibleMonthChange, + onSelectedDateChange, + closeOnSelection, + ...rest + } = props; + const [isOpen, setIsOpen] = useState(false); + + const handleDateSelection = useCallback( + (_: React.SyntheticEvent, r: RangeSelectionValueType) => { + onSelectedDateChange(r); + if (closeOnSelection && r.endDate) setIsOpen(false); + }, + [onSelectedDateChange, closeOnSelection] + ); + + const handleVisibleMonthChange = useCallback( + (_: React.SyntheticEvent, d: DateValue) => { + onVisibleMonthChange(d); + }, + [onVisibleMonthChange] + ); + + const defaultSelectedDate = useMemo( + () => ({ startDate: today(getLocalTimeZone()) }), + [] + ); + + return ( + + } + className="vuuDatePickerTooltip" + disableHoverListener + hideArrow + hideIcon + placement="bottom-end" + open={isOpen} + > + setIsOpen((o) => !o)} /> + + ); +}; diff --git a/vuu-ui/packages/vuu-ui-controls/src/date-picker/index.ts b/vuu-ui/packages/vuu-ui-controls/src/date-picker/index.ts new file mode 100644 index 0000000000..6b64438569 --- /dev/null +++ b/vuu-ui/packages/vuu-ui-controls/src/date-picker/index.ts @@ -0,0 +1,2 @@ +export * from "./DatePicker"; +export * from "./DateRangePicker"; diff --git a/vuu-ui/packages/vuu-ui-controls/src/date-picker/input/DatePickerInput.css b/vuu-ui/packages/vuu-ui-controls/src/date-picker/input/DatePickerInput.css new file mode 100644 index 0000000000..8b9a72c0e4 --- /dev/null +++ b/vuu-ui/packages/vuu-ui-controls/src/date-picker/input/DatePickerInput.css @@ -0,0 +1,13 @@ +.vuuDatePickerInput { + border: none; + width: 100%; + padding-left: 0; +} + +.vuuDatePickerInput:focus { + outline: none; +} + +input[type="date"]::-webkit-calendar-picker-indicator { + display: none; +} diff --git a/vuu-ui/packages/vuu-ui-controls/src/date-picker/input/DatePickerInput.tsx b/vuu-ui/packages/vuu-ui-controls/src/date-picker/input/DatePickerInput.tsx new file mode 100644 index 0000000000..aa0bb07709 --- /dev/null +++ b/vuu-ui/packages/vuu-ui-controls/src/date-picker/input/DatePickerInput.tsx @@ -0,0 +1,36 @@ +import React, { useCallback } from "react"; +import { DateValue, CalendarDate } from "@internationalized/date"; +import { clsx } from "clsx"; +import { BasePickerInputProps } from "./types"; + +import "./DatePickerInput.css"; + +const baseClass = "vuuDatePickerInput"; + +type Props = BasePickerInputProps; + +export const DatePickerInput: React.FC = (props) => { + const { value, onChange, className } = props; + + const onInputChange = useCallback>( + (e) => { + const v = e.target.value; + if (v === "") return; + else onChange(toCalendarDate(new Date(v))); + }, + [onChange] + ); + + return ( + + ); +}; + +function toCalendarDate(d: Date): CalendarDate { + return new CalendarDate(d.getFullYear(), d.getMonth() + 1, d.getDate()); +} diff --git a/vuu-ui/packages/vuu-ui-controls/src/date-picker/input/DateRangePickerInput.css b/vuu-ui/packages/vuu-ui-controls/src/date-picker/input/DateRangePickerInput.css new file mode 100644 index 0000000000..40ab52b132 --- /dev/null +++ b/vuu-ui/packages/vuu-ui-controls/src/date-picker/input/DateRangePickerInput.css @@ -0,0 +1,24 @@ +.vuuDateRangePickerInput { + display: flex; + flex-direction: row; + gap: 1px; + align-items: center; + justify-content: space-between; +} + +.vuuDateRangePickerInput > * { + width: fit-content; +} + +.vuuDateRangePickerInput input:last-child { + text-align: right; +} + +.vuuDateRangePickerInput input:last-child::before { + display: block; + text-align: center; + font-size: 12px; + margin: 0 2px; + color: black; + content: "—"; +} diff --git a/vuu-ui/packages/vuu-ui-controls/src/date-picker/input/DateRangePickerInput.tsx b/vuu-ui/packages/vuu-ui-controls/src/date-picker/input/DateRangePickerInput.tsx new file mode 100644 index 0000000000..664e1ab571 --- /dev/null +++ b/vuu-ui/packages/vuu-ui-controls/src/date-picker/input/DateRangePickerInput.tsx @@ -0,0 +1,35 @@ +import { useCallback } from "react"; +import { DateValue } from "@internationalized/date"; +import { clsx } from "clsx"; +import { DatePickerInput } from "./DatePickerInput"; +import { BasePickerInputProps } from "./types"; +import { RangeSelectionValueType } from "../../calendar"; + +import "./DateRangePickerInput.css"; + +const baseClass = "vuuDateRangePickerInput"; + +type Props = BasePickerInputProps; + +export const DateRangePickerInput: React.FC = (props) => { + const { value, onChange, className } = props; + + const getHandleInputChange = useCallback( + (k: keyof RangeSelectionValueType) => (d: DateValue) => { + onChange({ ...value, [k]: d }); + }, + [value, onChange] + ); + + return ( +
+ {(["startDate", "endDate"] as const).map((t) => ( + + ))} +
+ ); +}; diff --git a/vuu-ui/packages/vuu-ui-controls/src/date-picker/input/types.ts b/vuu-ui/packages/vuu-ui-controls/src/date-picker/input/types.ts new file mode 100644 index 0000000000..4ffe949773 --- /dev/null +++ b/vuu-ui/packages/vuu-ui-controls/src/date-picker/input/types.ts @@ -0,0 +1,10 @@ +import { DateValue } from "@internationalized/date"; +import { RangeSelectionValueType } from "../../calendar"; + +export type PickerSelectionType = DateValue | RangeSelectionValueType; + +export interface BasePickerInputProps { + onChange: (selected: T) => void; + value?: T; + className?: string; +} diff --git a/vuu-ui/packages/vuu-ui-controls/src/date-picker/internal/CalendarIconButton.tsx b/vuu-ui/packages/vuu-ui-controls/src/date-picker/internal/CalendarIconButton.tsx new file mode 100644 index 0000000000..f8b5aac141 --- /dev/null +++ b/vuu-ui/packages/vuu-ui-controls/src/date-picker/internal/CalendarIconButton.tsx @@ -0,0 +1,14 @@ +import { Button } from "@salt-ds/core"; +import { CalendarIcon } from "@salt-ds/icons"; +import { ComponentPropsWithoutRef, ForwardedRef, forwardRef } from "react"; + +export const CalendarIconButton = forwardRef(function CalendarIconButton( + props: ComponentPropsWithoutRef<"button">, + ref: ForwardedRef +) { + return ( + + ); +}); diff --git a/vuu-ui/packages/vuu-ui-controls/src/index.ts b/vuu-ui/packages/vuu-ui-controls/src/index.ts index 44f32a3b8a..5c6814c8ef 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/index.ts +++ b/vuu-ui/packages/vuu-ui-controls/src/index.ts @@ -2,6 +2,7 @@ export * from "./calendar"; export * from "./combo-box"; export * from "./common-hooks"; export * from "./cycle-state-button"; +export * from "./date-picker"; export * from "./drag-drop"; export * from "./dropdown"; export * from "./editable"; diff --git a/vuu-ui/showcase/src/examples/UiControls/DatePicker.examples.tsx b/vuu-ui/showcase/src/examples/UiControls/DatePicker.examples.tsx new file mode 100644 index 0000000000..7e483b4d8d --- /dev/null +++ b/vuu-ui/showcase/src/examples/UiControls/DatePicker.examples.tsx @@ -0,0 +1,42 @@ +import { DatePicker, DateRangePicker } from "@finos/vuu-ui-controls"; +import { CalendarDate, DateValue } from "@internationalized/date"; +import { useState } from "react"; + +let displaySequence = 1; + +export const DefaultDatePicker = () => { + const [date, setDate] = useState(new CalendarDate(2024, 1, 1)); + + return ( +
+ +
+ ); +}; +DefaultDatePicker.displaySequence = displaySequence++; + +export const DefaultDateRangePicker = () => { + const [date, setDate] = useState<{ + startDate?: DateValue; + endDate?: DateValue; + }>({ + startDate: new CalendarDate(2024, 1, 1), + }); + + return ( +
+ +
+ ); +}; +DefaultDateRangePicker.displaySequence = displaySequence++; diff --git a/vuu-ui/showcase/src/examples/UiControls/index.ts b/vuu-ui/showcase/src/examples/UiControls/index.ts index b9f05a8bc0..1c4e6ad58d 100644 --- a/vuu-ui/showcase/src/examples/UiControls/index.ts +++ b/vuu-ui/showcase/src/examples/UiControls/index.ts @@ -1,5 +1,6 @@ export * as Calendar from "./Calendar.examples"; export * as ComboBox from "./Combobox.examples"; +export * as DatePicker from "./DatePicker.examples"; export * as DragDrop from "./DragDrop.examples"; export * as Dropdown from "./Dropdown.examples"; export * as EditableLabel from "./EditableLabel.examples";