diff --git a/packages/lsd-react/src/components/Calendar/Calendar.styles.ts b/packages/lsd-react/src/components/Calendar/Calendar.styles.ts index 767a448..eb09c4b 100644 --- a/packages/lsd-react/src/components/Calendar/Calendar.styles.ts +++ b/packages/lsd-react/src/components/Calendar/Calendar.styles.ts @@ -117,7 +117,7 @@ export const CalendarStyles = css` position: absolute; left: 50%; transform: translateX(-50%); - bottom: 0; + bottom: 2px; } .${calendarClasses.disabled} { diff --git a/packages/lsd-react/src/components/Calendar/Calendar.tsx b/packages/lsd-react/src/components/Calendar/Calendar.tsx index 3cc0779..2a3172e 100644 --- a/packages/lsd-react/src/components/Calendar/Calendar.tsx +++ b/packages/lsd-react/src/components/Calendar/Calendar.tsx @@ -22,6 +22,9 @@ export type CalendarProps = Omit< handleRef: React.RefObject size?: 'large' | 'medium' | 'small' onClose?: () => void + onCalendarClickaway?: (event: Event) => void + minDate?: Date + maxDate?: Date } export const Calendar: React.FC & { @@ -34,17 +37,28 @@ export const Calendar: React.FC & { disabled = false, onChange, onClose, + onCalendarClickaway, + // minDate and maxDate are necessary because onDateFocus freaks out with small/large date values. + minDate = new Date(1900, 0, 1), + maxDate = new Date(2100, 0, 1), children, ...props }) => { const ref = useRef(null) const [style, setStyle] = useState({}) const [value, setValue] = useState( - valueProp ? safeConvertDateToString(valueProp).date : null, + valueProp + ? safeConvertDateToString(valueProp, minDate, maxDate).date + : null, ) + const isOpenControlled = typeof open !== 'undefined' useClickAway(ref, (event) => { - if (!open || event.composedPath().includes(handleRef.current!)) return + if (!open) return + + onCalendarClickaway && onCalendarClickaway(event) + + if (isOpenControlled) return onClose && onClose() }) @@ -84,7 +98,7 @@ export const Calendar: React.FC & { useEffect(() => { if (typeof valueProp === 'undefined') return - const { date } = safeConvertDateToString(valueProp) + const { date } = safeConvertDateToString(valueProp, minDate, maxDate) setValue(date) }, [valueProp]) diff --git a/packages/lsd-react/src/components/Calendar/Day.tsx b/packages/lsd-react/src/components/Calendar/Day.tsx index 4a9a369..f5120f4 100644 --- a/packages/lsd-react/src/components/Calendar/Day.tsx +++ b/packages/lsd-react/src/components/Calendar/Day.tsx @@ -64,7 +64,7 @@ export const Day = ({ day, date, disabled = false }: DayProps) => { {parseInt(day, 10)} {isToday && ( - ■ + ▬ )} diff --git a/packages/lsd-react/src/components/DateField/DateField.classes.ts b/packages/lsd-react/src/components/DateField/DateField.classes.ts index bb8ebca..f86a3ef 100644 --- a/packages/lsd-react/src/components/DateField/DateField.classes.ts +++ b/packages/lsd-react/src/components/DateField/DateField.classes.ts @@ -4,6 +4,7 @@ export const dateFieldClasses = { inputContainer: `lsd-date-field__input-container`, input: `lsd-date-field__input-container__input`, + inputFilled: `lsd-date-field__input-container__input--filled`, icon: `lsd-date-field__input-container__icon`, iconButton: `lsd-date-field__input-container__icon-button`, diff --git a/packages/lsd-react/src/components/DateField/DateField.styles.ts b/packages/lsd-react/src/components/DateField/DateField.styles.ts index f505260..00af25d 100644 --- a/packages/lsd-react/src/components/DateField/DateField.styles.ts +++ b/packages/lsd-react/src/components/DateField/DateField.styles.ts @@ -42,6 +42,8 @@ export const DateFieldStyles = css` color: rgb(var(--lsd-text-primary)); background: none; width: 100%; + opacity: 0.4; + transition: opacity 0.2s ease-in-out; } .${dateFieldClasses.input}::-webkit-inner-spin-button, @@ -116,11 +118,9 @@ export const DateFieldStyles = css` } } - .${dateFieldClasses.input}::-webkit-datetime-edit-month-field:focus, - .${dateFieldClasses.input}::-webkit-datetime-edit-day-field:focus, - .${dateFieldClasses.input}::-webkit-datetime-edit-year-field:focus { - color: rgb(var(--lsd-text-primary)); - opacity: 0.4; + .${dateFieldClasses.input}:invalid, .${dateFieldClasses.inputFilled} { + color: rgb(var(--lsd-border-primary)); + opacity: 1; } .${dateFieldClasses.error} diff --git a/packages/lsd-react/src/components/DateField/DateField.tsx b/packages/lsd-react/src/components/DateField/DateField.tsx index 53667aa..e47e8fe 100644 --- a/packages/lsd-react/src/components/DateField/DateField.tsx +++ b/packages/lsd-react/src/components/DateField/DateField.tsx @@ -24,6 +24,7 @@ export type DateFieldProps = Omit< onIconClick?: () => void inputProps?: React.InputHTMLAttributes variant?: 'outlined' | 'outlined-bottom' + calendarIconRef?: React.RefObject } export const DateField: React.FC & { @@ -48,7 +49,12 @@ export const DateField: React.FC & { ...props }) => { const ref = useRef(null) - const input = useInput({ defaultValue, value, onChange, ref }) + const input = useInput({ + defaultValue, + value, + onChange, + ref, + }) const onCancel = () => input.setValue('') @@ -90,15 +96,20 @@ export const DateField: React.FC & { placeholder={placeholder} {...inputProps} ref={ref} - value={input.value} + value={input.value || ''} onChange={input.onChange} - className={clsx(inputProps.className, dateFieldClasses.input)} + className={clsx( + inputProps.className, + dateFieldClasses.input, + input.filled && dateFieldClasses.inputFilled, + )} max={inputProps.max || '9999-12-31'} /> {icon ? ( !disabled && onIconClick && onIconClick()} + ref={props.calendarIconRef} > {icon} diff --git a/packages/lsd-react/src/components/DatePicker/DatePicker.tsx b/packages/lsd-react/src/components/DatePicker/DatePicker.tsx index 12e9007..d8c3062 100644 --- a/packages/lsd-react/src/components/DatePicker/DatePicker.tsx +++ b/packages/lsd-react/src/components/DatePicker/DatePicker.tsx @@ -43,7 +43,9 @@ export const DatePicker: React.FC & { ...props }) => { const ref = useRef(null) + const calendarIconRef = useRef(null) const [openCalendar, setOpenCalendar] = useState(false) + const isControlled = typeof valueProp !== 'undefined' const input = useInput({ value: valueProp, @@ -77,8 +79,10 @@ export const DatePicker: React.FC & { variant={variant} icon={withCalendar && } onIconClick={() => setOpenCalendar((prev) => !prev)} - value={input.value} + // The DateField component is only controlled when the value prop is provided OR the calendar is open. + value={isControlled || openCalendar ? input.value : undefined} onChange={input.onChange} + calendarIconRef={calendarIconRef} {...props} > @@ -86,7 +90,18 @@ export const DatePicker: React.FC & { handleDateChange(date)} open={openCalendar} - onClose={() => setOpenCalendar(false)} + onCalendarClickaway={(event) => { + // If the calendar icon was clicked, return and don't close the calendar here. + // Let the onIconClick above handle the closing. + if ( + calendarIconRef.current && + event?.composedPath().includes(calendarIconRef.current) + ) { + return + } + + setOpenCalendar(false) + }} handleRef={ref} value={input.value} disabled={props.disabled} diff --git a/packages/lsd-react/src/utils/date.utils.ts b/packages/lsd-react/src/utils/date.utils.ts index a5dba50..6b8f977 100644 --- a/packages/lsd-react/src/utils/date.utils.ts +++ b/packages/lsd-react/src/utils/date.utils.ts @@ -1,13 +1,16 @@ -export const safeConvertDateToString = (value: string) => { +export const safeConvertDateToString = ( + value: string, + minDate: Date, + maxDate: Date, +) => { const date = new Date(value ?? undefined) - const isValid = !Number.isNaN(+date) + const isValid = !Number.isNaN(+date) && date >= minDate && date <= maxDate return { isValid, date: isValid ? date : new Date(), } } - export const removeDateTimezoneOffset = (date: Date) => new Date(+date - date.getTimezoneOffset() * 60 * 1000)