Skip to content

Commit

Permalink
#1074 add DatePicker & DateRangePicker (first-cut)
Browse files Browse the repository at this point in the history
- first-cut since we still need enhancements along the way around
  theme, handling closing calendar on click away, better focus management, etc.
  • Loading branch information
junaidzm13 committed Jan 8, 2024
1 parent 62edc24 commit 9b1ba4e
Show file tree
Hide file tree
Showing 14 changed files with 414 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ interface BaseUseSelectionCalendarProps<SelectionVariantType> {

type SingleSelectionValueType = DateValue;
type MultiSelectionValueType = DateValue[];
type RangeSelectionValueType = {
export type RangeSelectionValueType = {
startDate?: DateValue;
endDate?: DateValue;
};
Expand Down
23 changes: 23 additions & 0 deletions vuu-ui/packages/vuu-ui-controls/src/date-picker/DatePicker.css
Original file line number Diff line number Diff line change
@@ -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;
}
107 changes: 107 additions & 0 deletions vuu-ui/packages/vuu-ui-controls/src/date-picker/DatePicker.tsx
Original file line number Diff line number Diff line change
@@ -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<T extends PickerSelectionType>
extends Pick<
CalendarProps,
| "hideOutOfRangeDates"
| "isDayUnselectable"
| "minDate"
| "maxDate"
| "hideYearDropdown"
> {
onSelectedDateChange: (selected: T) => void;
selectedDate: T | undefined;
closeOnSelection?: boolean;
className?: string;
}

export const DatePicker = (props: BaseDatePickerProps<DateValue>) => {
const { selectedDate, onSelectedDateChange, className } = props;
const [visibleMonth, setVisibleMonth] = useState<DateValue | undefined>(
selectedDate
);

const handleInputChange = useCallback(
(d: DateValue) => {
onSelectedDateChange(d);
setVisibleMonth(d);
},
[onSelectedDateChange]
);

return (
<div className={clsx(baseClass, className)}>
<DatePickerInput value={selectedDate} onChange={handleInputChange} />
<DatePickerTooltip
visibleMonth={visibleMonth}
onVisibleMonthChange={setVisibleMonth}
{...props}
/>
</div>
);
};

interface DatePickerTooltipProps extends BaseDatePickerProps<DateValue> {
visibleMonth: DateValue | undefined;
onVisibleMonthChange: (d: DateValue) => void;
}

const DatePickerTooltip = (props: DatePickerTooltipProps) => {
const {
closeOnSelection,
onSelectedDateChange,
onVisibleMonthChange,
...rest
} = props;
const [isOpen, setIsOpen] = useState<boolean>(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 (
<Tooltip
content={
<Calendar
selectionVariant="default"
onVisibleMonthChange={handleVisibleMonthChange}
onSelectedDateChange={handleDateSelection}
defaultSelectedDate={defaultSelectedDate}
{...rest}
/>
}
className="vuuDatePickerTooltip"
disableHoverListener
hideArrow
hideIcon
placement="bottom-end"
open={isOpen}
>
<CalendarIconButton onClick={() => setIsOpen((o) => !o)} />
</Tooltip>
);
};
Original file line number Diff line number Diff line change
@@ -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<RangeSelectionValueType>
) => {
const { selectedDate, onSelectedDateChange, className } = props;
const [visibleMonth, setVisibleMonth] = useState<DateValue | undefined>(
selectedDate?.startDate
);

const handleInputChange = useCallback(
(r: RangeSelectionValueType) => {
onSelectedDateChange(r);
setVisibleMonth(r.endDate ?? r.startDate);
},
[onSelectedDateChange]
);

return (
<div className={clsx(baseClass, className)}>
<DateRangePickerInput value={selectedDate} onChange={handleInputChange} />
<DateRangePickerTooltip
{...props}
visibleMonth={visibleMonth}
onVisibleMonthChange={setVisibleMonth}
/>
</div>
);
};

interface DateRangePickerTooltipProps
extends BaseDatePickerProps<RangeSelectionValueType> {
visibleMonth: DateValue | undefined;
onVisibleMonthChange: (d: DateValue) => void;
}

const DateRangePickerTooltip = (props: DateRangePickerTooltipProps) => {
const {
onVisibleMonthChange,
onSelectedDateChange,
closeOnSelection,
...rest
} = props;
const [isOpen, setIsOpen] = useState<boolean>(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 (
<Tooltip
content={
<Calendar
selectionVariant="range"
onVisibleMonthChange={handleVisibleMonthChange}
onSelectedDateChange={handleDateSelection}
defaultSelectedDate={defaultSelectedDate}
{...rest}
/>
}
className="vuuDatePickerTooltip"
disableHoverListener
hideArrow
hideIcon
placement="bottom-end"
open={isOpen}
>
<CalendarIconButton onClick={() => setIsOpen((o) => !o)} />
</Tooltip>
);
};
2 changes: 2 additions & 0 deletions vuu-ui/packages/vuu-ui-controls/src/date-picker/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./DatePicker";
export * from "./DateRangePicker";
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
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<DateValue>;

export const DatePickerInput: React.FC<Props> = (props) => {
const { value, onChange, className } = props;

const onInputChange = useCallback<React.ChangeEventHandler<HTMLInputElement>>(
(e) => {
const v = e.target.value;
if (v === "") return;
else onChange(toCalendarDate(new Date(v)));
},
[onChange]
);

return (
<input
className={clsx(baseClass, className)}
type="date"
value={value?.toString()}
onChange={onInputChange}
aria-label="date-input"
/>
);
};

function toCalendarDate(d: Date): CalendarDate {
return new CalendarDate(d.getFullYear(), d.getMonth() + 1, d.getDate());
}
Original file line number Diff line number Diff line change
@@ -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: "—";
}
Original file line number Diff line number Diff line change
@@ -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<RangeSelectionValueType>;

export const DateRangePickerInput: React.FC<Props> = (props) => {
const { value, onChange, className } = props;

const getHandleInputChange = useCallback(
(k: keyof RangeSelectionValueType) => (d: DateValue) => {
onChange({ ...value, [k]: d });
},
[value, onChange]
);

return (
<div className={clsx(baseClass, className)}>
{(["startDate", "endDate"] as const).map((t) => (
<DatePickerInput
key={t}
value={value?.[t]}
onChange={getHandleInputChange(t)}
/>
))}
</div>
);
};
10 changes: 10 additions & 0 deletions vuu-ui/packages/vuu-ui-controls/src/date-picker/input/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { DateValue } from "@internationalized/date";
import { RangeSelectionValueType } from "../../calendar";

export type PickerSelectionType = DateValue | RangeSelectionValueType;

export interface BasePickerInputProps<T extends PickerSelectionType> {
onChange: (selected: T) => void;
value?: T;
className?: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
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<HTMLButtonElement>
) {
return (
<Button
variant={"secondary"}
aria-label="calendar-icon-button"
{...props}
ref={ref}
>
<CalendarIcon />
</Button>
);
});
1 change: 1 addition & 0 deletions vuu-ui/packages/vuu-ui-controls/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Loading

0 comments on commit 9b1ba4e

Please sign in to comment.