From 37633483f5b1f7a04b8107e2132893c0cbbb7e12 Mon Sep 17 00:00:00 2001 From: Pratik Kumar Singh <56654568+singh-pk@users.noreply.github.com> Date: Tue, 24 Sep 2024 13:49:37 +0530 Subject: [PATCH] feat: TextField editable in date-picker (#143) * fix: Tooltip width auto as per content length * feat: TextField editable in date-picker * fix: Datepicker closing on selecting month or year from dropdown * Updated date-picked default date format * Removed dropdownRef from date-picked * Updated values to be always have fixed dateFormats * fix: dayjs not able to format itself without customParseFormat plugin * Updated minor code styling --- .../raystack/calendar/calendar.module.css | 4 - packages/raystack/calendar/date-picker.tsx | 127 +++++++++++++++--- .../raystack/inputfield/inputfield.module.css | 6 +- .../raystack/textfield/textfield.module.css | 4 +- 4 files changed, 114 insertions(+), 27 deletions(-) diff --git a/packages/raystack/calendar/calendar.module.css b/packages/raystack/calendar/calendar.module.css index 49ead203..486fc44b 100644 --- a/packages/raystack/calendar/calendar.module.css +++ b/packages/raystack/calendar/calendar.module.css @@ -161,10 +161,6 @@ min-width: max-content; } -.datePickerInput { - cursor: pointer; -} - .dropdowns { display: flex; align-items: center; diff --git a/packages/raystack/calendar/date-picker.tsx b/packages/raystack/calendar/date-picker.tsx index 74530a4e..3dd56224 100644 --- a/packages/raystack/calendar/date-picker.tsx +++ b/packages/raystack/calendar/date-picker.tsx @@ -2,71 +2,162 @@ import { Popover } from "~/popover"; import { TextField } from "~/textfield"; import { Calendar } from "./calendar"; import styles from "./calendar.module.css"; -import { useRef, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { CalendarIcon } from "@radix-ui/react-icons"; import dayjs from "dayjs"; import { TextfieldProps } from "~/textfield/textfield"; import { PropsBase, PropsSingleRequired } from "react-day-picker"; +import customParseFormat from 'dayjs/plugin/customParseFormat'; + +dayjs.extend(customParseFormat); + + interface DatePickerProps { side?: "top" | "right" | "bottom" | "left"; dateFormat?: string; textFieldProps?: TextfieldProps; calendarProps?: PropsSingleRequired & PropsBase; onSelect?: (date: Date) => void; + placeholder?: string; value?: Date; } export function DatePicker({ side = "top", dateFormat = "DD/MM/YYYY", + placeholder = "DD/MM/YYYY", textFieldProps, calendarProps, value = new Date(), onSelect = () => {}, }: DatePickerProps) { + const defaultDate = dayjs(value).format(dateFormat); + const [showCalendar, setShowCalendar] = useState(false); - const dateValue = dayjs(value).format(dateFormat); + const [calendarVal, setCalendarVal] = useState(value); + const [inputState, setInputState] = useState['state']>>(); + + const isDropdownOpenRef = useRef(false); + const textFieldRef = useRef(null); + const contentRef = useRef(null); + const isInputFieldFocused = useRef(false); - const isDropdownOpenedRef = useRef(false); + function isElementOutside(el: HTMLElement) { + return !isDropdownOpenRef.current && // Month and Year dropdown from Date picker + !textFieldRef.current?.contains(el) && // TextField + !contentRef.current?.contains(el); + } + + const handleMouseDown = useCallback((event: MouseEvent) => { + const el = (event.target) as HTMLElement | null; + if (el && isElementOutside(el)) removeEventListeners(); + }, []) + + function registerEventListeners() { + isInputFieldFocused.current = true; + document.addEventListener('mouseup', handleMouseDown, true); + } + + function removeEventListeners() { + isInputFieldFocused.current = false; + setShowCalendar(false); + + const updatedVal = dayjs(calendarVal).format(dateFormat); + + if (textFieldRef.current) textFieldRef.current.value = updatedVal; + if (inputState === undefined) onSelect(dayjs(updatedVal).toDate()); + + document.removeEventListener('mouseup', handleMouseDown); + } const handleSelect = (day: Date) => { onSelect(day); - setShowCalendar(false); + setCalendarVal(day); + setInputState(undefined); + removeEventListeners(); }; function onDropdownOpen() { - isDropdownOpenedRef.current = true; + isDropdownOpenRef.current = true; } function onOpenChange(open?: boolean) { - if (!isDropdownOpenedRef.current) { + if (!isDropdownOpenRef.current && !(isInputFieldFocused.current && showCalendar)) { setShowCalendar(Boolean(open)); } - isDropdownOpenedRef.current = false; + isDropdownOpenRef.current = false; + } + + function handleInputFocus() { + if (isInputFieldFocused.current) return; + if (!showCalendar) setShowCalendar(true); + } + + function handleInputBlur(event: React.FocusEvent) { + if (isInputFieldFocused.current) { + const el = event.relatedTarget as HTMLElement | null; + if (el && isElementOutside(el)) removeEventListeners(); + } else { + registerEventListeners(); + setTimeout(() => textFieldRef.current?.focus()); + } + } + + function handleKeyUp(event: React.KeyboardEvent) { + if (event.code === 'Enter' && textFieldRef.current) { + textFieldRef.current.blur(); + removeEventListeners(); + } + } + + function handleInputChange(event: React.ChangeEvent) { + const { value } = event.target; + + const format = value.includes("/") ? "DD/MM/YYYY" : value.includes("-") ? "DD-MM-YYYY" : undefined; + const date = dayjs(value, format); + + const isValidDate = date.isValid(); + + const isAfter = calendarProps?.startMonth !== undefined ? dayjs(date).isSameOrAfter(calendarProps.startMonth) : true; + const isBefore = calendarProps?.endMonth !== undefined ? dayjs(date).isSameOrBefore(calendarProps.endMonth) : true; + + const isValid = isValidDate && isAfter && isBefore && dayjs(date).isSameOrBefore(dayjs()); + + if (isValid) { + setCalendarVal(date.toDate()); + if (inputState === 'invalid') setInputState(undefined); + } else { + setInputState('invalid'); + } } return ( - - } - className={styles.datePickerInput} - readOnly + } + onChange={handleInputChange} + onFocus={handleInputFocus} + onBlur={handleInputBlur} + state={inputState} + placeholder={placeholder} + onKeyUp={handleKeyUp} {...textFieldProps} - /> - - + /> + + diff --git a/packages/raystack/inputfield/inputfield.module.css b/packages/raystack/inputfield/inputfield.module.css index 210cd35a..8e28a86f 100644 --- a/packages/raystack/inputfield/inputfield.module.css +++ b/packages/raystack/inputfield/inputfield.module.css @@ -52,10 +52,10 @@ padding: var(--pd-8); } -.textfield-invlid { +.textfield-invalid { border: 1px solid var(--border-danger); } -.textfield-invlid:focus { +.textfield-invalid:focus { border: 1px solid var(--border-danger); } @@ -70,4 +70,4 @@ .bold { font-weight: 500 !important; -} \ No newline at end of file +} diff --git a/packages/raystack/textfield/textfield.module.css b/packages/raystack/textfield/textfield.module.css index cdc65e32..fe7409fd 100644 --- a/packages/raystack/textfield/textfield.module.css +++ b/packages/raystack/textfield/textfield.module.css @@ -52,10 +52,10 @@ padding: var(--pd-8); } -.textfield-invlid { +.textfield-invalid { outline: 1px solid var(--border-danger); } -.textfield-invlid:focus { +.textfield-invalid:focus { outline: 1px solid var(--border-danger); }