@mbao01/common 0.0.43 → 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 +4 -2
- 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/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/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/index.ts +2 -0
- package/src/index.ts +1 -0
|
@@ -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;
|
|
@@ -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
|
+
};
|