import classNames from "classnames" ; import * as React from "react" ; import type { DayPickerProps } from "react-day-picker" ; import { polyfill } from "react-lifecycles-compat" ; import { AbstractPureComponent2 , DISPLAYNAME_PREFIX , InputGroupProps2 , InputGroup , Intent , IPopoverProps , Props , IRef , Keys , Popover , refHandler , setRef , } from "@blueprintjs/core" ; import * as Classes from "./common/classes" ; import { isDateValid , isDayInRange } from "./common/dateUtils" ; import { getFormattedDateString , DateFormatProps } from "./dateFormat" ; import { DatePicker } from "./datePicker" ; import { getDefaultMaxDate , getDefaultMinDate , IDatePickerBaseProps } from "./datePickerCore" ; import { DatePickerShortcut } from "./shortcuts" ; export type DateInputProps = IDateInputProps ; deprecated export interface IDateInputProps extends IDatePickerBaseProps , DateFormatProps , Props { default canClearSelection ? : boolean ; default clearButtonText ? : string ; default closeOnSelection ? : boolean ; default disabled ? : boolean ; /**
     * The default date to be used in the component when uncontrolled.
     */ defaultValue ? : Date ; /**
     * Whether the component should take up the full width of its container.
     */ fill ? : boolean ; /**
     * Props to pass to the [input group](#core/components/text-inputs.input-group).
     * `disabled` and `value` will be ignored in favor of the top-level props on this component.
     * `type` is fixed to "text".
     */ inputProps ? : InputGroupProps2 ; /**
     * Called when the user selects a new valid date through the `DatePicker` or by typing
     * in the input. The second argument is true if the user clicked on a date in the
     * calendar, changed the input value, or cleared the selection; it will be false if the date
     * was changed by choosing a new month or year.
     */ onChange ? : ( selectedDate : Date , isUserChange : boolean ) => void ; /**
     * Called when the user finishes typing in a new date and the date causes an error state.
     * If the date is invalid, `new Date(undefined)` will be returned. If the date is out of range,
     * the out of range date will be returned (`onChange` is not called in this case).
     */ onError ? : ( errorDate : Date ) => void ; /**
     * Props to pass to the `Popover`.
     * Note that `content`, `autoFocus`, and `enforceFocus` cannot be changed.
     */ popoverProps ? : Partial < IPopoverProps > & object ; /**
     * Element to render on right side of input.
     */ rightElement ? : JSX . Element ; default showActionsBar ? : boolean ; default shortcuts ? : boolean | DatePickerShortcut [ ] ; /**
     * The currently selected day. If this prop is provided, the component acts in a controlled manner.
     * To display no date in the input field, pass `null` to the value prop. To display an invalid date error
     * in the input field, pass `new Date(undefined)` to the value prop.
     */ value ? : Date | null ; default todayButtonText ? : string ; } export interface IDateInputState { value : Date ; valueString : string ; isInputFocused : boolean ; isOpen : boolean ; selectedShortcutIndex ? : number ; } @ polyfill export class DateInput extends AbstractPureComponent2 < DateInputProps , IDateInputState > { public static displayName = `${ DISPLAYNAME_PREFIX }.DateInput` ; public static defaultProps : Partial < DateInputProps > = { closeOnSelection : true , dayPickerProps : { } , disabled : false , invalidDateMessage : "Invalid date" , maxDate : getDefaultMaxDate ( ) , minDate : getDefaultMinDate ( ) , outOfRangeMessage : "Out of range" , reverseMonthAndYearMenus : false , } ; public state : IDateInputState = { isInputFocused : false , isOpen : false , value : this . props . value !== undefined ? this . props . value : this . props . defaultValue , valueString : null , } ; public inputElement : HTMLInputElement | null = null ; public popoverContentElement : HTMLDivElement | null = null ; private handleInputRef = refHandler < HTMLInputElement , "inputElement" > ( this , "inputElement" , this . props . inputProps ?. inputRef ) ; private handlePopoverContentRef : IRef < HTMLDivElement > = refHandler ( this , "popoverContentElement" ) ; public render ( ) { const { value , valueString } = this . state ; const dateString = this . state . isInputFocused ? valueString : getFormattedDateString ( value , this . props ) ; const dateValue = isDateValid ( value ) ? value : null ; const dayPickerProps = { ... this . props . dayPickerProps , onDayKeyDown : ( day , modifiers , e ) => { this . props . dayPickerProps . onDayKeyDown ?. ( day , modifiers , e ) ; } , onMonthChange : ( month ) => { this . props . dayPickerProps . onMonthChange ?. ( month ) ; } , } ; const wrappedPopoverContent = ( < div ref = { this . handlePopoverContentRef } > < div onFocus = { this . handleStartFocusBoundaryFocusIn } tabIndex = { 0 } / > < DatePicker { ... this . props } dayPickerProps = { dayPickerProps } onChange = { this . handleDateChange } value = { dateValue } onShortcutChange = { this . handleShortcutChange } selectedShortcutIndex = { this . state . selectedShortcutIndex } / > < div onFocus = { this . handleEndFocusBoundaryFocusIn } tabIndex = { 0 } / > < /div>); const { inputProps = { } , popoverProps = { } } = this . props ; const isErrorState = value != null && ( ! isDateValid ( value ) || ! this . isDateInRange ( value ) ) ; return ( < Popover isOpen = { this . state . isOpen && ! this . props . disabled } fill = { this . props . fill } { ... popoverProps } autoFocus = { false } className = { classNames ( popoverProps . className , this . props . className ) } content = { wrappedPopoverContent } enforceFocus = { false } onClose = { this . handleClosePopover } popoverClassName = { classNames ( Classes . DATEINPUT_POPOVER , popoverProps . popoverClassName ) } > < InputGroup autoComplete = "off" intent = { isErrorState ? Intent . DANGER : Intent . NONE } placeholder = { this . props . placeholder } rightElement = { this . props . rightElement } type = "text" { ... inputProps } disabled = { this . props . disabled } inputRef = { this . handleInputRef } onBlur = { this . handleInputBlur } onChange = { this . handleInputChange } onClick = { this . handleInputClick } onFocus = { this . handleInputFocus } onKeyDown = { this . handleInputKeyDown } value = { dateString } / > { } < /Popover>); } public componentDidUpdate ( prevProps , prevState ) { super . componentDidUpdate ( prevProps , prevState ) ; if ( prevProps . inputProps ?. inputRef !== this . props . inputProps ?. inputRef ) { setRef ( prevProps . inputProps ?. inputRef , null ) ; this . handleInputRef = refHandler ( this , "inputElement" , this . props . inputProps ?. inputRef ) ; setRef ( this . props . inputProps ?. inputRef , this . inputElement ) ; } if ( prevProps . value !== this . props . value ) { this . setState ( { value : this . props . value } ) ; } } private isDateInRange ( value ) { return isDayInRange ( value , [ this . props . minDate , this . props . maxDate ] ) ; } private handleClosePopover = ( e ? ) => { const { popoverProps = { } } = this . props ; popoverProps . onClose ?. ( e ) ; this . setState ( { isOpen : false } ) ; } ; private handleDateChange = ( newDate , isUserChange , didSubmitWithEnter = false ) => { const prevDate = this . state . value ; const isOpen = ! isUserChange || ! this . props . closeOnSelection || ( prevDate != null && ( this . hasMonthChanged ( prevDate , newDate ) || this . hasTimeChanged ( prevDate , newDate ) ) ) ; const isInputFocused = didSubmitWithEnter ? true : false ; if ( this . props . value === undefined ) { const valueString = getFormattedDateString ( newDate , this . props ) ; this . setState ( { isInputFocused , isOpen , value : newDate , valueString } ) ; } else { this . setState ( { isInputFocused , isOpen } ) ; } this . props . onChange ?. ( newDate , isUserChange ) ; } ; private hasMonthChanged ( prevDate , nextDate ) { return ( prevDate == null ) !== ( nextDate == null ) || nextDate . getMonth ( ) !== prevDate . getMonth ( ) ; } private hasTimeChanged ( prevDate , nextDate ) { if ( this . props . timePrecision == null ) { return false ; } return ( ( prevDate == null ) !== ( nextDate == null ) || nextDate . getHours ( ) !== prevDate . getHours ( ) || nextDate . getMinutes ( ) !== prevDate . getMinutes ( ) || nextDate . getSeconds ( ) !== prevDate . getSeconds ( ) || nextDate . getMilliseconds ( ) !== prevDate . getMilliseconds ( ) ) ; } private handleInputFocus = ( e ) => { const valueString = this . state . value == null ? "" : this . formatDate ( this . state . value ) ; this . setState ( { isInputFocused : true , isOpen : true , valueString } ) ; this . safeInvokeInputProp ( "onFocus" , e ) ; } ; private handleInputClick = ( e ) => { e . stopPropagation ( ) ; this . safeInvokeInputProp ( "onClick" , e ) ; } ; private handleInputChange = ( e ) => { const valueString = ( e . target as HTMLInputElement ) . value ; const value = this . parseDate ( valueString ) ; if ( isDateValid ( value ) && this . isDateInRange ( value ) ) { if ( this . props . value === undefined ) { this . setState ( { value , valueString } ) ; } else { this . setState ( { valueString } ) ; } this . props . onChange ?. ( value , true ) ; } else { if ( valueString . length === 0 ) { this . props . onChange ?. ( null , true ) ; } this . setState ( { valueString } ) ; } this . safeInvokeInputProp ( "onChange" , e ) ; } ; private handleInputBlur = ( e ) => { const { valueString } = this . state ; const date = this . parseDate ( valueString ) ; if ( valueString . length > 0 && valueString !== getFormattedDateString ( this . state . value , this . props ) && ( ! isDateValid ( date ) || ! this . isDateInRange ( date ) ) ) { if ( this . props . value === undefined ) { this . setState ( { isInputFocused : false , value : date , valueString : null } ) ; } else { this . setState ( { isInputFocused : false } ) ; } if ( isNaN ( date . valueOf ( ) ) ) { this . props . onError ?. ( new Date ( undefined ) ) ; } else if ( ! this . isDateInRange ( date ) ) { this . props . onError ?. ( date ) ; } else { this . props . onChange ?. ( date , true ) ; } } else { if ( valueString . length === 0 ) { this . setState ( { isInputFocused : false , value : null , valueString : null } ) ; } else { this . setState ( { isInputFocused : false } ) ; } } this . safeInvokeInputProp ( "onBlur" , e ) ; } ; private handleInputKeyDown = ( e ) => { if ( e . which === Keys . ENTER ) { const nextDate = this . parseDate ( this . state . valueString ) ; this . handleDateChange ( nextDate , true , true ) ; } else if ( e . which === Keys . TAB && e . shiftKey ) { this . handleClosePopover ( ) ; } else if ( e . which === Keys . TAB && this . state . isOpen ) { this . getKeyboardFocusableElements ( ) . shift ( ) ?. focus ( ) ; e . preventDefault ( ) ; } else if ( e . which === Keys . ESCAPE ) { this . setState ( { isOpen : false } ) ; this . inputElement ?. blur ( ) ; } this . safeInvokeInputProp ( "onKeyDown" , e ) ; } ; private getKeyboardFocusableElements = ( ) : HTMLElement [ ] => { const elements = Array . from ( this . popoverContentElement ?. querySelectorAll ( "button:not([disabled]),input,[tabindex]:not([tabindex='-1'])" ) ) ; elements . pop ( ) ; elements . shift ( ) ; return elements ; } ; private handleStartFocusBoundaryFocusIn = ( e ) => { if ( this . popoverContentElement . contains ( this . getRelatedTarget ( e ) ) ) { this . inputElement ?. focus ( ) ; } else { this . getKeyboardFocusableElements ( ) . shift ( ) ?. focus ( ) ; } } ; private handleEndFocusBoundaryFocusIn = ( e ) => { if ( this . popoverContentElement . contains ( this . getRelatedTarget ( e ) ) ) { this . inputElement ?. focus ( ) ; this . handleClosePopover ( ) ; } else { this . getKeyboardFocusableElements ( ) . pop ( ) ?. focus ( ) ; } } ; private getRelatedTarget ( e ) { return ( e . relatedTarget ?? document . activeElement ) as HTMLElement ; } private handleShortcutChange = ( _ , selectedShortcutIndex ) => { this . setState ( { selectedShortcutIndex } ) ; } ; /** safe wrapper around invoking input props event handler (prop defaults to undefined) */ private safeInvokeInputProp ( name , e ) { const { inputProps = { } } = this . props ; inputProps [ name ] ?. ( e ) ; } private parseDate ( dateString ) { if ( dateString === this . props . outOfRangeMessage || dateString === this . props . invalidDateMessage ) { return null ; } const { locale , parseDate } = this . props ; const newDate = parseDate ( dateString , locale ) ; return newDate === false ? new Date ( undefined ) : newDate ; } private formatDate ( date ) { if ( ! isDateValid ( date ) || ! this . isDateInRange ( date ) ) { return "" ; } const { locale , formatDate } = this . props ; return formatDate ( date , locale ) ; } }