@mbao01/common 0.0.42 → 0.0.44
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/types/components/DatetimePicker/DatetimeGrid.d.ts +26 -0
- package/dist/types/components/DatetimePicker/DatetimePicker.d.ts +13 -0
- package/dist/types/components/DatetimePicker/constants.d.ts +15 -0
- package/dist/types/components/DatetimePicker/index.d.ts +1 -0
- package/dist/types/components/DatetimePicker/types.d.ts +25 -0
- package/dist/types/components/Form/DatetimeInput/DatetimeCalendar.d.ts +5 -0
- package/dist/types/components/Form/DatetimeInput/DatetimeInput.d.ts +83 -0
- package/dist/types/components/Form/DatetimeInput/DatetimeInputContext.d.ts +2 -0
- package/dist/types/components/Form/DatetimeInput/NaturalLanguageInput.d.ts +5 -0
- package/dist/types/components/Form/DatetimeInput/TimePicker.d.ts +1 -0
- package/dist/types/components/Form/DatetimeInput/constants.d.ts +24 -0
- package/dist/types/components/Form/DatetimeInput/helpers.d.ts +27 -0
- package/dist/types/components/Form/DatetimeInput/hooks/index.d.ts +1 -0
- package/dist/types/components/Form/DatetimeInput/hooks/useDateInput/index.d.ts +1 -0
- package/dist/types/components/Form/DatetimeInput/hooks/useDateInput/useDateInput.d.ts +1 -0
- package/dist/types/components/Form/DatetimeInput/index.d.ts +1 -0
- package/dist/types/components/Form/DatetimeInput/types.d.ts +31 -0
- package/dist/types/components/Form/MultiSelect/MultiSelect.d.ts +46 -0
- package/dist/types/components/Form/MultiSelect/MultiSelectContext.d.ts +2 -0
- package/dist/types/components/Form/MultiSelect/constants.d.ts +19 -0
- package/dist/types/components/Form/MultiSelect/hooks/index.d.ts +1 -0
- package/dist/types/components/Form/MultiSelect/hooks/useMultiSelect/index.d.ts +1 -0
- package/dist/types/components/Form/MultiSelect/hooks/useMultiSelect/useMultiSelect.d.ts +1 -0
- package/dist/types/components/Form/MultiSelect/index.d.ts +1 -0
- package/dist/types/components/Form/MultiSelect/types.d.ts +31 -0
- package/dist/types/components/Form/TagsInput/TagsInput.d.ts +13 -0
- package/dist/types/components/Form/TagsInput/constants.d.ts +16 -0
- package/dist/types/components/Form/TagsInput/index.d.ts +1 -0
- package/dist/types/components/Form/TagsInput/types.d.ts +9 -0
- package/dist/types/components/Form/index.d.ts +2 -0
- package/dist/types/index.d.ts +1 -0
- package/package.json +7 -2
- package/src/components/Calendar/Calendar.tsx +2 -1
- package/src/components/DatetimePicker/DatetimeGrid.tsx +59 -0
- package/src/components/DatetimePicker/DatetimePicker.tsx +59 -0
- package/src/components/DatetimePicker/constants.ts +102 -0
- package/src/components/DatetimePicker/index.ts +1 -0
- package/src/components/DatetimePicker/types.ts +36 -0
- package/src/components/Description/Description.tsx +2 -2
- package/src/components/Form/DatetimeInput/DatetimeCalendar.tsx +68 -0
- package/src/components/Form/DatetimeInput/DatetimeInput.tsx +90 -0
- package/src/components/Form/DatetimeInput/DatetimeInputContext.tsx +4 -0
- package/src/components/Form/DatetimeInput/NaturalLanguageInput.tsx +73 -0
- package/src/components/Form/DatetimeInput/TimePicker.tsx +202 -0
- package/src/components/Form/DatetimeInput/constants.ts +135 -0
- package/src/components/Form/DatetimeInput/helpers.ts +93 -0
- package/src/components/Form/DatetimeInput/hooks/index.ts +1 -0
- package/src/components/Form/DatetimeInput/hooks/useDateInput/index.ts +1 -0
- package/src/components/Form/DatetimeInput/hooks/useDateInput/useDateInput.ts +10 -0
- package/src/components/Form/DatetimeInput/index.ts +1 -0
- package/src/components/Form/DatetimeInput/types.ts +36 -0
- package/src/components/Form/MultiSelect/MultiSelect.tsx +348 -0
- package/src/components/Form/MultiSelect/MultiSelectContext.tsx +4 -0
- package/src/components/Form/MultiSelect/constants.ts +103 -0
- package/src/components/Form/MultiSelect/hooks/index.ts +1 -0
- package/src/components/Form/MultiSelect/hooks/useMultiSelect/index.ts +1 -0
- package/src/components/Form/MultiSelect/hooks/useMultiSelect/useMultiSelect.ts +10 -0
- package/src/components/Form/MultiSelect/index.ts +1 -0
- package/src/components/Form/MultiSelect/types.ts +46 -0
- package/src/components/Form/Phone/Phone.tsx +1 -0
- package/src/components/Form/Slider/Slider.tsx +20 -5
- package/src/components/Form/TagsInput/TagsInput.tsx +278 -0
- package/src/components/Form/TagsInput/constants.ts +87 -0
- package/src/components/Form/TagsInput/index.ts +1 -0
- package/src/components/Form/TagsInput/types.ts +10 -0
- package/src/components/Form/TextField/TextField.tsx +1 -1
- package/src/components/Form/index.ts +2 -0
- package/src/index.ts +1 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { forwardRef, useCallback } from "react";
|
|
4
|
+
import { useTimescape } from "timescape/react";
|
|
5
|
+
import type { DatetimePickerProps } from "./types";
|
|
6
|
+
import { DEFAULTS, INPUT_PLACEHOLDERS } from "./constants";
|
|
7
|
+
import { DatetimeGrid } from "./DatetimeGrid";
|
|
8
|
+
|
|
9
|
+
export const DatetimePicker = forwardRef<HTMLDivElement, DatetimePickerProps>(
|
|
10
|
+
(
|
|
11
|
+
{
|
|
12
|
+
format = DEFAULTS,
|
|
13
|
+
placeholders,
|
|
14
|
+
onChange,
|
|
15
|
+
className,
|
|
16
|
+
date,
|
|
17
|
+
minDate,
|
|
18
|
+
maxDate,
|
|
19
|
+
hour12 = true,
|
|
20
|
+
digits,
|
|
21
|
+
wrapAround,
|
|
22
|
+
snapToStep,
|
|
23
|
+
...props
|
|
24
|
+
},
|
|
25
|
+
ref
|
|
26
|
+
) => {
|
|
27
|
+
const handleDateChange = useCallback(
|
|
28
|
+
(date: Date | undefined) => {
|
|
29
|
+
if (onChange) {
|
|
30
|
+
onChange(date);
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
[onChange]
|
|
34
|
+
);
|
|
35
|
+
const timescape = useTimescape({
|
|
36
|
+
date,
|
|
37
|
+
onChangeDate: handleDateChange,
|
|
38
|
+
minDate,
|
|
39
|
+
maxDate,
|
|
40
|
+
hour12,
|
|
41
|
+
digits,
|
|
42
|
+
wrapAround,
|
|
43
|
+
snapToStep,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<DatetimeGrid
|
|
48
|
+
format={format}
|
|
49
|
+
className={className}
|
|
50
|
+
timescape={timescape}
|
|
51
|
+
placeholders={placeholders ?? INPUT_PLACEHOLDERS}
|
|
52
|
+
ref={ref}
|
|
53
|
+
{...props}
|
|
54
|
+
/>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
DatetimePicker.displayName = "DatetimePicker";
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { DateFormat, DatetimeFormatDefaults, InputPlaceholders, TimeFormat } from "./types";
|
|
2
|
+
import { cva } from "../../libs";
|
|
3
|
+
|
|
4
|
+
export const DEFAULTS = [
|
|
5
|
+
["days", "months", "years"],
|
|
6
|
+
["hours", "minutes", "am/pm"],
|
|
7
|
+
] as DatetimeFormatDefaults;
|
|
8
|
+
|
|
9
|
+
export const INPUT_PLACEHOLDERS: InputPlaceholders = {
|
|
10
|
+
months: "MM",
|
|
11
|
+
days: "DD",
|
|
12
|
+
years: "YYYY",
|
|
13
|
+
hours: "HH",
|
|
14
|
+
minutes: "MM",
|
|
15
|
+
seconds: "SS",
|
|
16
|
+
"am/pm": "AM/PM",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const getDatetimeGridClasses = cva(
|
|
20
|
+
`flex items-center w-fit p-1 rounded-md transition-all duration-100 gap-1 selection:bg-transparent selection:text-base-content
|
|
21
|
+
[&:has(input:focus)]:duration-100 [&:has(input:focus)]:outline [&:has(input:focus)]:outline-2 [&:has(input:focus)]:outline-offset-2 [&:has(input:focus)]:outline-base-content/20
|
|
22
|
+
[&:has(input:focus-within)]:duration-100 [&:has(input:focus-within)]:outline [&:has(input:focus-within)]:outline-2 [&:has(input:focus-within)]:outline-offset-2 [&:has(input:focus-within)]:outline-base-content/20
|
|
23
|
+
`,
|
|
24
|
+
{
|
|
25
|
+
variants: {
|
|
26
|
+
variant: {
|
|
27
|
+
default: "border-0",
|
|
28
|
+
accent: "border border-accent",
|
|
29
|
+
error: "border border-error",
|
|
30
|
+
ghost: "border border-ghost",
|
|
31
|
+
info: "border border-info",
|
|
32
|
+
primary: "border border-primary",
|
|
33
|
+
secondary: "border border-secondary",
|
|
34
|
+
success: "border border-success",
|
|
35
|
+
warning: "border border-warning",
|
|
36
|
+
},
|
|
37
|
+
outline: {
|
|
38
|
+
true: "border",
|
|
39
|
+
},
|
|
40
|
+
disabled: {
|
|
41
|
+
true: "border-base-300",
|
|
42
|
+
},
|
|
43
|
+
wide: {
|
|
44
|
+
true: "w-full",
|
|
45
|
+
},
|
|
46
|
+
size: {
|
|
47
|
+
xs: "h-6 leading-relaxed px-2 text-xs",
|
|
48
|
+
sm: "h-8 leading-8 px-3 text-sm",
|
|
49
|
+
md: "h-12 leading-loose px-4 text-sm",
|
|
50
|
+
lg: "h-16 leading-loose px-5 text-lg",
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
compoundVariants: [
|
|
54
|
+
{
|
|
55
|
+
size: undefined,
|
|
56
|
+
className: "min-h-fit h-10",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
variant: undefined,
|
|
60
|
+
outline: true,
|
|
61
|
+
className: "border-neutral-content",
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
variant: "default",
|
|
65
|
+
outline: true,
|
|
66
|
+
className: "border-base-content",
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
}
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
export const getDatetimeSeparatorClasses = cva("text-xs text-gray-400");
|
|
73
|
+
|
|
74
|
+
export const getDatetimeInputClasses = cva(
|
|
75
|
+
"min-w-8 p-1 inline tabular-nums h-fit border-none outline-none select-none content-box caret-transparent rounded-sm min-w-8 text-center focus:outline-none focus:bg-base-content/20 focus-visible:ring-0 focus-visible:outline-none",
|
|
76
|
+
{
|
|
77
|
+
variants: {
|
|
78
|
+
size: {
|
|
79
|
+
xs: "max-h-4",
|
|
80
|
+
sm: "max-h-6",
|
|
81
|
+
md: "",
|
|
82
|
+
lg: "min-w-8",
|
|
83
|
+
},
|
|
84
|
+
unit: {
|
|
85
|
+
years: "min-w-12",
|
|
86
|
+
"am/pm": "bg-base-content/15",
|
|
87
|
+
} as Record<DateFormat | TimeFormat, string>,
|
|
88
|
+
},
|
|
89
|
+
compoundVariants: [
|
|
90
|
+
{
|
|
91
|
+
size: "lg",
|
|
92
|
+
unit: "years",
|
|
93
|
+
className: "min-w-14",
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
size: "lg",
|
|
97
|
+
unit: "am/pm",
|
|
98
|
+
className: "min-w-10",
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
}
|
|
102
|
+
);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { DatetimePicker } from "./DatetimePicker";
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Options } from "timescape/react";
|
|
2
|
+
import { useTimescape } from "timescape/react";
|
|
3
|
+
import { type VariantProps } from "../../libs";
|
|
4
|
+
import { getDatetimeGridClasses } from "./constants";
|
|
5
|
+
|
|
6
|
+
export type DateFormat = "days" | "months" | "years";
|
|
7
|
+
|
|
8
|
+
export type TimeFormat = "hours" | "minutes" | "seconds" | "am/pm";
|
|
9
|
+
|
|
10
|
+
type TimescapeReturn = ReturnType<typeof useTimescape>;
|
|
11
|
+
type DatetimeArray<T extends DateFormat | TimeFormat> = T[];
|
|
12
|
+
|
|
13
|
+
export type DatetimeFormatDefaults =
|
|
14
|
+
| [DatetimeArray<DateFormat>]
|
|
15
|
+
| [DatetimeArray<TimeFormat>]
|
|
16
|
+
| [DatetimeArray<DateFormat>, DatetimeArray<TimeFormat>]
|
|
17
|
+
| [DatetimeArray<TimeFormat>, DatetimeArray<DateFormat>];
|
|
18
|
+
|
|
19
|
+
export type InputPlaceholders = Record<DateFormat | TimeFormat, string>;
|
|
20
|
+
|
|
21
|
+
type DatetimeGridVariantProps = VariantProps<typeof getDatetimeGridClasses>;
|
|
22
|
+
export type DatetimeGridProps = {
|
|
23
|
+
format: DatetimeFormatDefaults;
|
|
24
|
+
disabled?: boolean;
|
|
25
|
+
className?: string;
|
|
26
|
+
timescape: Pick<TimescapeReturn, "getRootProps" | "getInputProps">;
|
|
27
|
+
placeholders: InputPlaceholders;
|
|
28
|
+
} & DatetimeGridVariantProps;
|
|
29
|
+
|
|
30
|
+
export type DatetimePickerProps = Omit<Options, "onChangeDate"> & {
|
|
31
|
+
format?: DatetimeFormatDefaults;
|
|
32
|
+
disabled?: boolean;
|
|
33
|
+
placeholders?: InputPlaceholders;
|
|
34
|
+
onChange?: Options["onChangeDate"];
|
|
35
|
+
className?: string;
|
|
36
|
+
} & DatetimeGridVariantProps;
|
|
@@ -2,11 +2,11 @@ import type { DescriptionDetailProps, DescriptionProps, DescriptionTermProps } f
|
|
|
2
2
|
import { cn } from "../../utilities";
|
|
3
3
|
|
|
4
4
|
const Description = (props: DescriptionProps) => {
|
|
5
|
-
return <dl
|
|
5
|
+
return <dl {...props} />;
|
|
6
6
|
};
|
|
7
7
|
|
|
8
8
|
const DescriptionTerm = ({ className, ...props }: DescriptionTermProps) => {
|
|
9
|
-
return <dt
|
|
9
|
+
return <dt className={cn("py-2 text-sm font-semibold", className)} {...props} />;
|
|
10
10
|
};
|
|
11
11
|
|
|
12
12
|
const DescriptionDetail = ({ className, ...props }: DescriptionDetailProps) => {
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
import { CalendarIcon } from "@radix-ui/react-icons";
|
|
3
|
+
import { cn } from "../../../utilities";
|
|
4
|
+
import { Button } from "../../Button";
|
|
5
|
+
import { Calendar } from "../../Calendar";
|
|
6
|
+
import { Popover } from "../../Popover";
|
|
7
|
+
import {
|
|
8
|
+
getDatetimeCalendarClasses,
|
|
9
|
+
getDatetimeCalendarIconClasses,
|
|
10
|
+
getDatetimeCalendarTriggerClasses,
|
|
11
|
+
} from "./constants";
|
|
12
|
+
import { parseDateTime } from "./helpers";
|
|
13
|
+
import { useDateInput } from "./hooks";
|
|
14
|
+
import { TimePicker } from "./TimePicker";
|
|
15
|
+
import { type DatetimeCalendarProps } from "./types";
|
|
16
|
+
|
|
17
|
+
export const DatetimeCalendar = ({
|
|
18
|
+
size,
|
|
19
|
+
disabled,
|
|
20
|
+
className,
|
|
21
|
+
...props
|
|
22
|
+
}: DatetimeCalendarProps) => {
|
|
23
|
+
const { value, onDateChange, time } = useDateInput();
|
|
24
|
+
|
|
25
|
+
const handleSelect = useCallback(
|
|
26
|
+
(_: Date | undefined, triggerDate: Date) => {
|
|
27
|
+
const parsedDateTime = parseDateTime(triggerDate);
|
|
28
|
+
|
|
29
|
+
if (parsedDateTime) {
|
|
30
|
+
const [hours, minutes] = time.split(":");
|
|
31
|
+
parsedDateTime.setHours(parseInt(hours) || 0, parseInt(minutes) || 0);
|
|
32
|
+
onDateChange(parsedDateTime);
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
[time, onDateChange]
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<Popover>
|
|
40
|
+
<Popover.Trigger asChild>
|
|
41
|
+
<Button
|
|
42
|
+
variant="default"
|
|
43
|
+
disabled={disabled}
|
|
44
|
+
className={cn(getDatetimeCalendarTriggerClasses({ size }))}
|
|
45
|
+
>
|
|
46
|
+
<CalendarIcon className={getDatetimeCalendarIconClasses({ size })} />
|
|
47
|
+
<span className="sr-only">calendar</span>
|
|
48
|
+
</Button>
|
|
49
|
+
</Popover.Trigger>
|
|
50
|
+
<Popover.Content className="w-auto p-0" sideOffset={8}>
|
|
51
|
+
<div className="flex gap-1">
|
|
52
|
+
<Calendar
|
|
53
|
+
{...props}
|
|
54
|
+
autoFocus
|
|
55
|
+
disabled={disabled}
|
|
56
|
+
className={cn(getDatetimeCalendarClasses(), className)}
|
|
57
|
+
mode="single"
|
|
58
|
+
selected={value}
|
|
59
|
+
onSelect={handleSelect}
|
|
60
|
+
/>
|
|
61
|
+
<TimePicker />
|
|
62
|
+
</div>
|
|
63
|
+
</Popover.Content>
|
|
64
|
+
</Popover>
|
|
65
|
+
);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
DatetimeCalendar.displayName = "DatetimeCalendar";
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { forwardRef, useCallback, useEffect, useState } from "react";
|
|
4
|
+
import type { DatetimeInputProps, TimeString } from "./types";
|
|
5
|
+
import { cn } from "../../../utilities";
|
|
6
|
+
import { getDatetimeInputContainerClasses } from "./constants";
|
|
7
|
+
import { DatetimeCalendar } from "./DatetimeCalendar";
|
|
8
|
+
import { DatetimeInputContext } from "./DatetimeInputContext";
|
|
9
|
+
import { getParsedTime, parseDateTime } from "./helpers";
|
|
10
|
+
import { NaturalLanguageInput } from "./NaturalLanguageInput";
|
|
11
|
+
|
|
12
|
+
export const DatetimeInput = forwardRef<HTMLInputElement, DatetimeInputProps>(
|
|
13
|
+
(
|
|
14
|
+
{
|
|
15
|
+
calendar,
|
|
16
|
+
className,
|
|
17
|
+
date,
|
|
18
|
+
defaultDate,
|
|
19
|
+
locale,
|
|
20
|
+
onDateChange,
|
|
21
|
+
placeholder,
|
|
22
|
+
outline,
|
|
23
|
+
disabled,
|
|
24
|
+
size,
|
|
25
|
+
variant,
|
|
26
|
+
wide,
|
|
27
|
+
...props
|
|
28
|
+
},
|
|
29
|
+
ref
|
|
30
|
+
) => {
|
|
31
|
+
const [_date, setDate] = useState<Date | undefined>(
|
|
32
|
+
defaultDate ? parseDateTime(defaultDate) : undefined
|
|
33
|
+
);
|
|
34
|
+
const [time, setTime] = useState<TimeString>(defaultDate ? getParsedTime(defaultDate) : "");
|
|
35
|
+
const isControlled = date !== undefined;
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
// sync internal state with controlled value if provided
|
|
39
|
+
if (isControlled) {
|
|
40
|
+
setDate(date);
|
|
41
|
+
}
|
|
42
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
43
|
+
}, [date]);
|
|
44
|
+
|
|
45
|
+
const handleDateChange = useCallback((d: Date | undefined) => {
|
|
46
|
+
if (!isControlled) {
|
|
47
|
+
setDate(d);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
onDateChange?.(d);
|
|
51
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
52
|
+
}, []);
|
|
53
|
+
|
|
54
|
+
const onTimeChange = useCallback((time: TimeString) => {
|
|
55
|
+
setTime(time);
|
|
56
|
+
}, []);
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<DatetimeInputContext.Provider
|
|
60
|
+
value={{
|
|
61
|
+
value: isControlled ? date : _date,
|
|
62
|
+
onDateChange: handleDateChange,
|
|
63
|
+
time,
|
|
64
|
+
onTimeChange,
|
|
65
|
+
}}
|
|
66
|
+
>
|
|
67
|
+
<div className="flex items-center justify-center flex-nowrap">
|
|
68
|
+
<div
|
|
69
|
+
className={cn(
|
|
70
|
+
getDatetimeInputContainerClasses({ outline, disabled, size, variant, wide }),
|
|
71
|
+
className
|
|
72
|
+
)}
|
|
73
|
+
>
|
|
74
|
+
<DatetimeCalendar size={size} disabled={disabled} defaultMonth={_date} {...calendar} />
|
|
75
|
+
<NaturalLanguageInput
|
|
76
|
+
ref={ref}
|
|
77
|
+
size={size}
|
|
78
|
+
locale={locale}
|
|
79
|
+
disabled={disabled}
|
|
80
|
+
placeholder={placeholder}
|
|
81
|
+
{...props}
|
|
82
|
+
/>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
</DatetimeInputContext.Provider>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
DatetimeInput.displayName = "DatetimeInput";
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { ChangeEvent, KeyboardEvent } from "react";
|
|
2
|
+
import { forwardRef, useCallback, useEffect, useState } from "react";
|
|
3
|
+
import { Input } from "../Input";
|
|
4
|
+
import { getNaturalLanguageInputClasses } from "./constants";
|
|
5
|
+
import { formatDateTime, getParsedTime, parseDateTime, setDateTime } from "./helpers";
|
|
6
|
+
import { useDateInput } from "./hooks";
|
|
7
|
+
import { type NaturalLanguageInputProps } from "./types";
|
|
8
|
+
|
|
9
|
+
export const NaturalLanguageInput = forwardRef<HTMLInputElement, NaturalLanguageInputProps>(
|
|
10
|
+
({ placeholder = 'e.g. "tomorrow at 5pm" or "in 2 hours"', size, locale, ...props }, ref) => {
|
|
11
|
+
const { value, onDateChange, time, onTimeChange } = useDateInput();
|
|
12
|
+
const [inputValue, setInputValue] = useState<string>("");
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
const timeVal = time || getParsedTime(new Date());
|
|
16
|
+
setInputValue(value ? formatDateTime(setDateTime(value, timeVal), locale) : "");
|
|
17
|
+
onTimeChange(timeVal);
|
|
18
|
+
}, [value, time, locale, onTimeChange]);
|
|
19
|
+
|
|
20
|
+
const handleParse = useCallback(
|
|
21
|
+
(e: ChangeEvent<HTMLInputElement>) => {
|
|
22
|
+
const input = e.currentTarget.value;
|
|
23
|
+
if (!input) {
|
|
24
|
+
onDateChange(undefined);
|
|
25
|
+
onTimeChange("");
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// parse the date string when the input field loses focus
|
|
30
|
+
const parsedDateTime = parseDateTime(input);
|
|
31
|
+
if (parsedDateTime) {
|
|
32
|
+
onDateChange(parsedDateTime);
|
|
33
|
+
setInputValue(formatDateTime(parsedDateTime, locale));
|
|
34
|
+
onTimeChange(getParsedTime(parsedDateTime));
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
[locale, onDateChange, onTimeChange]
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const handleKeydown = useCallback(
|
|
41
|
+
(e: KeyboardEvent<HTMLInputElement>) => {
|
|
42
|
+
const parsedDateTime = parseDateTime(e.currentTarget.value);
|
|
43
|
+
switch (e.key) {
|
|
44
|
+
case "Enter":
|
|
45
|
+
if (parsedDateTime) {
|
|
46
|
+
onDateChange(parsedDateTime);
|
|
47
|
+
setInputValue(formatDateTime(parsedDateTime, locale));
|
|
48
|
+
onTimeChange(getParsedTime(parsedDateTime));
|
|
49
|
+
}
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
[locale, onDateChange, onTimeChange]
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<Input
|
|
58
|
+
ref={ref}
|
|
59
|
+
type="text"
|
|
60
|
+
size={size}
|
|
61
|
+
placeholder={placeholder}
|
|
62
|
+
value={inputValue}
|
|
63
|
+
onChange={(e) => setInputValue(e.currentTarget.value)}
|
|
64
|
+
onKeyDown={handleKeydown}
|
|
65
|
+
onBlur={handleParse}
|
|
66
|
+
className={getNaturalLanguageInputClasses({ size })}
|
|
67
|
+
{...props}
|
|
68
|
+
/>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
NaturalLanguageInput.displayName = "NaturalLanguageInput";
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import type { KeyboardEvent } from "react";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
3
|
+
import { cn } from "../../../utilities";
|
|
4
|
+
import { ScrollArea } from "../../ScrollArea";
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_SIZE,
|
|
7
|
+
getTimePickerClasses,
|
|
8
|
+
getTimePickerListClasses,
|
|
9
|
+
getTimePickerScrollAreaClasses,
|
|
10
|
+
} from "./constants";
|
|
11
|
+
import { parseDateTime, setDateTime } from "./helpers";
|
|
12
|
+
import { useDateInput } from "./hooks";
|
|
13
|
+
import { type TimeString } from "./types";
|
|
14
|
+
|
|
15
|
+
export const TimePicker = () => {
|
|
16
|
+
const { value, onDateChange, time, onTimeChange } = useDateInput();
|
|
17
|
+
const [activeIndex, setActiveIndex] = useState(-1);
|
|
18
|
+
const timestamp = 15;
|
|
19
|
+
|
|
20
|
+
const formateSelectedTime = useCallback(
|
|
21
|
+
(time: TimeString) => {
|
|
22
|
+
onTimeChange(time);
|
|
23
|
+
|
|
24
|
+
const newVal = parseDateTime(value ?? new Date());
|
|
25
|
+
|
|
26
|
+
if (!newVal) return;
|
|
27
|
+
|
|
28
|
+
onDateChange(setDateTime(newVal, time));
|
|
29
|
+
},
|
|
30
|
+
[value, onDateChange, onTimeChange]
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const handleKeydown = useCallback(
|
|
34
|
+
(e: KeyboardEvent<HTMLDivElement>) => {
|
|
35
|
+
e.stopPropagation();
|
|
36
|
+
|
|
37
|
+
if (!document) return;
|
|
38
|
+
|
|
39
|
+
const moveNext = () => {
|
|
40
|
+
const nextIndex = activeIndex + 1 > DEFAULT_SIZE - 1 ? 0 : activeIndex + 1;
|
|
41
|
+
|
|
42
|
+
const currentElm = document.getElementById(`time-${nextIndex}`);
|
|
43
|
+
|
|
44
|
+
currentElm?.focus();
|
|
45
|
+
|
|
46
|
+
setActiveIndex(nextIndex);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const movePrev = () => {
|
|
50
|
+
const prevIndex = activeIndex - 1 < 0 ? DEFAULT_SIZE - 1 : activeIndex - 1;
|
|
51
|
+
|
|
52
|
+
const currentElm = document.getElementById(`time-${prevIndex}`);
|
|
53
|
+
|
|
54
|
+
currentElm?.focus();
|
|
55
|
+
|
|
56
|
+
setActiveIndex(prevIndex);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const setElement = () => {
|
|
60
|
+
const currentElm = document.getElementById(`time-${activeIndex}`);
|
|
61
|
+
|
|
62
|
+
if (!currentElm) return;
|
|
63
|
+
|
|
64
|
+
currentElm.focus();
|
|
65
|
+
|
|
66
|
+
formateSelectedTime((currentElm.textContent ?? "") as TimeString);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const reset = () => {
|
|
70
|
+
const currentElm = document.getElementById(`time-${activeIndex}`);
|
|
71
|
+
currentElm?.blur();
|
|
72
|
+
setActiveIndex(-1);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
switch (e.key) {
|
|
76
|
+
case "ArrowUp":
|
|
77
|
+
movePrev();
|
|
78
|
+
break;
|
|
79
|
+
|
|
80
|
+
case "ArrowDown":
|
|
81
|
+
moveNext();
|
|
82
|
+
break;
|
|
83
|
+
|
|
84
|
+
case "Escape":
|
|
85
|
+
reset();
|
|
86
|
+
break;
|
|
87
|
+
|
|
88
|
+
case "Enter":
|
|
89
|
+
setElement();
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
[activeIndex, formateSelectedTime]
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
const handleClick = useCallback(
|
|
97
|
+
(time: TimeString, currentIndex: number) => {
|
|
98
|
+
formateSelectedTime(time);
|
|
99
|
+
setActiveIndex(currentIndex);
|
|
100
|
+
},
|
|
101
|
+
[formateSelectedTime]
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const currentTime = useMemo(() => {
|
|
105
|
+
const timeVal = time.split(" ")[0];
|
|
106
|
+
return {
|
|
107
|
+
hours: parseInt(timeVal.split(":")[0]),
|
|
108
|
+
minutes: parseInt(timeVal.split(":")[1]),
|
|
109
|
+
};
|
|
110
|
+
}, [time]);
|
|
111
|
+
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
const getCurrentElementTime = () => {
|
|
114
|
+
const timeVal = time.split(" ")[0];
|
|
115
|
+
const hours = parseInt(timeVal.split(":")[0]);
|
|
116
|
+
const minutes = parseInt(timeVal.split(":")[1]);
|
|
117
|
+
const PM_AM = time.split(" ")[1];
|
|
118
|
+
|
|
119
|
+
const formatIndex = PM_AM === "AM" ? hours : hours === 12 ? hours : hours + 12;
|
|
120
|
+
const formattedHours = formatIndex;
|
|
121
|
+
|
|
122
|
+
for (let j = 0; j <= 3; j++) {
|
|
123
|
+
const diff = Math.abs(j * timestamp - minutes);
|
|
124
|
+
const selected =
|
|
125
|
+
PM_AM === (formattedHours >= 12 ? "PM" : "AM") &&
|
|
126
|
+
(minutes <= 53 ? diff < Math.ceil(timestamp / 2) : diff < timestamp);
|
|
127
|
+
|
|
128
|
+
if (selected) {
|
|
129
|
+
const trueIndex = activeIndex === -1 ? formattedHours * 4 + j : activeIndex;
|
|
130
|
+
|
|
131
|
+
setActiveIndex(trueIndex);
|
|
132
|
+
|
|
133
|
+
const currentElm = document.getElementById(`time-${trueIndex}`);
|
|
134
|
+
currentElm?.scrollIntoView({
|
|
135
|
+
block: "center",
|
|
136
|
+
behavior: "smooth",
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
getCurrentElementTime();
|
|
143
|
+
}, [time, activeIndex]);
|
|
144
|
+
|
|
145
|
+
const height = useMemo(() => {
|
|
146
|
+
if (!document) return;
|
|
147
|
+
const calendarElm = document.getElementById("calendar");
|
|
148
|
+
if (!calendarElm) return;
|
|
149
|
+
return calendarElm.style.height;
|
|
150
|
+
}, []);
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<div className="space-y-2 pr-3 py-3 relative ">
|
|
154
|
+
<h3 className="text-sm font-medium ">Time</h3>
|
|
155
|
+
<ScrollArea
|
|
156
|
+
onKeyDown={handleKeydown}
|
|
157
|
+
className={cn(getTimePickerScrollAreaClasses())}
|
|
158
|
+
style={{ height }}
|
|
159
|
+
>
|
|
160
|
+
<ul className={cn(getTimePickerListClasses())}>
|
|
161
|
+
{Array.from({ length: 24 }).map((_, i) => {
|
|
162
|
+
const PM_AM = i >= 12 ? "PM" : "AM";
|
|
163
|
+
const formatIndex = i > 12 ? i % 12 : i === 0 || i === 12 ? 12 : i;
|
|
164
|
+
return Array.from({ length: 4 }).map((_, part) => {
|
|
165
|
+
const diff = Math.abs(part * timestamp - currentTime.minutes);
|
|
166
|
+
|
|
167
|
+
const trueIndex = i * 4 + part;
|
|
168
|
+
|
|
169
|
+
// ? refactor : add the select of the default time on the current device (H:MM)
|
|
170
|
+
const isSelected =
|
|
171
|
+
(currentTime.hours === i || currentTime.hours === formatIndex) &&
|
|
172
|
+
time.split(" ")[1] === PM_AM &&
|
|
173
|
+
(currentTime.minutes <= 53 ? diff < Math.ceil(timestamp / 2) : diff < timestamp);
|
|
174
|
+
|
|
175
|
+
const isSuggested = !value && isSelected;
|
|
176
|
+
|
|
177
|
+
const currentValue = `${formatIndex}:${
|
|
178
|
+
part === 0 ? "00" : timestamp * part
|
|
179
|
+
} ${PM_AM}` as TimeString;
|
|
180
|
+
|
|
181
|
+
return (
|
|
182
|
+
<li
|
|
183
|
+
tabIndex={isSelected ? 0 : -1}
|
|
184
|
+
id={`time-${trueIndex}`}
|
|
185
|
+
key={`time-${trueIndex}`}
|
|
186
|
+
aria-label={currentValue}
|
|
187
|
+
className={cn(
|
|
188
|
+
getTimePickerClasses({ selected: isSelected, suggested: isSuggested })
|
|
189
|
+
)}
|
|
190
|
+
onClick={() => handleClick(currentValue, trueIndex)}
|
|
191
|
+
onFocus={() => isSuggested && setActiveIndex(trueIndex)}
|
|
192
|
+
>
|
|
193
|
+
{currentValue}
|
|
194
|
+
</li>
|
|
195
|
+
);
|
|
196
|
+
});
|
|
197
|
+
})}
|
|
198
|
+
</ul>
|
|
199
|
+
</ScrollArea>
|
|
200
|
+
</div>
|
|
201
|
+
);
|
|
202
|
+
};
|