-
Notifications
You must be signed in to change notification settings - Fork 27
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
#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.
- Loading branch information
1 parent
62edc24
commit 9b1ba4e
Showing
14 changed files
with
414 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
23 changes: 23 additions & 0 deletions
23
vuu-ui/packages/vuu-ui-controls/src/date-picker/DatePicker.css
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
107
vuu-ui/packages/vuu-ui-controls/src/date-picker/DatePicker.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
}; |
99 changes: 99 additions & 0 deletions
99
vuu-ui/packages/vuu-ui-controls/src/date-picker/DateRangePicker.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from "./DatePicker"; | ||
export * from "./DateRangePicker"; |
13 changes: 13 additions & 0 deletions
13
vuu-ui/packages/vuu-ui-controls/src/date-picker/input/DatePickerInput.css
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
37 changes: 37 additions & 0 deletions
37
vuu-ui/packages/vuu-ui-controls/src/date-picker/input/DatePickerInput.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | ||
} |
24 changes: 24 additions & 0 deletions
24
vuu-ui/packages/vuu-ui-controls/src/date-picker/input/DateRangePickerInput.css
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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: "—"; | ||
} |
35 changes: 35 additions & 0 deletions
35
vuu-ui/packages/vuu-ui-controls/src/date-picker/input/DateRangePickerInput.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
10
vuu-ui/packages/vuu-ui-controls/src/date-picker/input/types.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
19 changes: 19 additions & 0 deletions
19
vuu-ui/packages/vuu-ui-controls/src/date-picker/internal/CalendarIconButton.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.