UNPKG

@utahdts/utah-design-system

Version:
242 lines (236 loc) 8.86 kB
import { useCallback, useRef } from 'react'; import { useFloating, autoUpdate, offset as floatingOffset, shift, flip } from '@floating-ui/react-dom'; import { useImmer } from 'use-immer'; import { popupPlacement } from '../../enums/popupPlacement'; import { useInterval } from '../../hooks/useInterval'; import { joinClassNames } from '../../util/joinClassNames'; import { useOnKeyUp } from '../../util/useOnKeyUp'; import { IconButton } from '../buttons/IconButton'; import { CalendarInput } from './CalendarInput/CalendarInput'; import { TextInput } from './TextInput'; /** * @param {HTMLDivElement | null} myWrapper * @returns {boolean} */ function isActiveElementInsideCalendarInput(myWrapper) { return document.activeElement?.closest('.input-wrapper--date-input') === myWrapper; } /** * @param {object} props * @param {string} [props.ariaLabel] * @param {string} [props.className] * @param {string} [props.dateFormat] use `date-fns` modifiers for formatting the date; used for CalendarInput * @param {string} [props.defaultValue] * @param {string} [props.errorMessage] * @param {boolean} [props.hasCalendarPopup] defaults to true so that the calendar popup opens; otherwise entry is only textual keyboard * @param {string} props.id * @param {import('react').MutableRefObject<HTMLDivElement | null>} [props.innerRef] * @param {boolean} [props.isClearable] * @param {boolean} [props.isDisabled] * @param {boolean} [props.isRequired] * @param {string} props.label * @param {string} [props.labelClassName] * @param {string} [props.name] defaults to id if not provided * @param {(newValue: string) => void} [props.onChange] e => {}; can be omitted for uncontrolled * @param {() => void} [props.onClear] * @param {(e: React.KeyboardEvent<HTMLInputElement>) => void} [props.onKeyUp] * @param {string} [props.placeholder] * @param {boolean} [props.showCalendarTodayButton] on the calendar popup, should the `today` button be shown * @param {string} [props.value] * @param {string} [props.wrapperClassName] * @returns {import('react').JSX.Element} */ export function DateInput({ ariaLabel, className, dateFormat, defaultValue, errorMessage, hasCalendarPopup = true, id, innerRef: draftInnerRef, isClearable, isDisabled, isRequired, label, labelClassName, name, onChange, onClear, onKeyUp, placeholder, showCalendarTodayButton, value, wrapperClassName, ...rest }) { const wrapperInternalRef = useRef(/** @type {HTMLDivElement | null} */(null)); const [isCalendarPopupOpen, setIsCalendarPopupOpen] = useImmer(false); const popupReferenceElementRef = useRef(/** @type {HTMLDivElement | null} */(null)); const calendarRef = useRef(/** @type {HTMLDivElement | null} */(null)); const { floatingStyles } = useFloating({ elements: { reference: popupReferenceElementRef.current, floating: calendarRef.current, }, middleware: [ floatingOffset({mainAxis: 4, crossAxis: 0, alignmentAxis: 0}), flip(), shift(), ], open: isCalendarPopupOpen, placement: popupPlacement.BOTTOM, whileElementsMounted: autoUpdate, }); // check if no longer have focus when open useInterval( () => { if (!isActiveElementInsideCalendarInput(wrapperInternalRef.current)) { setIsCalendarPopupOpen(false); } }, 250, { isDisabled: !isCalendarPopupOpen } ); const onDownArrowPress = useOnKeyUp( 'ArrowDown', useCallback( () => setIsCalendarPopupOpen(true), [] ), true ); return ( <div className={joinClassNames('input-wrapper input-wrapper--date-input', wrapperClassName)} ref={(ref) => { if (draftInnerRef) { draftInnerRef.current = ref; } wrapperInternalRef.current = ref; }} > <div className="date-input__inner-wrapper"> <div> <TextInput // table date range filter date picker still goes to a calendar on down arrow press even if !hasCalendarPopup aria-label={joinClassNames(ariaLabel, 'Press down arrow to open a calendar picker')} className={joinClassNames(className, 'date-input')} defaultValue={defaultValue} errorMessage={errorMessage} id={id} innerRef={popupReferenceElementRef} isClearable={isClearable} isDisabled={isDisabled} isRequired={isRequired} label={label} labelClassName={labelClassName} name={name} onChange={(e) => onChange?.(e.target.value)} onClear={isClearable ? onClear : undefined} onKeyUp={(e) => { onDownArrowPress(e); onKeyUp?.(e); }} placeholder={placeholder} value={value ?? ''} rightContent={( hasCalendarPopup ? ( <IconButton aria-hidden="true" className="date-input__calendar-icon icon-button--borderless icon-button--small" icon={<span className="utds-icon-before-calendar " aria-hidden="true" />} isDisabled={isDisabled} onClick={(e) => { e.stopPropagation(); setIsCalendarPopupOpen((isOpen) => { if (isOpen) { const textInput = popupReferenceElementRef.current?.querySelector('input[type="text"]'); // @ts-expect-error textInput?.focus(); } return !isOpen; }); }} title="Open popup calendar" // prevent closing and reopening the popup // @ts-expect-error onMouseDown={(e) => e.preventDefault()} onFocus={() => setIsCalendarPopupOpen(false)} /> ) : ( <div aria-hidden className={joinClassNames('date-input__calendar-icon date-input__icon-static', isDisabled && 'date-input__calendar-icon--is-disabled')} onMouseDown={(e) => { // without the preventDefault, clicking the calendar was closing the popup instead of focusing in the text input e.preventDefault(); popupReferenceElementRef.current?.querySelector('input')?.focus(); }} > <span className="utds-icon-before-calendar " aria-hidden="true" /> </div> ) )} // @ts-expect-error onBlur={() => { // give time for new item to become focused setTimeout( () => { // if still active inside the wrapper, don't close the popup if (!isActiveElementInsideCalendarInput(wrapperInternalRef.current)) { setIsCalendarPopupOpen(false); } }, 0 ); }} onClick={() => setIsCalendarPopupOpen(true)} // eslint-disable-next-line react/jsx-props-no-spreading {...rest} /> </div> { hasCalendarPopup ? ( <div className={joinClassNames('date-input__popup', isCalendarPopupOpen ? '' : 'visually-hidden')} ref={calendarRef} style={{ ...floatingStyles, minWidth: popupReferenceElementRef.current?.offsetWidth, }} onKeyUp={(e) => { if (e.key === 'Escape') { setIsCalendarPopupOpen(false); } }} > <CalendarInput dateFormat={dateFormat} label={label} labelClassName="visually-hidden" isDisabled={isDisabled} isHidden={!isCalendarPopupOpen} onChange={(newValue) => { onChange?.(newValue); setIsCalendarPopupOpen(false); const textInput = popupReferenceElementRef.current?.querySelector('input[type="text"]'); // @ts-expect-error textInput?.focus(); }} id={`calendar-input__${id}`} shouldSetFocusOnMount showTodayButton={showCalendarTodayButton} value={value} /> </div> ) : null } </div> </div> ); }