import React, { KeyboardEvent, ChangeEvent, FocusEvent, ComponentType, Ref, FC, useRef, useState, useContext, ReactNode, } from 'react'; import classnames from 'classnames'; import dayjs from 'dayjs'; import localizedFormat from 'dayjs/plugin/localizedFormat'; import { Button } from './Button'; import { FormElement } from './FormElement'; import { Input, InputProps } from './Input'; import { Datepicker, DatepickerProps } from './Datepicker'; import { ComponentSettingsContext } from './ComponentSettings'; import { AutoAlign, AutoAlignInjectedProps, AutoAlignProps } from './AutoAlign'; import { isElInChildren } from './util'; import { useControlledValue, useEventCallback, useMergeRefs } from './hooks'; import { createFC } from './common'; /** * */ dayjs.extend(localizedFormat); /** * */ type DatepickerDropdownProps = { className?: string; dateValue?: string; minDate?: string; maxDate?: string; extensionRenderer?: ComponentType; elementRef?: Ref; onSelect?: (date: string) => void; onBlur?: (e: React.FocusEvent) => void; onClose?: () => void; }; /** * */ const DatepickerDropdownInner: FC< DatepickerDropdownProps & AutoAlignInjectedProps > = (props) => { const { className, alignment, dateValue, minDate, maxDate, extensionRenderer, elementRef: elementRef_, autoAlignContentRef, onSelect, onBlur, onClose, } = props; const elRef = useRef(null); const elementRef = useMergeRefs([elRef, autoAlignContentRef, elementRef_]); const [vertAlign, align] = alignment; const datepickerClassNames = classnames( className, 'slds-dropdown', align ? `slds-dropdown_${align}` : undefined, vertAlign ? `slds-dropdown_${vertAlign}` : undefined ); return ( ); }; /** * */ const DatepickerDropdown: FC< DatepickerDropdownProps & Pick > = ({ portalClassName, align, ...props }) => ( {(injectedProps) => ( )} ); /** * */ export type DateInputProps = { value?: string | null; defaultValue?: string | null; opened?: boolean; defaultOpened?: boolean; dateFormat?: string; parsingFormats?: string[]; includeTime?: boolean; minDate?: string; maxDate?: string; menuAlign?: 'left' | 'right'; tooltip?: ReactNode; tooltipIcon?: string; elementRef?: Ref; datepickerRef?: Ref; onBlur?: () => void; onValueChange?: (value: string | null, prevValue: string | null) => void; onComplete?: () => void; extensionRenderer?: ComponentType; } & Omit; /** * */ export const DateInput = createFC( (props) => { const { id, opened: opened_, defaultOpened, value: value_, defaultValue, dateFormat, parsingFormats: parsingFormats_, includeTime, className, cols, label, required, error, menuAlign, minDate, maxDate, extensionRenderer, tooltip, tooltipIcon, elementRef: elementRef_, inputRef: inputRef_, datepickerRef: datepickerRef_, onChange, onValueChange, onKeyDown, onBlur, onComplete, ...rprops } = props; const [opened, setOpened] = useControlledValue( opened_, defaultOpened ?? false ); const [value, setValue] = useControlledValue(value_, defaultValue ?? null); const valueFormat = includeTime ? 'YYYY-MM-DDTHH:mm:ss.SSSZ' : 'YYYY-MM-DD'; const inputValueFormat = dateFormat || (includeTime ? 'L HH:mm' : 'L'); const parsingFormats = parsingFormats_ ?? [inputValueFormat]; const dvalue = dayjs(value ?? undefined, valueFormat); const [inputValue_, setInputValue] = useState(null); const inputValue = inputValue_ != null ? inputValue_ : value != null && dvalue.isValid() ? dvalue.format(inputValueFormat) : ''; const elRef = useRef(null); const elementRef = useMergeRefs([elRef, elementRef_]); const inputElRef = useRef(null); const inputRef = useMergeRefs([inputElRef, inputRef_]); const datepickerElRef = useRef(null); const datepickerRef = useMergeRefs([datepickerElRef, datepickerRef_]); const { getActiveElement } = useContext(ComponentSettingsContext); const onChangeValue = useEventCallback((newValue: string | null) => { if (newValue !== value) { onValueChange?.(newValue, value); setValue(newValue); } }); const setValueFromInput = useEventCallback((inputValue: string) => { let newValue = value; if (!inputValue) { newValue = ''; } else { const dvalue = dayjs(inputValue, parsingFormats); if (dvalue.isValid()) { newValue = dvalue.format(valueFormat); } else { newValue = ''; } } onChangeValue(newValue); setInputValue(null); }); const isFocusedInComponent = useEventCallback(() => { const targetEl = getActiveElement(); return ( isElInChildren(elRef.current, targetEl) || isElInChildren(datepickerElRef.current, targetEl) ); }); const showDatepicker = useEventCallback(() => { let newValue = value; if (inputValue != null) { const dvalue = dayjs(inputValue, parsingFormats); if (dvalue.isValid()) { newValue = dvalue.format(valueFormat); } } setOpened(true); onChangeValue(newValue); }); const onDateIconClick = useEventCallback(() => { inputElRef.current?.focus(); setTimeout(() => { showDatepicker(); }, 10); }); const onInputKeyDown = useEventCallback( (e: KeyboardEvent) => { if (e.keyCode === 13) { // return key e.preventDefault(); e.stopPropagation(); if (e.currentTarget.value !== undefined) { setValueFromInput(e.currentTarget.value); } if (onComplete) { setTimeout(() => { onComplete?.(); }, 10); } } else if (e.keyCode === 40) { // down key showDatepicker(); e.preventDefault(); e.stopPropagation(); } onKeyDown?.(e); } ); const onInputChange = useEventCallback( (e: ChangeEvent) => { const inputValue = e.target.value; setInputValue(inputValue); onChange?.(e); } ); const onInputBlur = useEventCallback( (e: FocusEvent) => { if (e.target.tagName.toLowerCase() === 'input') { setValueFromInput(e.target.value); } setTimeout(() => { if (!isFocusedInComponent()) { onBlur?.(); onComplete?.(); } }, 10); } ); const onDatepickerSelect = useEventCallback((dvalue: string) => { const value = dayjs(dvalue).format(valueFormat); onChangeValue(value); setInputValue(null); setTimeout(() => { setOpened(false); const inputEl = inputElRef.current; if (inputEl) { inputEl.focus(); inputEl.select(); } onComplete?.(); }, 200); }); const onDatepickerBlur = useEventCallback(() => { setOpened(false); setTimeout(() => { if (!isFocusedInComponent()) { onBlur?.(); onComplete?.(); } }, 500); }); const onDatepickerClose = useEventCallback(() => { setOpened(false); const inputEl = inputElRef.current; if (inputEl) { inputEl.focus(); inputEl.select(); } }); const formElemProps = { controlId: id, cols, label, required, error, tooltip, tooltipIcon, elementRef, }; return (
{opened ? ( ) : undefined}
); }, { isFormElement: true } );