diff --git a/apps/desktop/src/components/grid/TemporalCellEditor.vue b/apps/desktop/src/components/grid/TemporalCellEditor.vue index 952bb12463..0aac5ed15c 100644 --- a/apps/desktop/src/components/grid/TemporalCellEditor.vue +++ b/apps/desktop/src/components/grid/TemporalCellEditor.vue @@ -3,8 +3,8 @@ import { computed, nextTick, onMounted, ref, watch } from "vue"; import type { FocusOutsideEvent, PointerDownOutsideEvent } from "reka-ui"; import { CalendarClock, ChevronDown, ChevronUp, CircleSlash } from "@lucide/vue"; import { Button } from "@/components/ui/button"; -import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; -import { formatTemporalInputValue, stepTemporalInputValue, type TemporalCellEditorKind } from "@/lib/dataGrid/dataGridTemporalEditor"; +import { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { formatTemporalInputValue, parseTemporalInputValue, stepTemporalInputValue, type TemporalCellEditorKind } from "@/lib/dataGrid/dataGridTemporalEditor"; const props = withDefaults( defineProps<{ @@ -27,19 +27,26 @@ const emit = defineEmits<{ cancel: []; }>(); -const open = ref(true); -const triggerRef = ref(null); +const open = ref(false); +const editorRootRef = ref(null); +const inputRef = ref(null); const localValue = ref(props.modelValue); let closeHandled = false; let isCommitting = false; +let skipCommitOnClose = false; const hasDate = computed(() => props.kind !== "time"); const hasTime = computed(() => props.kind !== "date"); -const displayValue = computed(() => localValue.value || "NULL"); -const triggerClass = computed(() => +const editorRootClass = computed(() => (props.variant === "inline" ? "relative h-9 w-full" : "absolute inset-0 z-10")); +const inputClass = computed(() => props.variant === "inline" - ? "cell-edit-input flex h-9 w-full items-center gap-2 rounded border bg-background px-2 text-left text-xs outline-none hover:border-primary/60 focus:border-primary" - : ["cell-edit-input absolute inset-0 z-10 flex items-center gap-1 border-2 border-primary bg-background py-0 text-left text-xs outline-none", props.cellLayout === "transpose" ? "px-1.5" : "px-2.5"], + ? "cell-edit-input h-9 w-full rounded border bg-background py-0 pl-2 pr-8 text-left text-xs outline-none hover:border-primary/60 focus:border-primary" + : ["cell-edit-input absolute inset-0 z-10 border-2 border-primary bg-background py-0 text-left text-xs outline-none", props.cellLayout === "transpose" ? "pl-1.5 pr-6" : "pl-2.5 pr-7"], +); +const triggerButtonClass = computed(() => + props.variant === "inline" + ? "absolute right-1 top-1/2 z-20 flex h-6 w-6 -translate-y-1/2 items-center justify-center rounded text-muted-foreground hover:bg-muted hover:text-foreground" + : "absolute right-0.5 top-1/2 z-20 flex h-5 w-5 -translate-y-1/2 items-center justify-center rounded text-muted-foreground hover:bg-muted hover:text-foreground", ); const dateParts = computed(() => { const text = formatTemporalInputValue(localValue.value, "date"); @@ -62,7 +69,10 @@ const timeParts = computed(() => { }); onMounted(() => { - nextTick(() => triggerRef.value?.focus()); + nextTick(() => { + inputRef.value?.focus(); + inputRef.value?.select(); + }); }); watch( @@ -74,7 +84,7 @@ watch( function setOpen(value: boolean) { open.value = value; - if (!value && props.commitOnClose && !closeHandled) finishCommit(); + if (!value && props.commitOnClose && !closeHandled && !skipCommitOnClose) finishCommit(); } function setModelValue(value: string) { @@ -82,6 +92,10 @@ function setModelValue(value: string) { emit("update:modelValue", value); } +function updateTextInput(event: Event) { + setModelValue((event.target as HTMLInputElement).value); +} + function updateDate(part: "day" | "month" | "year", rawValue: string | number) { const numberValue = Number(rawValue); const next = { ...dateParts.value }; @@ -130,6 +144,10 @@ function flushInputValue(target: EventTarget | null) { } } +function normalizeTextInputValue() { + setModelValue(parseTemporalInputValue(localValue.value, props.kind) ?? ""); +} + function setNull() { setModelValue("NULL"); } @@ -145,6 +163,7 @@ function setNow() { function finishCommit() { if (isCommitting) return; + normalizeTextInputValue(); closeHandled = true; isCommitting = true; emit("commit"); @@ -169,13 +188,36 @@ function onKeydown(event: KeyboardEvent) { } } +function onInputBlur() { + if (!props.commitOnClose || open.value || closeHandled || skipCommitOnClose) return; + finishCommit(); +} + function onPopoverInteractOutside(event: PointerDownOutsideEvent | FocusOutsideEvent) { const originalEvent = event.detail.originalEvent; - if (originalEvent instanceof FocusEvent || isSelectInteractionTarget(originalEvent.target)) { + if (isEditorInteractionTarget(originalEvent.target)) { + event.preventDefault(); + closePickerOnly(); + return; + } + if (isSelectInteractionTarget(originalEvent.target)) { event.preventDefault(); } } +function closePickerOnly() { + if (!open.value) return; + skipCommitOnClose = true; + setOpen(false); + nextTick(() => { + skipCommitOnClose = false; + }); +} + +function isEditorInteractionTarget(target: EventTarget | null): boolean { + return target instanceof Node && !!editorRootRef.value?.contains(target); +} + function isSelectInteractionTarget(target: EventTarget | null): boolean { return target instanceof Element && !!target.closest("[data-slot='select-content'], [data-slot='select-trigger']"); } @@ -202,14 +244,19 @@ function twoDigit(value: string | number): string {