myoperator-mcp 0.2.328 → 0.2.330
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/index.js +722 -302
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2520,17 +2520,10 @@ import { cva, type VariantProps } from "class-variance-authority";
|
|
|
2520
2520
|
import { ChevronLeft, ChevronRight, Clock2, X } from "lucide-react";
|
|
2521
2521
|
|
|
2522
2522
|
import { cn } from "@/lib/utils";
|
|
2523
|
-
import {
|
|
2524
|
-
Select,
|
|
2525
|
-
SelectContent,
|
|
2526
|
-
SelectItem,
|
|
2527
|
-
SelectTrigger,
|
|
2528
|
-
SelectValue,
|
|
2529
|
-
} from "./select";
|
|
2530
2523
|
|
|
2531
2524
|
const DEFAULT_START_TIME = "10:30:00";
|
|
2532
2525
|
const DEFAULT_END_TIME = "12:30:00";
|
|
2533
|
-
const DEFAULT_PLACEHOLDER = "
|
|
2526
|
+
const DEFAULT_PLACEHOLDER = "--/--/---- --:-- --";
|
|
2534
2527
|
const POPOVER_WIDTH = 336;
|
|
2535
2528
|
const POPOVER_MARGIN = 8;
|
|
2536
2529
|
const POPOVER_GAP = 4;
|
|
@@ -2540,23 +2533,8 @@ const POPOVER_WIDTH_VAR = "--date-time-picker-popover-width";
|
|
|
2540
2533
|
const CALENDAR_PLACEMENT: Placement = "bottom-start";
|
|
2541
2534
|
const YEAR_RANGE_BEFORE = 100;
|
|
2542
2535
|
const YEAR_RANGE_AFTER = 10;
|
|
2543
|
-
const
|
|
2544
|
-
"[
|
|
2545
|
-
const CALENDAR_SELECT_TRIGGER_SELECTOR =
|
|
2546
|
-
"[data-date-time-picker-calendar-select-trigger]";
|
|
2547
|
-
const CALENDAR_SELECT_TRIGGER_CLASS = "!w-[90px] !min-w-[90px] !gap-[6px] !px-3";
|
|
2548
|
-
const CALENDAR_SELECT_CONTENT_CLASS =
|
|
2549
|
-
"z-[10060] w-[var(--radix-select-trigger-width)] min-w-[var(--radix-select-trigger-width)] max-h-[min(16rem,var(--radix-select-content-available-height))]";
|
|
2550
|
-
type CalendarSelect = "month" | "year";
|
|
2551
|
-
const DATE_TIME_INPUT_SEGMENT_RANGES = {
|
|
2552
|
-
day: [0, 2],
|
|
2553
|
-
month: [3, 5],
|
|
2554
|
-
year: [6, 10],
|
|
2555
|
-
hour: [11, 13],
|
|
2556
|
-
minute: [14, 16],
|
|
2557
|
-
meridiem: [17, 19],
|
|
2558
|
-
} as const;
|
|
2559
|
-
|
|
2536
|
+
const CALENDAR_DROPDOWN_TRIGGER_CLASS =
|
|
2537
|
+
"h-9 min-w-[90px] rounded-md border border-solid border-semantic-border-input bg-semantic-bg-primary px-3 text-sm text-semantic-text-primary outline-none transition-colors hover:border-semantic-border-input-focus/50 focus:border-semantic-border-input-focus/50 focus:shadow-[0_0_0_1px_rgba(43,188,202,0.15)]";
|
|
2560
2538
|
const weekDays = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
|
|
2561
2539
|
const monthNames = Array.from({ length: 12 }, (_, monthIndex) =>
|
|
2562
2540
|
new Intl.DateTimeFormat("en-US", { month: "short" }).format(
|
|
@@ -2570,6 +2548,11 @@ const monthFormatter = new Intl.DateTimeFormat("en-US", {
|
|
|
2570
2548
|
|
|
2571
2549
|
const dateTimePickerVariants = cva("relative inline-block w-full max-w-full", {
|
|
2572
2550
|
variants: {
|
|
2551
|
+
variant: {
|
|
2552
|
+
"date-time": "",
|
|
2553
|
+
"date-only": "",
|
|
2554
|
+
"time-only": "",
|
|
2555
|
+
},
|
|
2573
2556
|
size: {
|
|
2574
2557
|
sm: "sm:w-[280px]",
|
|
2575
2558
|
default: "sm:w-[336px]",
|
|
@@ -2577,6 +2560,7 @@ const dateTimePickerVariants = cva("relative inline-block w-full max-w-full", {
|
|
|
2577
2560
|
},
|
|
2578
2561
|
},
|
|
2579
2562
|
defaultVariants: {
|
|
2563
|
+
variant: "date-time",
|
|
2580
2564
|
size: "default",
|
|
2581
2565
|
},
|
|
2582
2566
|
});
|
|
@@ -2622,6 +2606,7 @@ export interface DateTimePickerProps
|
|
|
2622
2606
|
readOnly?: boolean;
|
|
2623
2607
|
name?: string;
|
|
2624
2608
|
showEndTime?: boolean;
|
|
2609
|
+
showSeconds?: boolean;
|
|
2625
2610
|
showClear?: boolean;
|
|
2626
2611
|
closeOnSelect?: boolean;
|
|
2627
2612
|
startTimeLabel?: string;
|
|
@@ -2635,6 +2620,17 @@ export interface DateTimePickerProps
|
|
|
2635
2620
|
portalContainer?: HTMLElement | null;
|
|
2636
2621
|
}
|
|
2637
2622
|
|
|
2623
|
+
type DateTimePickerVariant = NonNullable<
|
|
2624
|
+
VariantProps<typeof dateTimePickerVariants>["variant"]
|
|
2625
|
+
>;
|
|
2626
|
+
type CalendarDropdownKind = "month" | "year";
|
|
2627
|
+
|
|
2628
|
+
interface CalendarDropdownOption {
|
|
2629
|
+
value: string;
|
|
2630
|
+
label: string;
|
|
2631
|
+
disabled?: boolean;
|
|
2632
|
+
}
|
|
2633
|
+
|
|
2638
2634
|
function normalizeValue(
|
|
2639
2635
|
value?: Partial<DateTimePickerValue>
|
|
2640
2636
|
): DateTimePickerValue {
|
|
@@ -2694,6 +2690,13 @@ function clampMonth(date: Date, minDate?: Date, maxDate?: Date) {
|
|
|
2694
2690
|
return startOfMonth(date);
|
|
2695
2691
|
}
|
|
2696
2692
|
|
|
2693
|
+
function isSelectableDay(date: Date, minDate?: Date, maxDate?: Date) {
|
|
2694
|
+
return (
|
|
2695
|
+
!(minDate && isBeforeDay(date, minDate)) &&
|
|
2696
|
+
!(maxDate && isAfterDay(date, maxDate))
|
|
2697
|
+
);
|
|
2698
|
+
}
|
|
2699
|
+
|
|
2697
2700
|
function getYearOptions(visibleMonth: Date, minDate?: Date, maxDate?: Date) {
|
|
2698
2701
|
const currentYear = new Date().getFullYear();
|
|
2699
2702
|
const selectedYear = visibleMonth.getFullYear();
|
|
@@ -2742,32 +2745,84 @@ function isPointerInsideElement(
|
|
|
2742
2745
|
return false;
|
|
2743
2746
|
}
|
|
2744
2747
|
|
|
2745
|
-
function
|
|
2746
|
-
const
|
|
2747
|
-
|
|
2748
|
-
return target instanceof Element && target.closest(selector) !== null;
|
|
2748
|
+
function timeHasVisibleSeconds(time?: string) {
|
|
2749
|
+
const [, , second = "00"] = (time ?? "").split(":");
|
|
2750
|
+
return /^\\d{1,2}$/.test(second) && Number(second) !== 0;
|
|
2749
2751
|
}
|
|
2750
2752
|
|
|
2751
|
-
function formatTimeForDisplay(time: string) {
|
|
2752
|
-
const [hour = "0", minute = "0"] = time.split(":");
|
|
2753
|
+
function formatTimeForDisplay(time: string, showSeconds = false) {
|
|
2754
|
+
const [hour = "0", minute = "0", second = "00"] = time.split(":");
|
|
2753
2755
|
const hourNumber = Number(hour);
|
|
2754
2756
|
const suffix = hourNumber >= 12 ? "PM" : "AM";
|
|
2755
2757
|
const hour12 = hourNumber % 12 || 12;
|
|
2758
|
+
const normalizedSecond = second.padStart(2, "0");
|
|
2759
|
+
const formattedTime = showSeconds
|
|
2760
|
+
? \`\${hour12.toString().padStart(2, "0")}:\${minute.padStart(2, "0")}:\${normalizedSecond}\`
|
|
2761
|
+
: \`\${hour12.toString().padStart(2, "0")}:\${minute.padStart(2, "0")}\`;
|
|
2756
2762
|
|
|
2757
|
-
return \`\${
|
|
2763
|
+
return \`\${formattedTime} \${suffix}\`;
|
|
2758
2764
|
}
|
|
2759
2765
|
|
|
2760
|
-
function formatDateForDisplay(date?: Date, time?: string) {
|
|
2766
|
+
function formatDateForDisplay(date?: Date, time?: string, showSeconds = false) {
|
|
2761
2767
|
if (!date) return "";
|
|
2762
2768
|
|
|
2763
2769
|
const month = (date.getMonth() + 1).toString().padStart(2, "0");
|
|
2764
2770
|
const day = date.getDate().toString().padStart(2, "0");
|
|
2765
2771
|
const year = date.getFullYear();
|
|
2766
|
-
const formattedTime = formatTimeForDisplay(
|
|
2772
|
+
const formattedTime = formatTimeForDisplay(
|
|
2773
|
+
time ?? DEFAULT_START_TIME,
|
|
2774
|
+
showSeconds
|
|
2775
|
+
);
|
|
2767
2776
|
|
|
2768
2777
|
return \`\${day}/\${month}/\${year} \${formattedTime}\`;
|
|
2769
2778
|
}
|
|
2770
2779
|
|
|
2780
|
+
function formatDateOnlyForDisplay(date?: Date) {
|
|
2781
|
+
if (!date) return "";
|
|
2782
|
+
|
|
2783
|
+
const month = (date.getMonth() + 1).toString().padStart(2, "0");
|
|
2784
|
+
const day = date.getDate().toString().padStart(2, "0");
|
|
2785
|
+
const year = date.getFullYear();
|
|
2786
|
+
|
|
2787
|
+
return \`\${day}/\${month}/\${year}\`;
|
|
2788
|
+
}
|
|
2789
|
+
|
|
2790
|
+
function formatValueForDisplay(
|
|
2791
|
+
value: DateTimePickerValue,
|
|
2792
|
+
variant: DateTimePickerVariant,
|
|
2793
|
+
showEndTime: boolean,
|
|
2794
|
+
hasTimeValue: boolean,
|
|
2795
|
+
showSeconds: boolean
|
|
2796
|
+
) {
|
|
2797
|
+
if (variant === "date-only") {
|
|
2798
|
+
return formatDateOnlyForDisplay(value.date);
|
|
2799
|
+
}
|
|
2800
|
+
|
|
2801
|
+
if (variant === "time-only") {
|
|
2802
|
+
if (!hasTimeValue) return "";
|
|
2803
|
+
|
|
2804
|
+
return showEndTime
|
|
2805
|
+
? \`\${formatTimeForDisplay(value.startTime, showSeconds)} - \${formatTimeForDisplay(value.endTime, showSeconds)}\`
|
|
2806
|
+
: formatTimeForDisplay(value.startTime, showSeconds);
|
|
2807
|
+
}
|
|
2808
|
+
|
|
2809
|
+
return formatDateForDisplay(value.date, value.startTime, showSeconds);
|
|
2810
|
+
}
|
|
2811
|
+
|
|
2812
|
+
function getDefaultPlaceholder(
|
|
2813
|
+
variant: DateTimePickerVariant,
|
|
2814
|
+
showSeconds: boolean
|
|
2815
|
+
) {
|
|
2816
|
+
if (variant === "date-only") return "--/--/----";
|
|
2817
|
+
if (variant === "time-only") {
|
|
2818
|
+
return showSeconds ? "--:--:-- --" : "--:-- --";
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2821
|
+
if (showSeconds) return "--/--/---- --:--:-- --";
|
|
2822
|
+
|
|
2823
|
+
return DEFAULT_PLACEHOLDER;
|
|
2824
|
+
}
|
|
2825
|
+
|
|
2771
2826
|
function parseDatePart(datePart: string) {
|
|
2772
2827
|
const isoMatch = datePart.match(/^(\\d{4})-(\\d{1,2})-(\\d{1,2})$/);
|
|
2773
2828
|
const dayFirstMatch = datePart.match(/^(\\d{1,2})[/-](\\d{1,2})[/-](\\d{4})$/);
|
|
@@ -2979,16 +3034,20 @@ function splitTypedDateInput(value: string) {
|
|
|
2979
3034
|
};
|
|
2980
3035
|
}
|
|
2981
3036
|
|
|
2982
|
-
function isPotentiallyValidTimeDigits(timeDigits: string) {
|
|
3037
|
+
function isPotentiallyValidTimeDigits(timeDigits: string, showSeconds: boolean) {
|
|
2983
3038
|
const hourValue = timeDigits.slice(0, 2);
|
|
2984
3039
|
const minuteValue = timeDigits.slice(2, 4);
|
|
3040
|
+
const secondValue = showSeconds ? timeDigits.slice(4, 6) : "";
|
|
2985
3041
|
const hour = Number(hourValue);
|
|
2986
3042
|
const minute = Number(minuteValue);
|
|
3043
|
+
const second = Number(secondValue);
|
|
2987
3044
|
|
|
2988
3045
|
if (hourValue.length === 1 && hour > 1) return false;
|
|
2989
3046
|
if (hourValue.length === 2 && (hour < 1 || hour > 12)) return false;
|
|
2990
3047
|
if (minuteValue.length === 1 && minute > 5) return false;
|
|
2991
3048
|
if (minuteValue.length === 2 && minute > 59) return false;
|
|
3049
|
+
if (showSeconds && secondValue.length === 1 && second > 5) return false;
|
|
3050
|
+
if (showSeconds && secondValue.length === 2 && second > 59) return false;
|
|
2992
3051
|
|
|
2993
3052
|
return true;
|
|
2994
3053
|
}
|
|
@@ -3001,26 +3060,65 @@ function formatMeridiemInput(letters: string) {
|
|
|
3001
3060
|
return null;
|
|
3002
3061
|
}
|
|
3003
3062
|
|
|
3004
|
-
function formatTimeInput(restValue: string) {
|
|
3063
|
+
function formatTimeInput(restValue: string, showSeconds: boolean) {
|
|
3005
3064
|
const normalizedValue = restValue.toUpperCase();
|
|
3006
|
-
const
|
|
3065
|
+
const maxTimeDigits = showSeconds ? 6 : 4;
|
|
3066
|
+
const timeDigits = normalizedValue.replace(/\\D/g, "").slice(0, maxTimeDigits);
|
|
3007
3067
|
const meridiemLetters = normalizedValue.replace(/[^A-Z]/g, "");
|
|
3008
3068
|
const meridiem = formatMeridiemInput(meridiemLetters.slice(0, 2));
|
|
3009
3069
|
|
|
3010
3070
|
if (!timeDigits && !meridiem) return "";
|
|
3011
|
-
if (!isPotentiallyValidTimeDigits(timeDigits) || meridiem === null) {
|
|
3071
|
+
if (!isPotentiallyValidTimeDigits(timeDigits, showSeconds) || meridiem === null) {
|
|
3012
3072
|
return null;
|
|
3013
3073
|
}
|
|
3014
3074
|
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3075
|
+
let timeValue = timeDigits.slice(0, 2);
|
|
3076
|
+
if (timeDigits.length > 2) {
|
|
3077
|
+
timeValue = \`\${timeValue}:\${timeDigits.slice(2, 4)}\`;
|
|
3078
|
+
}
|
|
3079
|
+
if (showSeconds && timeDigits.length > 4) {
|
|
3080
|
+
timeValue = \`\${timeValue}:\${timeDigits.slice(4, 6)}\`;
|
|
3081
|
+
}
|
|
3019
3082
|
|
|
3020
3083
|
return [timeValue, meridiem].filter(Boolean).join(" ");
|
|
3021
3084
|
}
|
|
3022
3085
|
|
|
3023
|
-
function
|
|
3086
|
+
function sanitizeTypedDateInput(value: string, previousValue: string) {
|
|
3087
|
+
const trimmedValue = value.trimStart();
|
|
3088
|
+
if (/^[A-Za-z]/.test(trimmedValue)) return previousValue;
|
|
3089
|
+
|
|
3090
|
+
const normalizedValue = value.toUpperCase().replace(/[^0-9\\s/-]/g, "");
|
|
3091
|
+
const { dateDigits, dateValue, isValid } = splitTypedDateInput(normalizedValue);
|
|
3092
|
+
const limitedDateDigits = dateDigits.slice(0, 8);
|
|
3093
|
+
|
|
3094
|
+
if (!limitedDateDigits) return "";
|
|
3095
|
+
if (!isValid) return previousValue;
|
|
3096
|
+
|
|
3097
|
+
return dateValue;
|
|
3098
|
+
}
|
|
3099
|
+
|
|
3100
|
+
function sanitizeTypedTimeInput(
|
|
3101
|
+
value: string,
|
|
3102
|
+
previousValue: string,
|
|
3103
|
+
showSeconds: boolean
|
|
3104
|
+
) {
|
|
3105
|
+
const trimmedValue = value.trimStart();
|
|
3106
|
+
if (/^[A-Za-z]/.test(trimmedValue) && !/^[AP]/i.test(trimmedValue)) {
|
|
3107
|
+
return previousValue;
|
|
3108
|
+
}
|
|
3109
|
+
|
|
3110
|
+
const normalizedValue = value.toUpperCase().replace(/[^0-9\\s:APM]/g, "");
|
|
3111
|
+
const formattedTime = formatTimeInput(normalizedValue, showSeconds);
|
|
3112
|
+
if (formattedTime === null) return previousValue;
|
|
3113
|
+
|
|
3114
|
+
return formattedTime;
|
|
3115
|
+
}
|
|
3116
|
+
|
|
3117
|
+
function sanitizeTypedDateTimeInput(
|
|
3118
|
+
value: string,
|
|
3119
|
+
previousValue: string,
|
|
3120
|
+
showSeconds: boolean
|
|
3121
|
+
) {
|
|
3024
3122
|
const trimmedValue = value.trimStart();
|
|
3025
3123
|
if (/^[A-Za-z]/.test(trimmedValue)) return previousValue;
|
|
3026
3124
|
|
|
@@ -3034,22 +3132,59 @@ function sanitizeTypedDateTimeInput(value: string, previousValue: string) {
|
|
|
3034
3132
|
if (!limitedDateDigits) return "";
|
|
3035
3133
|
if (!isValid) return previousValue;
|
|
3036
3134
|
|
|
3037
|
-
const formattedTime = isComplete ? formatTimeInput(restValue) : "";
|
|
3135
|
+
const formattedTime = isComplete ? formatTimeInput(restValue, showSeconds) : "";
|
|
3038
3136
|
if (formattedTime === null) return previousValue;
|
|
3039
3137
|
|
|
3040
3138
|
return [dateValue, formattedTime].filter(Boolean).join(" ");
|
|
3041
3139
|
}
|
|
3042
3140
|
|
|
3043
|
-
type DateTimeInputSegment =
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3141
|
+
type DateTimeInputSegment =
|
|
3142
|
+
| "day"
|
|
3143
|
+
| "month"
|
|
3144
|
+
| "year"
|
|
3145
|
+
| "hour"
|
|
3146
|
+
| "minute"
|
|
3147
|
+
| "second"
|
|
3148
|
+
| "meridiem";
|
|
3149
|
+
|
|
3150
|
+
function getDateTimeInputSegmentRanges(showSeconds: boolean) {
|
|
3151
|
+
return showSeconds
|
|
3152
|
+
? ({
|
|
3153
|
+
day: [0, 2],
|
|
3154
|
+
month: [3, 5],
|
|
3155
|
+
year: [6, 10],
|
|
3156
|
+
hour: [11, 13],
|
|
3157
|
+
minute: [14, 16],
|
|
3158
|
+
second: [17, 19],
|
|
3159
|
+
meridiem: [20, 22],
|
|
3160
|
+
} satisfies Record<DateTimeInputSegment, readonly [number, number]>)
|
|
3161
|
+
: ({
|
|
3162
|
+
day: [0, 2],
|
|
3163
|
+
month: [3, 5],
|
|
3164
|
+
year: [6, 10],
|
|
3165
|
+
hour: [11, 13],
|
|
3166
|
+
minute: [14, 16],
|
|
3167
|
+
second: [17, 19],
|
|
3168
|
+
meridiem: [17, 19],
|
|
3169
|
+
} satisfies Record<DateTimeInputSegment, readonly [number, number]>);
|
|
3170
|
+
}
|
|
3171
|
+
|
|
3172
|
+
function getDateTimeInputSegment(
|
|
3173
|
+
cursorPosition: number,
|
|
3174
|
+
showSeconds: boolean
|
|
3175
|
+
): DateTimeInputSegment {
|
|
3176
|
+
const ranges = getDateTimeInputSegmentRanges(showSeconds);
|
|
3177
|
+
|
|
3178
|
+
if (cursorPosition <= ranges.day[1]) return "day";
|
|
3179
|
+
if (cursorPosition <= ranges.month[1]) return "month";
|
|
3180
|
+
if (cursorPosition <= ranges.year[1]) return "year";
|
|
3181
|
+
if (cursorPosition <= ranges.hour[1]) return "hour";
|
|
3182
|
+
if (cursorPosition <= ranges.minute[1]) {
|
|
3051
3183
|
return "minute";
|
|
3052
3184
|
}
|
|
3185
|
+
if (showSeconds && cursorPosition <= ranges.second[1]) {
|
|
3186
|
+
return "second";
|
|
3187
|
+
}
|
|
3053
3188
|
|
|
3054
3189
|
return "meridiem";
|
|
3055
3190
|
}
|
|
@@ -3062,12 +3197,13 @@ function stepDateTimeInputValue(
|
|
|
3062
3197
|
value: string,
|
|
3063
3198
|
cursorPosition: number,
|
|
3064
3199
|
direction: 1 | -1,
|
|
3065
|
-
fallbackTime: string
|
|
3200
|
+
fallbackTime: string,
|
|
3201
|
+
showSeconds: boolean
|
|
3066
3202
|
) {
|
|
3067
3203
|
const typedDateTime = parseTypedDateTime(value);
|
|
3068
3204
|
if (!typedDateTime) return null;
|
|
3069
3205
|
|
|
3070
|
-
const segment = getDateTimeInputSegment(cursorPosition);
|
|
3206
|
+
const segment = getDateTimeInputSegment(cursorPosition, showSeconds);
|
|
3071
3207
|
const nextDate = new Date(typedDateTime.date);
|
|
3072
3208
|
const [hourValue = "0", minuteValue = "0", secondValue = "00"] = (
|
|
3073
3209
|
typedDateTime.startTime ?? fallbackTime
|
|
@@ -3104,6 +3240,8 @@ function stepDateTimeInputValue(
|
|
|
3104
3240
|
nextDate.setHours(nextDate.getHours() + direction);
|
|
3105
3241
|
} else if (segment === "minute") {
|
|
3106
3242
|
nextDate.setMinutes(nextDate.getMinutes() + direction);
|
|
3243
|
+
} else if (segment === "second") {
|
|
3244
|
+
nextDate.setSeconds(nextDate.getSeconds() + direction);
|
|
3107
3245
|
} else {
|
|
3108
3246
|
nextDate.setHours(nextDate.getHours() + 12 * direction);
|
|
3109
3247
|
}
|
|
@@ -3126,6 +3264,34 @@ function stepDateTimeInputValue(
|
|
|
3126
3264
|
};
|
|
3127
3265
|
}
|
|
3128
3266
|
|
|
3267
|
+
function formatTimeForTimeInput(time: string, showSeconds: boolean) {
|
|
3268
|
+
const normalizedTime = parseTimePart(time) ?? DEFAULT_START_TIME;
|
|
3269
|
+
const [hour = "00", minute = "00", second = "00"] = normalizedTime.split(":");
|
|
3270
|
+
|
|
3271
|
+
return showSeconds ? \`\${hour}:\${minute}:\${second}\` : \`\${hour}:\${minute}\`;
|
|
3272
|
+
}
|
|
3273
|
+
|
|
3274
|
+
function normalizeTimeInputValue(time: string, fallbackTime: string) {
|
|
3275
|
+
return parseTimePart(time) ?? parseTimePart(fallbackTime) ?? DEFAULT_START_TIME;
|
|
3276
|
+
}
|
|
3277
|
+
|
|
3278
|
+
function openNativeTimePicker(input: HTMLInputElement | null) {
|
|
3279
|
+
if (!input) return;
|
|
3280
|
+
|
|
3281
|
+
input.focus();
|
|
3282
|
+
|
|
3283
|
+
const showPicker = (
|
|
3284
|
+
input as HTMLInputElement & { showPicker?: () => void }
|
|
3285
|
+
).showPicker;
|
|
3286
|
+
if (!showPicker) return;
|
|
3287
|
+
|
|
3288
|
+
try {
|
|
3289
|
+
showPicker.call(input);
|
|
3290
|
+
} catch {
|
|
3291
|
+
input.focus();
|
|
3292
|
+
}
|
|
3293
|
+
}
|
|
3294
|
+
|
|
3129
3295
|
function parseTypedDateTime(value: string) {
|
|
3130
3296
|
const typedValue = value.trim();
|
|
3131
3297
|
if (!typedValue) return undefined;
|
|
@@ -3144,9 +3310,40 @@ function parseTypedDateTime(value: string) {
|
|
|
3144
3310
|
return { date, startTime };
|
|
3145
3311
|
}
|
|
3146
3312
|
|
|
3147
|
-
function
|
|
3313
|
+
function parseTypedDate(value: string) {
|
|
3314
|
+
const typedValue = value.trim();
|
|
3315
|
+
if (!typedValue) return undefined;
|
|
3316
|
+
|
|
3317
|
+
const typedMatch = typedValue.match(
|
|
3318
|
+
/^(\\d{4}-\\d{1,2}-\\d{1,2}|\\d{1,2}[/-]\\d{1,2}[/-]\\d{4})$/
|
|
3319
|
+
);
|
|
3320
|
+
if (!typedMatch) return null;
|
|
3321
|
+
|
|
3322
|
+
return parseDatePart(typedValue);
|
|
3323
|
+
}
|
|
3324
|
+
|
|
3325
|
+
function formatHiddenValue(
|
|
3326
|
+
value: DateTimePickerValue,
|
|
3327
|
+
variant: DateTimePickerVariant,
|
|
3328
|
+
showEndTime: boolean,
|
|
3329
|
+
hasTimeValue: boolean
|
|
3330
|
+
) {
|
|
3331
|
+
if (variant === "time-only") {
|
|
3332
|
+
if (!hasTimeValue) return "";
|
|
3333
|
+
|
|
3334
|
+
return showEndTime ? \`\${value.startTime}/\${value.endTime}\` : value.startTime;
|
|
3335
|
+
}
|
|
3336
|
+
|
|
3148
3337
|
if (!value.date) return "";
|
|
3149
3338
|
|
|
3339
|
+
if (variant === "date-only") {
|
|
3340
|
+
const month = (value.date.getMonth() + 1).toString().padStart(2, "0");
|
|
3341
|
+
const day = value.date.getDate().toString().padStart(2, "0");
|
|
3342
|
+
const year = value.date.getFullYear();
|
|
3343
|
+
|
|
3344
|
+
return \`\${year}-\${month}-\${day}\`;
|
|
3345
|
+
}
|
|
3346
|
+
|
|
3150
3347
|
const month = (value.date.getMonth() + 1).toString().padStart(2, "0");
|
|
3151
3348
|
const day = value.date.getDate().toString().padStart(2, "0");
|
|
3152
3349
|
const year = value.date.getFullYear();
|
|
@@ -3174,20 +3371,101 @@ function FigmaCalendarIcon({ className }: { className?: string }) {
|
|
|
3174
3371
|
);
|
|
3175
3372
|
}
|
|
3176
3373
|
|
|
3374
|
+
function CalendarDropdown({
|
|
3375
|
+
id,
|
|
3376
|
+
label,
|
|
3377
|
+
value,
|
|
3378
|
+
options,
|
|
3379
|
+
open,
|
|
3380
|
+
onOpenChange,
|
|
3381
|
+
onValueChange,
|
|
3382
|
+
}: {
|
|
3383
|
+
id: string;
|
|
3384
|
+
label: string;
|
|
3385
|
+
value: string;
|
|
3386
|
+
options: CalendarDropdownOption[];
|
|
3387
|
+
open: boolean;
|
|
3388
|
+
onOpenChange: (open: boolean) => void;
|
|
3389
|
+
onValueChange: (value: string) => void;
|
|
3390
|
+
}) {
|
|
3391
|
+
const selectedOption = options.find((option) => option.value === value);
|
|
3392
|
+
|
|
3393
|
+
return (
|
|
3394
|
+
<div className="relative">
|
|
3395
|
+
<label className="sr-only" htmlFor={id}>
|
|
3396
|
+
{label}
|
|
3397
|
+
</label>
|
|
3398
|
+
<button
|
|
3399
|
+
id={id}
|
|
3400
|
+
type="button"
|
|
3401
|
+
role="combobox"
|
|
3402
|
+
aria-label={label}
|
|
3403
|
+
aria-expanded={open}
|
|
3404
|
+
aria-controls={\`\${id}-options\`}
|
|
3405
|
+
data-value={value}
|
|
3406
|
+
className={cn(
|
|
3407
|
+
CALENDAR_DROPDOWN_TRIGGER_CLASS,
|
|
3408
|
+
"inline-flex items-center justify-between gap-2"
|
|
3409
|
+
)}
|
|
3410
|
+
onClick={() => onOpenChange(!open)}
|
|
3411
|
+
>
|
|
3412
|
+
<span>{selectedOption?.label ?? value}</span>
|
|
3413
|
+
<ChevronRight
|
|
3414
|
+
className={cn("size-3 transition-transform", open && "-rotate-90")}
|
|
3415
|
+
aria-hidden="true"
|
|
3416
|
+
/>
|
|
3417
|
+
</button>
|
|
3418
|
+
{open && (
|
|
3419
|
+
<div
|
|
3420
|
+
id={\`\${id}-options\`}
|
|
3421
|
+
role="listbox"
|
|
3422
|
+
aria-label={\`\${label} options\`}
|
|
3423
|
+
className="absolute left-0 top-full z-10 mt-1 max-h-52 min-w-full overflow-y-auto rounded-md border border-solid border-semantic-border-layout bg-semantic-bg-primary p-1 shadow-lg"
|
|
3424
|
+
>
|
|
3425
|
+
{options.map((option) => (
|
|
3426
|
+
<button
|
|
3427
|
+
key={option.value}
|
|
3428
|
+
type="button"
|
|
3429
|
+
role="option"
|
|
3430
|
+
aria-label={option.label}
|
|
3431
|
+
aria-selected={option.value === value}
|
|
3432
|
+
disabled={option.disabled}
|
|
3433
|
+
className={cn(
|
|
3434
|
+
"flex w-full items-center rounded px-2 py-1.5 text-left text-sm text-semantic-text-primary transition-colors hover:bg-semantic-bg-hover disabled:cursor-not-allowed disabled:opacity-40",
|
|
3435
|
+
option.value === value && "bg-semantic-primary text-semantic-text-inverted"
|
|
3436
|
+
)}
|
|
3437
|
+
onClick={() => {
|
|
3438
|
+
if (option.disabled) return;
|
|
3439
|
+
|
|
3440
|
+
onValueChange(option.value);
|
|
3441
|
+
onOpenChange(false);
|
|
3442
|
+
}}
|
|
3443
|
+
>
|
|
3444
|
+
{option.label}
|
|
3445
|
+
</button>
|
|
3446
|
+
))}
|
|
3447
|
+
</div>
|
|
3448
|
+
)}
|
|
3449
|
+
</div>
|
|
3450
|
+
);
|
|
3451
|
+
}
|
|
3452
|
+
|
|
3177
3453
|
const DateTimePicker = React.forwardRef<HTMLDivElement, DateTimePickerProps>(
|
|
3178
3454
|
(
|
|
3179
3455
|
{
|
|
3180
3456
|
className,
|
|
3457
|
+
variant,
|
|
3181
3458
|
size,
|
|
3182
3459
|
state,
|
|
3183
3460
|
value,
|
|
3184
3461
|
defaultValue,
|
|
3185
3462
|
onValueChange,
|
|
3186
|
-
placeholder
|
|
3463
|
+
placeholder: placeholderProp,
|
|
3187
3464
|
disabled = false,
|
|
3188
3465
|
readOnly = false,
|
|
3189
3466
|
name,
|
|
3190
3467
|
showEndTime = true,
|
|
3468
|
+
showSeconds,
|
|
3191
3469
|
showClear = true,
|
|
3192
3470
|
closeOnSelect = false,
|
|
3193
3471
|
startTimeLabel = "Start Time",
|
|
@@ -3206,28 +3484,52 @@ const DateTimePicker = React.forwardRef<HTMLDivElement, DateTimePickerProps>(
|
|
|
3206
3484
|
) => {
|
|
3207
3485
|
const generatedId = React.useId();
|
|
3208
3486
|
const triggerId = id ?? generatedId;
|
|
3487
|
+
const pickerVariant = variant ?? "date-time";
|
|
3488
|
+
const showCalendar = pickerVariant !== "time-only";
|
|
3489
|
+
const showTimeFields = pickerVariant !== "date-only";
|
|
3490
|
+
const resolvedShowEndTime = showTimeFields && showEndTime;
|
|
3209
3491
|
const isValueControlled = value !== undefined;
|
|
3210
3492
|
const isOpenControlled = controlledOpen !== undefined;
|
|
3211
3493
|
const [internalValue, setInternalValue] = React.useState(() =>
|
|
3212
3494
|
normalizeValue(defaultValue)
|
|
3213
3495
|
);
|
|
3496
|
+
const [internalHasTimeValue, setInternalHasTimeValue] = React.useState(() =>
|
|
3497
|
+
Boolean(defaultValue?.date || defaultValue?.startTime || defaultValue?.endTime)
|
|
3498
|
+
);
|
|
3214
3499
|
const [internalOpen, setInternalOpen] = React.useState(defaultOpen);
|
|
3215
3500
|
const currentValue = normalizeValue(
|
|
3216
3501
|
isValueControlled ? value : internalValue
|
|
3217
3502
|
);
|
|
3503
|
+
const hasTimeValue = isValueControlled
|
|
3504
|
+
? Boolean(value?.date || value?.startTime || value?.endTime)
|
|
3505
|
+
: internalHasTimeValue;
|
|
3506
|
+
const resolvedShowSeconds =
|
|
3507
|
+
showSeconds ??
|
|
3508
|
+
(timeHasVisibleSeconds(currentValue.startTime) ||
|
|
3509
|
+
(resolvedShowEndTime && timeHasVisibleSeconds(currentValue.endTime)));
|
|
3510
|
+
const placeholder =
|
|
3511
|
+
placeholderProp ?? getDefaultPlaceholder(pickerVariant, resolvedShowSeconds);
|
|
3218
3512
|
const open = isOpenControlled ? controlledOpen : internalOpen;
|
|
3219
3513
|
const [visibleMonth, setVisibleMonth] = React.useState(() =>
|
|
3220
3514
|
startOfMonth(currentValue.date ?? new Date())
|
|
3221
3515
|
);
|
|
3222
3516
|
const [dateInputValue, setDateInputValue] = React.useState(() =>
|
|
3223
|
-
|
|
3517
|
+
formatValueForDisplay(
|
|
3518
|
+
currentValue,
|
|
3519
|
+
pickerVariant,
|
|
3520
|
+
resolvedShowEndTime,
|
|
3521
|
+
hasTimeValue,
|
|
3522
|
+
resolvedShowSeconds
|
|
3523
|
+
)
|
|
3224
3524
|
);
|
|
3225
3525
|
const [isDateInputFocused, setIsDateInputFocused] = React.useState(false);
|
|
3226
|
-
const [openCalendarSelect, setOpenCalendarSelect] =
|
|
3227
|
-
React.useState<CalendarSelect | null>(null);
|
|
3228
3526
|
const rootRef = React.useRef<HTMLDivElement | null>(null);
|
|
3229
3527
|
const triggerRef = React.useRef<HTMLDivElement | null>(null);
|
|
3230
3528
|
const popoverRef = React.useRef<HTMLDivElement | null>(null);
|
|
3529
|
+
const startTimeInputRef = React.useRef<HTMLInputElement | null>(null);
|
|
3530
|
+
const endTimeInputRef = React.useRef<HTMLInputElement | null>(null);
|
|
3531
|
+
const [openCalendarDropdown, setOpenCalendarDropdown] =
|
|
3532
|
+
React.useState<CalendarDropdownKind | null>(null);
|
|
3231
3533
|
const usesContainerPortal = portalContainer !== undefined;
|
|
3232
3534
|
const floatingStrategy: Strategy = usesContainerPortal
|
|
3233
3535
|
? "absolute"
|
|
@@ -3270,9 +3572,12 @@ const DateTimePicker = React.forwardRef<HTMLDivElement, DateTimePickerProps>(
|
|
|
3270
3572
|
() => getCalendarDays(visibleMonth),
|
|
3271
3573
|
[visibleMonth]
|
|
3272
3574
|
);
|
|
3273
|
-
const displayValue =
|
|
3274
|
-
currentValue
|
|
3275
|
-
|
|
3575
|
+
const displayValue = formatValueForDisplay(
|
|
3576
|
+
currentValue,
|
|
3577
|
+
pickerVariant,
|
|
3578
|
+
resolvedShowEndTime,
|
|
3579
|
+
hasTimeValue,
|
|
3580
|
+
resolvedShowSeconds
|
|
3276
3581
|
);
|
|
3277
3582
|
const effectiveMinDate = React.useMemo(() => {
|
|
3278
3583
|
if (!disablePastDates) return minDate;
|
|
@@ -3291,12 +3596,12 @@ const DateTimePicker = React.forwardRef<HTMLDivElement, DateTimePickerProps>(
|
|
|
3291
3596
|
|
|
3292
3597
|
const setOpen = React.useCallback(
|
|
3293
3598
|
(nextOpen: boolean) => {
|
|
3294
|
-
if (!
|
|
3295
|
-
|
|
3599
|
+
if (!nextOpen) {
|
|
3600
|
+
setOpenCalendarDropdown(null);
|
|
3296
3601
|
}
|
|
3297
3602
|
|
|
3298
|
-
if (!
|
|
3299
|
-
|
|
3603
|
+
if (!isOpenControlled) {
|
|
3604
|
+
setInternalOpen(nextOpen);
|
|
3300
3605
|
}
|
|
3301
3606
|
|
|
3302
3607
|
onOpenChange?.(nextOpen);
|
|
@@ -3304,30 +3609,6 @@ const DateTimePicker = React.forwardRef<HTMLDivElement, DateTimePickerProps>(
|
|
|
3304
3609
|
[isOpenControlled, onOpenChange]
|
|
3305
3610
|
);
|
|
3306
3611
|
|
|
3307
|
-
const handleCalendarSelectOpenChange = React.useCallback(
|
|
3308
|
-
(select: CalendarSelect, nextOpen: boolean) => {
|
|
3309
|
-
setOpenCalendarSelect((currentSelect) => {
|
|
3310
|
-
if (nextOpen) return select;
|
|
3311
|
-
|
|
3312
|
-
return currentSelect === select ? null : currentSelect;
|
|
3313
|
-
});
|
|
3314
|
-
},
|
|
3315
|
-
[]
|
|
3316
|
-
);
|
|
3317
|
-
|
|
3318
|
-
const handleCalendarSelectTriggerPointerDown = React.useCallback(
|
|
3319
|
-
(
|
|
3320
|
-
select: CalendarSelect,
|
|
3321
|
-
event: React.PointerEvent<HTMLButtonElement>
|
|
3322
|
-
) => {
|
|
3323
|
-
if (openCalendarSelect !== select) return;
|
|
3324
|
-
|
|
3325
|
-
event.preventDefault();
|
|
3326
|
-
setOpenCalendarSelect(null);
|
|
3327
|
-
},
|
|
3328
|
-
[openCalendarSelect]
|
|
3329
|
-
);
|
|
3330
|
-
|
|
3331
3612
|
const setTriggerRef = React.useCallback(
|
|
3332
3613
|
(node: HTMLDivElement | null) => {
|
|
3333
3614
|
triggerRef.current = node;
|
|
@@ -3356,29 +3637,20 @@ const DateTimePicker = React.forwardRef<HTMLDivElement, DateTimePickerProps>(
|
|
|
3356
3637
|
}
|
|
3357
3638
|
}, [displayValue, isDateInputFocused]);
|
|
3358
3639
|
|
|
3359
|
-
React.useEffect(() => {
|
|
3360
|
-
if (!open) {
|
|
3361
|
-
setOpenCalendarSelect(null);
|
|
3362
|
-
}
|
|
3363
|
-
}, [open]);
|
|
3364
|
-
|
|
3365
3640
|
React.useEffect(() => {
|
|
3366
3641
|
if (!open) return;
|
|
3367
3642
|
|
|
3368
3643
|
const handlePointerDown = (event: MouseEvent) => {
|
|
3369
3644
|
if (
|
|
3370
3645
|
!isPointerInsideElement(event, rootRef.current) &&
|
|
3371
|
-
!isPointerInsideElement(event, popoverRef.current)
|
|
3372
|
-
!isPointerInsideSelector(event, CALENDAR_SELECT_CONTENT_SELECTOR)
|
|
3646
|
+
!isPointerInsideElement(event, popoverRef.current)
|
|
3373
3647
|
) {
|
|
3374
|
-
setOpenCalendarSelect(null);
|
|
3375
3648
|
setOpen(false);
|
|
3376
3649
|
}
|
|
3377
3650
|
};
|
|
3378
3651
|
|
|
3379
3652
|
const handleKeyDown = (event: KeyboardEvent) => {
|
|
3380
3653
|
if (event.key === "Escape") {
|
|
3381
|
-
setOpenCalendarSelect(null);
|
|
3382
3654
|
setOpen(false);
|
|
3383
3655
|
}
|
|
3384
3656
|
};
|
|
@@ -3393,9 +3665,15 @@ const DateTimePicker = React.forwardRef<HTMLDivElement, DateTimePickerProps>(
|
|
|
3393
3665
|
}, [open, setOpen]);
|
|
3394
3666
|
|
|
3395
3667
|
const updateValue = React.useCallback(
|
|
3396
|
-
(
|
|
3668
|
+
(
|
|
3669
|
+
nextValue: DateTimePickerValue,
|
|
3670
|
+
options?: { hasTimeValue?: boolean }
|
|
3671
|
+
) => {
|
|
3397
3672
|
if (!isValueControlled) {
|
|
3398
3673
|
setInternalValue(nextValue);
|
|
3674
|
+
if (options?.hasTimeValue !== undefined) {
|
|
3675
|
+
setInternalHasTimeValue(options.hasTimeValue);
|
|
3676
|
+
}
|
|
3399
3677
|
}
|
|
3400
3678
|
|
|
3401
3679
|
onValueChange?.(nextValue);
|
|
@@ -3412,7 +3690,7 @@ const DateTimePicker = React.forwardRef<HTMLDivElement, DateTimePickerProps>(
|
|
|
3412
3690
|
date: undefined,
|
|
3413
3691
|
startTime: currentValue.startTime,
|
|
3414
3692
|
endTime: currentValue.endTime,
|
|
3415
|
-
});
|
|
3693
|
+
}, { hasTimeValue: false });
|
|
3416
3694
|
};
|
|
3417
3695
|
|
|
3418
3696
|
const yearOptions = React.useMemo(
|
|
@@ -3427,12 +3705,134 @@ const DateTimePicker = React.forwardRef<HTMLDivElement, DateTimePickerProps>(
|
|
|
3427
3705
|
[effectiveMinDate, maxDate]
|
|
3428
3706
|
);
|
|
3429
3707
|
|
|
3708
|
+
const syncCalendarMonthAndValue = React.useCallback(
|
|
3709
|
+
(nextMonth: Date) => {
|
|
3710
|
+
const clampedMonth = clampMonth(nextMonth, effectiveMinDate, maxDate);
|
|
3711
|
+
setVisibleMonth(clampedMonth);
|
|
3712
|
+
|
|
3713
|
+
if (pickerVariant === "time-only" || !currentValue.date) return;
|
|
3714
|
+
|
|
3715
|
+
const selectedDay = currentValue.date.getDate();
|
|
3716
|
+
const daysInTargetMonth = getDaysInMonth(
|
|
3717
|
+
clampedMonth.getFullYear(),
|
|
3718
|
+
clampedMonth.getMonth() + 1
|
|
3719
|
+
);
|
|
3720
|
+
|
|
3721
|
+
if (selectedDay > daysInTargetMonth) {
|
|
3722
|
+
const fallbackDate = new Date(
|
|
3723
|
+
clampedMonth.getFullYear(),
|
|
3724
|
+
clampedMonth.getMonth(),
|
|
3725
|
+
1
|
|
3726
|
+
);
|
|
3727
|
+
|
|
3728
|
+
if (!isSelectableDay(fallbackDate, effectiveMinDate, maxDate)) {
|
|
3729
|
+
updateValue({ ...currentValue, date: undefined });
|
|
3730
|
+
return;
|
|
3731
|
+
}
|
|
3732
|
+
|
|
3733
|
+
updateValue({ ...currentValue, date: fallbackDate });
|
|
3734
|
+
return;
|
|
3735
|
+
}
|
|
3736
|
+
|
|
3737
|
+
const nextSelectedDate = new Date(
|
|
3738
|
+
clampedMonth.getFullYear(),
|
|
3739
|
+
clampedMonth.getMonth(),
|
|
3740
|
+
selectedDay
|
|
3741
|
+
);
|
|
3742
|
+
|
|
3743
|
+
if (!isSelectableDay(nextSelectedDate, effectiveMinDate, maxDate)) {
|
|
3744
|
+
updateValue({ ...currentValue, date: undefined });
|
|
3745
|
+
return;
|
|
3746
|
+
}
|
|
3747
|
+
|
|
3748
|
+
if (!isSameDay(currentValue.date, nextSelectedDate)) {
|
|
3749
|
+
updateValue({ ...currentValue, date: nextSelectedDate });
|
|
3750
|
+
}
|
|
3751
|
+
},
|
|
3752
|
+
[
|
|
3753
|
+
currentValue,
|
|
3754
|
+
effectiveMinDate,
|
|
3755
|
+
maxDate,
|
|
3756
|
+
pickerVariant,
|
|
3757
|
+
updateValue,
|
|
3758
|
+
]
|
|
3759
|
+
);
|
|
3760
|
+
|
|
3430
3761
|
const handleTypedDateChange = (
|
|
3431
3762
|
event: React.ChangeEvent<HTMLInputElement>
|
|
3432
3763
|
) => {
|
|
3764
|
+
if (pickerVariant === "time-only") {
|
|
3765
|
+
const nextInputValue = sanitizeTypedTimeInput(
|
|
3766
|
+
event.target.value,
|
|
3767
|
+
dateInputValue,
|
|
3768
|
+
resolvedShowSeconds
|
|
3769
|
+
);
|
|
3770
|
+
setDateInputValue(nextInputValue);
|
|
3771
|
+
|
|
3772
|
+
if (
|
|
3773
|
+
nextInputValue === dateInputValue &&
|
|
3774
|
+
event.target.value !== dateInputValue
|
|
3775
|
+
) {
|
|
3776
|
+
event.currentTarget.value = nextInputValue;
|
|
3777
|
+
return;
|
|
3778
|
+
}
|
|
3779
|
+
|
|
3780
|
+
const typedTime = parseTimePart(nextInputValue);
|
|
3781
|
+
if (typedTime === undefined) {
|
|
3782
|
+
updateValue({ ...currentValue, date: undefined }, { hasTimeValue: false });
|
|
3783
|
+
return;
|
|
3784
|
+
}
|
|
3785
|
+
|
|
3786
|
+
if (typedTime) {
|
|
3787
|
+
updateValue(
|
|
3788
|
+
{
|
|
3789
|
+
...currentValue,
|
|
3790
|
+
startTime: typedTime,
|
|
3791
|
+
},
|
|
3792
|
+
{ hasTimeValue: true }
|
|
3793
|
+
);
|
|
3794
|
+
}
|
|
3795
|
+
|
|
3796
|
+
return;
|
|
3797
|
+
}
|
|
3798
|
+
|
|
3799
|
+
if (pickerVariant === "date-only") {
|
|
3800
|
+
const nextInputValue = sanitizeTypedDateInput(
|
|
3801
|
+
event.target.value,
|
|
3802
|
+
dateInputValue
|
|
3803
|
+
);
|
|
3804
|
+
setDateInputValue(nextInputValue);
|
|
3805
|
+
|
|
3806
|
+
if (
|
|
3807
|
+
nextInputValue === dateInputValue &&
|
|
3808
|
+
event.target.value !== dateInputValue
|
|
3809
|
+
) {
|
|
3810
|
+
event.currentTarget.value = nextInputValue;
|
|
3811
|
+
return;
|
|
3812
|
+
}
|
|
3813
|
+
|
|
3814
|
+
const typedDate = parseTypedDate(nextInputValue);
|
|
3815
|
+
if (typedDate === undefined) {
|
|
3816
|
+
updateValue({ ...currentValue, date: undefined });
|
|
3817
|
+
return;
|
|
3818
|
+
}
|
|
3819
|
+
|
|
3820
|
+
if (
|
|
3821
|
+
typedDate &&
|
|
3822
|
+
!(effectiveMinDate && isBeforeDay(typedDate, effectiveMinDate)) &&
|
|
3823
|
+
!(maxDate && isAfterDay(typedDate, maxDate))
|
|
3824
|
+
) {
|
|
3825
|
+
updateValue({ ...currentValue, date: typedDate });
|
|
3826
|
+
updateVisibleMonth(typedDate);
|
|
3827
|
+
}
|
|
3828
|
+
|
|
3829
|
+
return;
|
|
3830
|
+
}
|
|
3831
|
+
|
|
3433
3832
|
const nextInputValue = sanitizeTypedDateTimeInput(
|
|
3434
3833
|
event.target.value,
|
|
3435
|
-
dateInputValue
|
|
3834
|
+
dateInputValue,
|
|
3835
|
+
resolvedShowSeconds
|
|
3436
3836
|
);
|
|
3437
3837
|
setDateInputValue(nextInputValue);
|
|
3438
3838
|
|
|
@@ -3469,6 +3869,7 @@ const DateTimePicker = React.forwardRef<HTMLDivElement, DateTimePickerProps>(
|
|
|
3469
3869
|
const handleTypedDateKeyDown = (
|
|
3470
3870
|
event: React.KeyboardEvent<HTMLInputElement>
|
|
3471
3871
|
) => {
|
|
3872
|
+
if (pickerVariant === "time-only") return;
|
|
3472
3873
|
if (event.key !== "ArrowUp" && event.key !== "ArrowDown") return;
|
|
3473
3874
|
|
|
3474
3875
|
const direction = event.key === "ArrowUp" ? 1 : -1;
|
|
@@ -3479,7 +3880,8 @@ const DateTimePicker = React.forwardRef<HTMLDivElement, DateTimePickerProps>(
|
|
|
3479
3880
|
dateInputValue,
|
|
3480
3881
|
cursorPosition,
|
|
3481
3882
|
direction,
|
|
3482
|
-
currentValue.startTime
|
|
3883
|
+
currentValue.startTime,
|
|
3884
|
+
resolvedShowSeconds
|
|
3483
3885
|
);
|
|
3484
3886
|
|
|
3485
3887
|
if (!steppedDateTime) return;
|
|
@@ -3495,16 +3897,26 @@ const DateTimePicker = React.forwardRef<HTMLDivElement, DateTimePickerProps>(
|
|
|
3495
3897
|
|
|
3496
3898
|
const nextInputValue = formatDateForDisplay(
|
|
3497
3899
|
steppedDateTime.date,
|
|
3498
|
-
steppedDateTime.startTime
|
|
3900
|
+
steppedDateTime.startTime,
|
|
3901
|
+
resolvedShowSeconds
|
|
3499
3902
|
);
|
|
3903
|
+
const dateTimeInputSegmentRanges =
|
|
3904
|
+
getDateTimeInputSegmentRanges(resolvedShowSeconds);
|
|
3500
3905
|
const [selectionStart, selectionEnd] =
|
|
3501
|
-
|
|
3906
|
+
dateTimeInputSegmentRanges[steppedDateTime.segment];
|
|
3907
|
+
const resolvedInputValue =
|
|
3908
|
+
pickerVariant === "date-only"
|
|
3909
|
+
? formatDateOnlyForDisplay(steppedDateTime.date)
|
|
3910
|
+
: nextInputValue;
|
|
3502
3911
|
|
|
3503
|
-
setDateInputValue(
|
|
3912
|
+
setDateInputValue(resolvedInputValue);
|
|
3504
3913
|
updateValue({
|
|
3505
3914
|
...currentValue,
|
|
3506
3915
|
date: steppedDateTime.date,
|
|
3507
|
-
startTime:
|
|
3916
|
+
startTime:
|
|
3917
|
+
pickerVariant === "date-only"
|
|
3918
|
+
? currentValue.startTime
|
|
3919
|
+
: steppedDateTime.startTime,
|
|
3508
3920
|
});
|
|
3509
3921
|
updateVisibleMonth(steppedDateTime.date);
|
|
3510
3922
|
|
|
@@ -3516,7 +3928,13 @@ const DateTimePicker = React.forwardRef<HTMLDivElement, DateTimePickerProps>(
|
|
|
3516
3928
|
const handleTypedDateBlur = () => {
|
|
3517
3929
|
setIsDateInputFocused(false);
|
|
3518
3930
|
setDateInputValue(
|
|
3519
|
-
|
|
3931
|
+
formatValueForDisplay(
|
|
3932
|
+
currentValue,
|
|
3933
|
+
pickerVariant,
|
|
3934
|
+
resolvedShowEndTime,
|
|
3935
|
+
hasTimeValue,
|
|
3936
|
+
resolvedShowSeconds
|
|
3937
|
+
)
|
|
3520
3938
|
);
|
|
3521
3939
|
};
|
|
3522
3940
|
|
|
@@ -3530,7 +3948,8 @@ const DateTimePicker = React.forwardRef<HTMLDivElement, DateTimePickerProps>(
|
|
|
3530
3948
|
ref={setPopoverRef}
|
|
3531
3949
|
role="dialog"
|
|
3532
3950
|
aria-modal="false"
|
|
3533
|
-
aria-labelledby={\`\${triggerId}-calendar-heading\`}
|
|
3951
|
+
aria-labelledby={showCalendar ? \`\${triggerId}-calendar-heading\` : undefined}
|
|
3952
|
+
aria-label={showCalendar ? undefined : "Time picker"}
|
|
3534
3953
|
className={cn(
|
|
3535
3954
|
"rounded-lg border border-solid border-semantic-border-layout bg-semantic-bg-primary shadow-lg flex flex-col min-h-0 overflow-y-auto overflow-x-hidden overscroll-contain pointer-events-auto",
|
|
3536
3955
|
"[scrollbar-gutter:stable] [scrollbar-width:thin] [scrollbar-color:var(--semantic-border-secondary)_transparent]",
|
|
@@ -3545,66 +3964,34 @@ const DateTimePicker = React.forwardRef<HTMLDivElement, DateTimePickerProps>(
|
|
|
3545
3964
|
zIndex: 10050,
|
|
3546
3965
|
visibility: isPositioned ? undefined : "hidden",
|
|
3547
3966
|
}}
|
|
3548
|
-
onPointerDown={(event) =>
|
|
3549
|
-
if (
|
|
3550
|
-
event.target instanceof Element &&
|
|
3551
|
-
!event.target.closest(CALENDAR_SELECT_TRIGGER_SELECTOR)
|
|
3552
|
-
) {
|
|
3553
|
-
setOpenCalendarSelect(null);
|
|
3554
|
-
}
|
|
3555
|
-
|
|
3556
|
-
event.stopPropagation();
|
|
3557
|
-
}}
|
|
3967
|
+
onPointerDown={(event) => event.stopPropagation()}
|
|
3558
3968
|
onMouseDown={(event) => event.stopPropagation()}
|
|
3559
3969
|
onWheel={(event) => event.stopPropagation()}
|
|
3560
3970
|
onTouchMove={(event) => event.stopPropagation()}
|
|
3561
3971
|
>
|
|
3562
|
-
|
|
3563
|
-
<div className="
|
|
3564
|
-
<
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
</button>
|
|
3572
|
-
<div className="flex min-w-0 items-center gap-1.5">
|
|
3573
|
-
<label className="sr-only" htmlFor={\`\${triggerId}-month\`}>
|
|
3574
|
-
Month
|
|
3575
|
-
</label>
|
|
3576
|
-
<Select
|
|
3577
|
-
open={openCalendarSelect === "month"}
|
|
3578
|
-
onOpenChange={(nextOpen) =>
|
|
3579
|
-
handleCalendarSelectOpenChange("month", nextOpen)
|
|
3580
|
-
}
|
|
3581
|
-
value={visibleMonth.getMonth().toString()}
|
|
3582
|
-
onValueChange={(nextMonth) =>
|
|
3583
|
-
updateVisibleMonth(
|
|
3584
|
-
new Date(
|
|
3585
|
-
visibleMonth.getFullYear(),
|
|
3586
|
-
Number(nextMonth),
|
|
3587
|
-
1
|
|
3588
|
-
)
|
|
3589
|
-
)
|
|
3972
|
+
{showCalendar && (
|
|
3973
|
+
<div className="p-3 touch-pan-y">
|
|
3974
|
+
<div className="mb-3 flex items-center justify-between gap-2">
|
|
3975
|
+
<button
|
|
3976
|
+
type="button"
|
|
3977
|
+
aria-label="Previous month"
|
|
3978
|
+
className="p-1 rounded hover:bg-semantic-bg-hover text-semantic-text-secondary transition-colors"
|
|
3979
|
+
onClick={() =>
|
|
3980
|
+
syncCalendarMonthAndValue(addMonths(visibleMonth, -1))
|
|
3590
3981
|
}
|
|
3591
3982
|
>
|
|
3592
|
-
<
|
|
3983
|
+
<ChevronLeft className="size-4" aria-hidden="true" />
|
|
3984
|
+
</button>
|
|
3985
|
+
<div className="flex min-w-0 items-center gap-1.5">
|
|
3986
|
+
<CalendarDropdown
|
|
3593
3987
|
id={\`\${triggerId}-month\`}
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
<SelectValue />
|
|
3602
|
-
</SelectTrigger>
|
|
3603
|
-
<SelectContent
|
|
3604
|
-
data-date-time-picker-calendar-select=""
|
|
3605
|
-
className={CALENDAR_SELECT_CONTENT_CLASS}
|
|
3606
|
-
>
|
|
3607
|
-
{monthNames.map((monthName, monthIndex) => {
|
|
3988
|
+
label="Month"
|
|
3989
|
+
value={visibleMonth.getMonth().toString()}
|
|
3990
|
+
open={openCalendarDropdown === "month"}
|
|
3991
|
+
onOpenChange={(nextOpen) =>
|
|
3992
|
+
setOpenCalendarDropdown(nextOpen ? "month" : null)
|
|
3993
|
+
}
|
|
3994
|
+
options={monthNames.map((monthName, monthIndex) => {
|
|
3608
3995
|
const optionMonth = new Date(
|
|
3609
3996
|
visibleMonth.getFullYear(),
|
|
3610
3997
|
monthIndex,
|
|
@@ -3615,134 +4002,129 @@ const DateTimePicker = React.forwardRef<HTMLDivElement, DateTimePickerProps>(
|
|
|
3615
4002
|
isMonthBefore(optionMonth, effectiveMinDate)) ||
|
|
3616
4003
|
(maxDate && isMonthAfter(optionMonth, maxDate));
|
|
3617
4004
|
|
|
3618
|
-
return
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
>
|
|
3624
|
-
{monthName}
|
|
3625
|
-
</SelectItem>
|
|
3626
|
-
);
|
|
4005
|
+
return {
|
|
4006
|
+
value: monthIndex.toString(),
|
|
4007
|
+
label: monthName,
|
|
4008
|
+
disabled: !!isDisabled,
|
|
4009
|
+
};
|
|
3627
4010
|
})}
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
onValueChange={(nextYear) =>
|
|
3640
|
-
updateVisibleMonth(
|
|
3641
|
-
new Date(
|
|
3642
|
-
Number(nextYear),
|
|
3643
|
-
visibleMonth.getMonth(),
|
|
3644
|
-
1
|
|
3645
|
-
)
|
|
3646
|
-
)
|
|
3647
|
-
}
|
|
3648
|
-
>
|
|
3649
|
-
<SelectTrigger
|
|
4011
|
+
onValueChange={(nextMonth) => {
|
|
4012
|
+
syncCalendarMonthAndValue(
|
|
4013
|
+
new Date(
|
|
4014
|
+
visibleMonth.getFullYear(),
|
|
4015
|
+
Number(nextMonth),
|
|
4016
|
+
1
|
|
4017
|
+
)
|
|
4018
|
+
);
|
|
4019
|
+
}}
|
|
4020
|
+
/>
|
|
4021
|
+
<CalendarDropdown
|
|
3650
4022
|
id={\`\${triggerId}-year\`}
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
4023
|
+
label="Year"
|
|
4024
|
+
value={visibleMonth.getFullYear().toString()}
|
|
4025
|
+
open={openCalendarDropdown === "year"}
|
|
4026
|
+
onOpenChange={(nextOpen) =>
|
|
4027
|
+
setOpenCalendarDropdown(nextOpen ? "year" : null)
|
|
4028
|
+
}
|
|
4029
|
+
options={yearOptions.map((year) => ({
|
|
4030
|
+
value: year.toString(),
|
|
4031
|
+
label: year.toString(),
|
|
4032
|
+
}))}
|
|
4033
|
+
onValueChange={(nextYear) => {
|
|
4034
|
+
syncCalendarMonthAndValue(
|
|
4035
|
+
new Date(
|
|
4036
|
+
Number(nextYear),
|
|
4037
|
+
visibleMonth.getMonth(),
|
|
4038
|
+
1
|
|
4039
|
+
)
|
|
4040
|
+
);
|
|
4041
|
+
}}
|
|
4042
|
+
/>
|
|
4043
|
+
<div id={\`\${triggerId}-calendar-heading\`} className="sr-only">
|
|
4044
|
+
{monthFormatter.format(visibleMonth)}
|
|
4045
|
+
</div>
|
|
3673
4046
|
</div>
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
<ChevronRight className="size-4" aria-hidden="true" />
|
|
3682
|
-
</button>
|
|
3683
|
-
</div>
|
|
3684
|
-
|
|
3685
|
-
<div className="grid grid-cols-7">
|
|
3686
|
-
{weekDays.map((day) => (
|
|
3687
|
-
<div
|
|
3688
|
-
key={day}
|
|
3689
|
-
className="flex size-8 items-center justify-center text-xs font-medium text-semantic-text-muted"
|
|
4047
|
+
<button
|
|
4048
|
+
type="button"
|
|
4049
|
+
aria-label="Next month"
|
|
4050
|
+
className="p-1 rounded hover:bg-semantic-bg-hover text-semantic-text-secondary transition-colors"
|
|
4051
|
+
onClick={() =>
|
|
4052
|
+
syncCalendarMonthAndValue(addMonths(visibleMonth, 1))
|
|
4053
|
+
}
|
|
3690
4054
|
>
|
|
3691
|
-
|
|
3692
|
-
</
|
|
3693
|
-
|
|
3694
|
-
{calendarDays.map((day) => {
|
|
3695
|
-
const isCurrentMonth =
|
|
3696
|
-
day.getMonth() === visibleMonth.getMonth();
|
|
3697
|
-
const isSelected = isSameDay(day, currentValue.date);
|
|
3698
|
-
const isToday = isSameDay(day, new Date());
|
|
3699
|
-
const isDisabled =
|
|
3700
|
-
(effectiveMinDate && isBeforeDay(day, effectiveMinDate)) ||
|
|
3701
|
-
(maxDate && isAfterDay(day, maxDate));
|
|
3702
|
-
const dayLabel = day.toLocaleDateString("en-US", {
|
|
3703
|
-
month: "long",
|
|
3704
|
-
day: "numeric",
|
|
3705
|
-
year: "numeric",
|
|
3706
|
-
});
|
|
3707
|
-
|
|
3708
|
-
return (
|
|
3709
|
-
<button
|
|
3710
|
-
key={day.toISOString()}
|
|
3711
|
-
type="button"
|
|
3712
|
-
aria-label={dayLabel}
|
|
3713
|
-
aria-pressed={isSelected}
|
|
3714
|
-
aria-current={isToday ? "date" : undefined}
|
|
3715
|
-
disabled={!!isDisabled}
|
|
3716
|
-
className={cn(
|
|
3717
|
-
"relative flex items-center justify-center size-8 mx-auto rounded-full text-xs transition-colors",
|
|
3718
|
-
isSelected
|
|
3719
|
-
? "bg-semantic-primary text-semantic-text-inverted font-semibold"
|
|
3720
|
-
: isCurrentMonth
|
|
3721
|
-
? "text-semantic-text-primary hover:bg-semantic-bg-hover"
|
|
3722
|
-
: "text-semantic-text-muted hover:bg-semantic-bg-hover",
|
|
3723
|
-
isDisabled &&
|
|
3724
|
-
"opacity-40 cursor-not-allowed pointer-events-none"
|
|
3725
|
-
)}
|
|
3726
|
-
onClick={() => {
|
|
3727
|
-
if (isDisabled) return;
|
|
4055
|
+
<ChevronRight className="size-4" aria-hidden="true" />
|
|
4056
|
+
</button>
|
|
4057
|
+
</div>
|
|
3728
4058
|
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
4059
|
+
<div className="grid grid-cols-7">
|
|
4060
|
+
{weekDays.map((day) => (
|
|
4061
|
+
<div
|
|
4062
|
+
key={day}
|
|
4063
|
+
className="flex size-8 items-center justify-center text-xs font-medium text-semantic-text-muted"
|
|
3734
4064
|
>
|
|
3735
|
-
{day
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
4065
|
+
{day}
|
|
4066
|
+
</div>
|
|
4067
|
+
))}
|
|
4068
|
+
{calendarDays.map((day) => {
|
|
4069
|
+
const isCurrentMonth =
|
|
4070
|
+
day.getMonth() === visibleMonth.getMonth();
|
|
4071
|
+
const isSelected = isSameDay(day, currentValue.date);
|
|
4072
|
+
const isToday = isSameDay(day, new Date());
|
|
4073
|
+
const isDisabled =
|
|
4074
|
+
(effectiveMinDate && isBeforeDay(day, effectiveMinDate)) ||
|
|
4075
|
+
(maxDate && isAfterDay(day, maxDate));
|
|
4076
|
+
const dayLabel = day.toLocaleDateString("en-US", {
|
|
4077
|
+
month: "long",
|
|
4078
|
+
day: "numeric",
|
|
4079
|
+
year: "numeric",
|
|
4080
|
+
});
|
|
4081
|
+
|
|
4082
|
+
return (
|
|
4083
|
+
<button
|
|
4084
|
+
key={day.toISOString()}
|
|
4085
|
+
type="button"
|
|
4086
|
+
aria-label={dayLabel}
|
|
4087
|
+
aria-pressed={isSelected}
|
|
4088
|
+
aria-current={isToday ? "date" : undefined}
|
|
4089
|
+
disabled={!!isDisabled}
|
|
4090
|
+
className={cn(
|
|
4091
|
+
"relative flex items-center justify-center size-8 mx-auto rounded-full text-xs transition-colors",
|
|
4092
|
+
isSelected
|
|
4093
|
+
? "bg-semantic-primary text-semantic-text-inverted font-semibold"
|
|
4094
|
+
: isCurrentMonth
|
|
4095
|
+
? "text-semantic-text-primary hover:bg-semantic-bg-hover"
|
|
4096
|
+
: "text-semantic-text-muted hover:bg-semantic-bg-hover",
|
|
4097
|
+
isDisabled &&
|
|
4098
|
+
"opacity-40 cursor-not-allowed pointer-events-none"
|
|
4099
|
+
)}
|
|
4100
|
+
onClick={() => {
|
|
4101
|
+
if (isDisabled) return;
|
|
4102
|
+
|
|
4103
|
+
updateValue({ ...currentValue, date: day });
|
|
4104
|
+
if (closeOnSelect) {
|
|
4105
|
+
setOpen(false);
|
|
4106
|
+
}
|
|
4107
|
+
}}
|
|
4108
|
+
>
|
|
4109
|
+
{day.getDate()}
|
|
4110
|
+
{isToday && !isSelected && (
|
|
4111
|
+
<span className="absolute bottom-0.5 left-1/2 -translate-x-1/2 size-1 rounded-full bg-semantic-primary" />
|
|
4112
|
+
)}
|
|
4113
|
+
</button>
|
|
4114
|
+
);
|
|
4115
|
+
})}
|
|
4116
|
+
</div>
|
|
3742
4117
|
</div>
|
|
3743
|
-
|
|
4118
|
+
)}
|
|
3744
4119
|
|
|
3745
|
-
|
|
4120
|
+
{showTimeFields && (
|
|
4121
|
+
<div
|
|
4122
|
+
className={cn(
|
|
4123
|
+
"space-y-3 bg-semantic-bg-primary p-3",
|
|
4124
|
+
showCalendar &&
|
|
4125
|
+
"border-t border-solid border-semantic-border-layout"
|
|
4126
|
+
)}
|
|
4127
|
+
>
|
|
3746
4128
|
<div className="flex flex-col gap-1.5">
|
|
3747
4129
|
<label
|
|
3748
4130
|
htmlFor={\`\${triggerId}-start-time\`}
|
|
@@ -3750,28 +4132,41 @@ const DateTimePicker = React.forwardRef<HTMLDivElement, DateTimePickerProps>(
|
|
|
3750
4132
|
>
|
|
3751
4133
|
{startTimeLabel}
|
|
3752
4134
|
</label>
|
|
3753
|
-
<div
|
|
4135
|
+
<div
|
|
4136
|
+
className="relative cursor-pointer"
|
|
4137
|
+
onClick={() => openNativeTimePicker(startTimeInputRef.current)}
|
|
4138
|
+
>
|
|
3754
4139
|
<Clock2
|
|
3755
4140
|
className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-semantic-text-muted"
|
|
3756
4141
|
aria-hidden="true"
|
|
3757
4142
|
/>
|
|
3758
4143
|
<input
|
|
4144
|
+
ref={startTimeInputRef}
|
|
3759
4145
|
id={\`\${triggerId}-start-time\`}
|
|
3760
4146
|
type="time"
|
|
3761
|
-
step="1"
|
|
3762
|
-
value={
|
|
4147
|
+
step={resolvedShowSeconds ? "1" : "60"}
|
|
4148
|
+
value={formatTimeForTimeInput(
|
|
4149
|
+
currentValue.startTime,
|
|
4150
|
+
resolvedShowSeconds
|
|
4151
|
+
)}
|
|
3763
4152
|
className="h-8 w-full rounded-md border border-solid border-semantic-border-input bg-semantic-bg-primary pl-9 pr-3 text-sm text-semantic-text-primary outline-none transition-colors hover:border-semantic-border-input-focus/50 focus:border-semantic-border-input-focus/50 focus:shadow-[0_0_0_1px_rgba(43,188,202,0.15)]"
|
|
3764
4153
|
onChange={(event) =>
|
|
3765
|
-
updateValue(
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
4154
|
+
updateValue(
|
|
4155
|
+
{
|
|
4156
|
+
...currentValue,
|
|
4157
|
+
startTime: normalizeTimeInputValue(
|
|
4158
|
+
event.target.value,
|
|
4159
|
+
currentValue.startTime
|
|
4160
|
+
),
|
|
4161
|
+
},
|
|
4162
|
+
{ hasTimeValue: true }
|
|
4163
|
+
)
|
|
3769
4164
|
}
|
|
3770
4165
|
/>
|
|
3771
4166
|
</div>
|
|
3772
4167
|
</div>
|
|
3773
4168
|
|
|
3774
|
-
{
|
|
4169
|
+
{resolvedShowEndTime && (
|
|
3775
4170
|
<div className="flex flex-col gap-1.5">
|
|
3776
4171
|
<label
|
|
3777
4172
|
htmlFor={\`\${triggerId}-end-time\`}
|
|
@@ -3779,28 +4174,42 @@ const DateTimePicker = React.forwardRef<HTMLDivElement, DateTimePickerProps>(
|
|
|
3779
4174
|
>
|
|
3780
4175
|
{endTimeLabel}
|
|
3781
4176
|
</label>
|
|
3782
|
-
<div
|
|
4177
|
+
<div
|
|
4178
|
+
className="relative cursor-pointer"
|
|
4179
|
+
onClick={() => openNativeTimePicker(endTimeInputRef.current)}
|
|
4180
|
+
>
|
|
3783
4181
|
<Clock2
|
|
3784
4182
|
className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-semantic-text-muted"
|
|
3785
4183
|
aria-hidden="true"
|
|
3786
4184
|
/>
|
|
3787
4185
|
<input
|
|
4186
|
+
ref={endTimeInputRef}
|
|
3788
4187
|
id={\`\${triggerId}-end-time\`}
|
|
3789
4188
|
type="time"
|
|
3790
|
-
step="1"
|
|
3791
|
-
value={
|
|
4189
|
+
step={resolvedShowSeconds ? "1" : "60"}
|
|
4190
|
+
value={formatTimeForTimeInput(
|
|
4191
|
+
currentValue.endTime,
|
|
4192
|
+
resolvedShowSeconds
|
|
4193
|
+
)}
|
|
3792
4194
|
className="h-8 w-full rounded-md border border-solid border-semantic-border-input bg-semantic-bg-primary pl-9 pr-3 text-sm text-semantic-text-primary outline-none transition-colors hover:border-semantic-border-input-focus/50 focus:border-semantic-border-input-focus/50 focus:shadow-[0_0_0_1px_rgba(43,188,202,0.15)]"
|
|
3793
4195
|
onChange={(event) =>
|
|
3794
|
-
updateValue(
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
4196
|
+
updateValue(
|
|
4197
|
+
{
|
|
4198
|
+
...currentValue,
|
|
4199
|
+
endTime: normalizeTimeInputValue(
|
|
4200
|
+
event.target.value,
|
|
4201
|
+
currentValue.endTime
|
|
4202
|
+
),
|
|
4203
|
+
},
|
|
4204
|
+
{ hasTimeValue: true }
|
|
4205
|
+
)
|
|
3798
4206
|
}
|
|
3799
4207
|
/>
|
|
3800
4208
|
</div>
|
|
3801
4209
|
</div>
|
|
3802
4210
|
)}
|
|
3803
|
-
|
|
4211
|
+
</div>
|
|
4212
|
+
)}
|
|
3804
4213
|
</div>,
|
|
3805
4214
|
portalMount
|
|
3806
4215
|
);
|
|
@@ -3824,7 +4233,12 @@ const DateTimePicker = React.forwardRef<HTMLDivElement, DateTimePickerProps>(
|
|
|
3824
4233
|
<input
|
|
3825
4234
|
type="hidden"
|
|
3826
4235
|
name={name}
|
|
3827
|
-
value={formatHiddenValue(
|
|
4236
|
+
value={formatHiddenValue(
|
|
4237
|
+
currentValue,
|
|
4238
|
+
pickerVariant,
|
|
4239
|
+
resolvedShowEndTime,
|
|
4240
|
+
hasTimeValue
|
|
4241
|
+
)}
|
|
3828
4242
|
/>
|
|
3829
4243
|
)}
|
|
3830
4244
|
<div
|
|
@@ -3846,7 +4260,13 @@ const DateTimePicker = React.forwardRef<HTMLDivElement, DateTimePickerProps>(
|
|
|
3846
4260
|
placeholder={placeholder}
|
|
3847
4261
|
aria-haspopup="dialog"
|
|
3848
4262
|
aria-expanded={open}
|
|
3849
|
-
aria-label=
|
|
4263
|
+
aria-label={
|
|
4264
|
+
pickerVariant === "date-only"
|
|
4265
|
+
? "Date"
|
|
4266
|
+
: pickerVariant === "time-only"
|
|
4267
|
+
? "Time"
|
|
4268
|
+
: "Date and time"
|
|
4269
|
+
}
|
|
3850
4270
|
className="min-w-0 flex-1 bg-transparent text-sm text-semantic-text-primary outline-none placeholder:text-semantic-text-placeholder disabled:cursor-not-allowed read-only:cursor-not-allowed"
|
|
3851
4271
|
onFocus={() => {
|
|
3852
4272
|
setIsDateInputFocused(true);
|
|
@@ -3870,7 +4290,7 @@ const DateTimePicker = React.forwardRef<HTMLDivElement, DateTimePickerProps>(
|
|
|
3870
4290
|
<button
|
|
3871
4291
|
type="button"
|
|
3872
4292
|
disabled={disabled || readOnly}
|
|
3873
|
-
aria-label="Open calendar"
|
|
4293
|
+
aria-label={showCalendar ? "Open calendar" : "Open time picker"}
|
|
3874
4294
|
className="inline-flex shrink-0 items-center justify-center rounded text-semantic-text-muted hover:bg-semantic-bg-hover hover:text-semantic-text-primary disabled:cursor-not-allowed"
|
|
3875
4295
|
onClick={() => setOpen(!open)}
|
|
3876
4296
|
>
|