Skip to content

Commit

Permalink
feat: TextField editable in date-picker (#143)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
singh-pk authored Sep 24, 2024
1 parent bf1163c commit 3763348
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 27 deletions.
4 changes: 0 additions & 4 deletions packages/raystack/calendar/calendar.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -161,10 +161,6 @@
min-width: max-content;
}

.datePickerInput {
cursor: pointer;
}

.dropdowns {
display: flex;
align-items: center;
Expand Down
127 changes: 109 additions & 18 deletions packages/raystack/calendar/date-picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<Partial<React.ComponentProps<typeof TextField>['state']>>();

const isDropdownOpenRef = useRef(false);
const textFieldRef = useRef<HTMLInputElement | null>(null);
const contentRef = useRef<HTMLDivElement | null>(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<HTMLInputElement>) {
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 (
<Popover open={showCalendar} onOpenChange={onOpenChange}>
<Popover.Trigger asChild>
<TextField
value={dateValue}
trailing={<CalendarIcon />}
className={styles.datePickerInput}
readOnly
<TextField
ref={textFieldRef}
defaultValue={defaultDate}
trailing={<Popover.Trigger asChild><CalendarIcon /></Popover.Trigger>}
onChange={handleInputChange}
onFocus={handleInputFocus}
onBlur={handleInputBlur}
state={inputState}
placeholder={placeholder}
onKeyUp={handleKeyUp}
{...textFieldProps}
/>
</Popover.Trigger>
<Popover.Content side={side} className={styles.calendarPopover}>
/>

<Popover.Content side={side} className={styles.calendarPopover} ref={contentRef}>
<Calendar
required={true}
{...calendarProps}
onDropdownOpen={onDropdownOpen}
mode="single"
selected={value}
defaultMonth={value}
selected={calendarVal}
month={calendarVal}
onSelect={handleSelect}
onMonthChange={setCalendarVal}
/>
</Popover.Content>
</Popover>
Expand Down
6 changes: 3 additions & 3 deletions packages/raystack/inputfield/inputfield.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand All @@ -70,4 +70,4 @@

.bold {
font-weight: 500 !important;
}
}
4 changes: 2 additions & 2 deletions packages/raystack/textfield/textfield.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down

0 comments on commit 3763348

Please sign in to comment.