import React, { Fragment, useEffect, useState } from 'react' import { createPortal } from 'react-dom' import { CSSTransition } from 'react-transition-group' import cc from 'classcat' import { canUseDOM } from 'exenv' import { color } from '../_utils/branding' import { useIsLargeMediaSize } from '../_utils/mediaSizeProvider' import { OnChangeParameters } from '../_utils/onChange' import { AutocompleteOnChange, AutoCompleteProps } from '../autoComplete' import { Bullet, BulletTypes } from '../bullet' import { DatePicker } from '../datePicker' import { CalendarIcon } from '../icon/calendarIcon' import { DoubleArrowIcon } from '../icon/doubleArrowIcon' import { SearchIcon } from '../icon/searchIcon' import { StandardSeatIcon as StandardSeat } from '../icon/standardSeat' import { TextTitle } from '../typography/title' import { AutoCompleteOverlay, AutoCompleteOverlayProps } from './autoComplete/overlay' import { AutoCompleteSection } from './autoComplete/section' import { DatePickerOverlay, DatePickerOverlayProps } from './datePicker/overlay' import { DatePickerSection } from './datePicker/section' import { Overlay } from './overlay' import { StyledSearchForm } from './SearchForm.style' import { ResponsiveDivider, VerticalDivider } from './SearchFormDivider' import { SlideSwitchTransition, SlideSwitchTransitionSide } from './SlideSwitchTransition' import { StepperOverlay } from './stepper/overlay' import { StepperSection } from './stepper/section' import { TRANSITION_SECTION_CLASS_NAME, transitionSectionTimeout } from './transitionConfig' export enum SearchFormDisplay { AUTO = 'auto', SMALL = 'small', LARGE = 'large', } export type SearchFormProps = Readonly<{ className?: string onSubmit: (formValues: SearchFormValues) => void onChange?: (formValues: SearchFormValues) => void disabledFrom?: boolean disabledTo?: boolean initialFrom?: string initialTo?: string autocompleteFromPlaceholder: AutoCompleteProps['placeholder'] autocompleteToPlaceholder: AutoCompleteProps['placeholder'] renderAutocompleteFrom: AutoCompleteOverlayProps['renderAutocompleteComponent'] renderAutocompleteTo: AutoCompleteOverlayProps['renderAutocompleteComponent'] renderDatePickerComponent?: DatePickerOverlayProps['renderDatePickerComponent'] datepickerProps: SearchFormDatePickerProps stepperProps: SearchFormStepperProps submitButtonText?: string submitButtonRef?: React.RefObject | null display?: SearchFormDisplay showDateField?: boolean showInvertButton?: boolean addon?: JSX.Element }> export type SearchFormDatePickerProps = Readonly<{ defaultValue: string format?: (value: string) => string }> export type SearchFormStepperProps = Readonly<{ min: number max: number defaultValue: number increaseLabel: string decreaseLabel: string title: string confirmLabel: string format?: (value: number) => string }> export enum SearchFormElements { DATEPICKER = 'DATEPICKER', STEPPER = 'STEPPER', AUTOCOMPLETE_FROM = 'AUTOCOMPLETE_FROM', AUTOCOMPLETE_TO = 'AUTOCOMPLETE_TO', } export type SearchFormValues = { DATEPICKER: string STEPPER: number AUTOCOMPLETE_FROM?: AutocompleteOnChange AUTOCOMPLETE_TO?: AutocompleteOnChange } const getPlaceholderText = ( initial: string, autocompleted: string, placeholder: string, ): string => { if (autocompleted) { return autocompleted } if (initial) { return initial } return placeholder } export const SearchForm = ({ className, onSubmit, onChange = () => {}, initialFrom, initialTo, disabledFrom, disabledTo, autocompleteFromPlaceholder, autocompleteToPlaceholder, renderAutocompleteFrom, renderAutocompleteTo, renderDatePickerComponent = props => , datepickerProps, stepperProps, submitButtonText, display = SearchFormDisplay.AUTO, showDateField = true, submitButtonRef = null, showInvertButton = true, addon, }: SearchFormProps) => { const isLargeMediaSize = useIsLargeMediaSize() const isSmallMediaSize = !isLargeMediaSize // We allow the component display to be overriden by a prop const isLargeDisplay = display === SearchFormDisplay.LARGE || (display === SearchFormDisplay.AUTO && isLargeMediaSize) const isSmallDisplay = display === SearchFormDisplay.SMALL || (display === SearchFormDisplay.AUTO && isSmallMediaSize) const [elementOpened, setElementOpened] = useState(null) // Used as "trigger" each time the value is changed for the invert animation. // Only the change resulting from the invert button should be animated. const animationKey = React.useRef(0) const [formValues, setFormValues] = useState({ [SearchFormElements.STEPPER]: stepperProps.defaultValue, [SearchFormElements.DATEPICKER]: datepickerProps.defaultValue, }) useEffect(() => { onChange(formValues) }, [onChange, formValues]) const getStepperFormattedValue = () => { if (stepperProps.format == null) { return `${formValues[SearchFormElements.STEPPER]}` } return stepperProps.format(formValues[SearchFormElements.STEPPER]) } const getDatepickerFormattedValue = () => { if (datepickerProps.format == null) { return formValues[SearchFormElements.DATEPICKER] } return datepickerProps.format(formValues[SearchFormElements.DATEPICKER]) } const selectedDate = new Date(formValues[SearchFormElements.DATEPICKER]) const closeElement = (elementToClose: SearchFormElements) => { setElementOpened(openedElement => { // Make sure we are still on the element we want to close. // We don't want to close another element. if (openedElement === elementToClose) { return null } return openedElement }) } const datepickerConfig = { title: getDatepickerFormattedValue(), name: 'datepicker', initialDate: selectedDate, initialMonth: selectedDate, onChange: ({ value }: OnChangeParameters) => { closeElement(SearchFormElements.DATEPICKER) setFormValues( (currentFormValues: SearchFormValues): SearchFormValues => ({ ...currentFormValues, [SearchFormElements.DATEPICKER]: value as string, }), ) }, renderDatePickerComponent, } const stepperConfig = { name: 'stepper', min: stepperProps.min, max: stepperProps.max, itemTitle: getStepperFormattedValue(), title: stepperProps.title, increaseLabel: stepperProps.increaseLabel, decreaseLabel: stepperProps.decreaseLabel, value: formValues[SearchFormElements.STEPPER], onChange: ({ value }: OnChangeParameters) => { closeElement(SearchFormElements.STEPPER) setFormValues( (currentFormValues: SearchFormValues): SearchFormValues => ({ ...currentFormValues, [SearchFormElements.STEPPER]: value as number, }), ) }, } const autocompleteFromConfig = { name: 'from', placeholder: autocompleteFromPlaceholder, renderAutocompleteComponent: renderAutocompleteFrom, onSelect: (value: AutocompleteOnChange) => { setFormValues( (currentFormValues: SearchFormValues): SearchFormValues => ({ ...currentFormValues, [SearchFormElements.AUTOCOMPLETE_FROM]: value, }), ) // We only open the destination when it doesn't have a value on large media. if (isLargeMediaSize && !initialTo && !formValues[SearchFormElements.AUTOCOMPLETE_TO]) { setElementOpened(SearchFormElements.AUTOCOMPLETE_TO) } else { closeElement(SearchFormElements.AUTOCOMPLETE_FROM) } }, } const autocompleteToConfig = { name: 'to', placeholder: autocompleteToPlaceholder, renderAutocompleteComponent: renderAutocompleteTo, onSelect: (value: AutocompleteOnChange) => { setFormValues( (currentFormValues: SearchFormValues): SearchFormValues => ({ ...currentFormValues, [SearchFormElements.AUTOCOMPLETE_TO]: value, }), ) // As the date have a default value we cannot check whether it was // changed by the user. if (isLargeMediaSize) { setElementOpened(SearchFormElements.DATEPICKER) } else { closeElement(SearchFormElements.AUTOCOMPLETE_TO) } }, } const transitionSectionConfig = { classNames: TRANSITION_SECTION_CLASS_NAME, timeout: transitionSectionTimeout, mountOnEnter: true, unmountOnExit: true, } const invertFromTo = () => { // Trigger the animation for the next update. animationKey.current += 1 setFormValues( (currentFormValues: SearchFormValues): SearchFormValues => ({ ...currentFormValues, [SearchFormElements.AUTOCOMPLETE_FROM]: formValues[SearchFormElements.AUTOCOMPLETE_TO], [SearchFormElements.AUTOCOMPLETE_TO]: formValues[SearchFormElements.AUTOCOMPLETE_FROM], }), ) } const autocompleteFromValue = formValues[SearchFormElements.AUTOCOMPLETE_FROM] const autocompleteToValue = formValues[SearchFormElements.AUTOCOMPLETE_TO] const doShowInvertButton = showInvertButton && (formValues[SearchFormElements.AUTOCOMPLETE_FROM] != null || formValues[SearchFormElements.AUTOCOMPLETE_TO] != null) return ( { evt.preventDefault() onSubmit(formValues) }} $isSmallDisplay={isSmallDisplay} $isLargeDisplay={isLargeDisplay} $showDateField={showDateField} $showAddon={Boolean(addon)} >
closeElement(SearchFormElements.AUTOCOMPLETE_FROM)} className="kirk-searchForm-overlay kirk-searchForm-autocomplete-from" > {isSmallMediaSize && canUseDOM && createPortal( closeElement(SearchFormElements.AUTOCOMPLETE_FROM)} /> , document.body, )}
closeElement(SearchFormElements.AUTOCOMPLETE_TO)} className="kirk-searchForm-overlay kirk-searchForm-autocomplete-to" > {isSmallMediaSize && canUseDOM && createPortal( closeElement(SearchFormElements.AUTOCOMPLETE_TO)} /> , document.body, )}
{(isLargeDisplay || showDateField) && (
closeElement(SearchFormElements.DATEPICKER)} className="kirk-searchForm-overlay kirk-searchForm-datepicker" > {isSmallMediaSize && canUseDOM && createPortal( closeElement(SearchFormElements.DATEPICKER)} /> , document.body, )}
)}
closeElement(SearchFormElements.STEPPER)} className="kirk-searchForm-overlay kirk-searchForm-stepper" > { setFormValues( (currentFormValues: SearchFormValues): SearchFormValues => ({ ...currentFormValues, [SearchFormElements.STEPPER]: value as number, }), ) }} /> {isSmallMediaSize && canUseDOM && createPortal( closeElement(SearchFormElements.STEPPER)} /> , document.body, )}
{isSmallDisplay && addon && (
{addon}
)}
) }