UNPKG

@dnb/eufemia

Version:

DNB Eufemia Design System UI Library

628 lines (627 loc) 20.4 kB
"use client"; var _span; import _pushInstanceProperty from "core-js-pure/stable/instance/push.js"; import React, { useCallback, useContext, useMemo, useRef, useState } from 'react'; import { isValid as isValidFn, parseISO } from 'date-fns'; import SegmentedField from "../input-masked/segmented-field/SegmentedField.js"; import Button from "../button/Button.js"; import Input, { SubmitButton } from "../input/Input.js"; import { warn, validateDOMAttributes } from "../../shared/component-helper.js"; import { convertStringToDate } from "./DatePickerCalc.js"; import DatePickerContext from "./DatePickerContext.js"; import { Context, useTranslation } from "../../shared/index.js"; import useInputDates from "./hooks/useInputDates.js"; import { formatDate } from "../date-format/DateFormatUtils.js"; import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; const defaultProps = { separatorRegExp: /[-/ ]/g, statusState: 'error', open: false }; function DatePickerInput(externalProps) { const props = { ...defaultProps, ...externalProps }; const { maskOrder: defaultMaskOrder, maskPlaceholder: defaultMaskPlaceholder } = useTranslation().DatePicker; const { isRange, maskOrder, separatorRegExp, id, title, submitAttributes, maskPlaceholder, onFocus, onBlur, onChange, onSubmit, selectedDateTitle, showInput, inputElement, lang, disabled, skeleton, open, size, status, statusState, statusProps, _omitInputShellClass, ...attributes } = props; const [focusState, setFocusState] = useState('virgin'); const invalidDatesRef = useRef({ invalidStartDate: null, invalidEndDate: null }); const isDateFullyFilledOutRef = useRef(false); const { updateDates, callOnChangeHandler, getReturnObject, startDate, endDate, props: { onType, label } } = useContext(DatePickerContext); const { inputDates, updateInputDates } = useInputDates({ startDate, endDate }); const translation = useTranslation().DatePicker; const { locale } = useContext(Context); const resolvedMaskOrder = maskOrder || (locale === 'en-US' ? 'mm/dd/yyyy' : defaultMaskOrder); const resolvedMaskPlaceholder = maskPlaceholder || (locale === 'en-US' ? 'mm/dd/yyyy' : defaultMaskPlaceholder); const hasHadValidDate = isValidFn(startDate) || isValidFn(endDate); const modeDate = useMemo(() => ({ startDate, endDate }), [startDate, endDate]); const dateRefs = useRef({ startDay: '', startMonth: '', startYear: '', endDay: '', endMonth: '', endYear: '' }); syncDateRefs(dateRefs, inputDates); const lastMaskValuesRef = useRef({ start: { day: '', month: '', year: '' }, end: { day: '', month: '', year: '' } }); const temporaryDates = useRef({ startDate: undefined, endDate: undefined }); const focusMode = useRef(undefined); const orderedParts = useMemo(() => { return resolvedMaskOrder.split(separatorRegExp).filter(Boolean).map(p => p.toLowerCase().startsWith('d') ? 'day' : p.toLowerCase().startsWith('m') ? 'month' : 'year'); }, [resolvedMaskOrder, separatorRegExp]); const delimiter = useMemo(() => { var _resolvedMaskPlacehol; return (_resolvedMaskPlacehol = resolvedMaskPlaceholder.match(/[./-]/)) === null || _resolvedMaskPlacehol === void 0 ? void 0 : _resolvedMaskPlacehol[0]; }, [resolvedMaskPlaceholder]); const getValues = useCallback(mode => { return { day: String(dateRefs.current[`${mode}Day`] || ''), month: String(dateRefs.current[`${mode}Month`] || ''), year: String(dateRefs.current[`${mode}Year`] || '') }; }, []); const copyHandler = useCallback((event, mode) => { const date = mode === 'end' ? endDate : startDate; if (isValidFn(date)) { event.preventDefault(); const valueToCopy = getCopyValue(date, locale); event.clipboardData.setData('text/plain', valueToCopy); } }, [endDate, locale, startDate]); const pasteHandler = useCallback(async event => { if (!focusMode.current) { return; } const success = (event.clipboardData || typeof window !== 'undefined' && window['clipboardData']).getData('text/plain'); if (!success) { return; } if (!hasFullDateStructure(success)) { return; } try { const separators = ['.', '/']; const possibleFormats = [resolvedMaskOrder.replace(/\//g, '-')]; const baseFormats = [...possibleFormats]; baseFormats.forEach(date => { separators.forEach(sep => { const format = date.replace(/-/g, sep); _pushInstanceProperty(possibleFormats).call(possibleFormats, format); _pushInstanceProperty(possibleFormats).call(possibleFormats, format.split('').reverse().join('')); }); }); let date; let index = 0; for (index; index < possibleFormats.length; ++index) { date = convertStringToDate(success, { dateFormat: possibleFormats[index], strictDateFormat: true }); if (date) { break; } } if (!date) { return; } event.preventDefault(); const mode = focusMode.current === 'start' ? 'startDate' : 'endDate'; { updateDates({ [mode]: date }); const mm = String(date.getMonth() + 1).padStart(2, '0'); const dd = String(date.getDate()).padStart(2, '0'); const yyyy = String(date.getFullYear()).padStart(4, '0'); const m = focusMode.current === 'start' ? 'start' : 'end'; dateRefs.current[`${m}Day`] = dd; dateRefs.current[`${m}Month`] = mm; dateRefs.current[`${m}Year`] = yyyy; updateInputDates({ [`${m}Day`]: dd, [`${m}Month`]: mm, [`${m}Year`]: yyyy }); } } catch (error) { warn(error); } }, [resolvedMaskOrder, updateDates, updateInputDates]); const buildInputs = useCallback(mode => { const phChars = translation.placeholderCharacters || { day: 'd', month: 'm', year: 'y' }; const byPart = part => { const len = part === 'year' ? 4 : 2; const placeholder = String(phChars[part] || (part === 'year' ? 'y' : part[0])).repeat(len); const labelBase = translation[part]; const label = isRange ? `${translation[mode]} ${labelBase}` : labelBase; const cls = `dnb-date-picker__input dnb-date-picker__input--${part}`; const mask = new Array(len).fill(/[0-9]/); const spinButton = part === 'day' ? { min: 1, max: 31, getInitialValue: () => new Date().getDate() } : part === 'month' ? { min: 1, max: 12, getInitialValue: () => new Date().getMonth() + 1 } : { min: 0, max: 9999, getInitialValue: () => new Date().getFullYear() }; return { id: part, label, placeholder, mask, spinButton, className: cls, inputMode: 'numeric', onPaste: pasteHandler, onCopy: e => copyHandler(e, mode) }; }; return orderedParts.map(p => byPart(p)); }, [isRange, orderedParts, pasteHandler, copyHandler, translation]); const callOnChangeAsInvalid = useCallback(params => { if (isDateFullyFilledOutRef.current || hasHadValidDate) { const datesFromContext = { startDate, endDate }; const { startDate: derivedStartDate, endDate: derivedEndDate, event } = { ...datesFromContext, ...params }; callOnChangeHandler({ startDate: derivedStartDate, endDate: derivedEndDate, event, ...invalidDatesRef.current }); } }, [callOnChangeHandler, hasHadValidDate, startDate, endDate]); const callOnChange = useCallback(({ startDate, endDate, event }) => { const state = {}; if (typeof startDate !== 'undefined' && isValidFn(startDate)) { state['startDate'] = startDate; } if (!isRange) { endDate = startDate; } if (typeof endDate !== 'undefined' && isValidFn(endDate)) { state['endDate'] = endDate; } updateDates(state, dates => { if (typeof startDate !== 'undefined' && isValidFn(startDate) || typeof endDate !== 'undefined' && isValidFn(endDate)) { callOnChangeHandler({ event, ...dates, ...invalidDatesRef.current }); } }); }, [updateDates, callOnChangeHandler, isRange]); const callOnType = useCallback(({ event }) => { const getInputPart = (mode, part, fallback) => { const refValue = dateRefs.current[`${mode}${part}`]; const inputValue = inputDates[`${mode}${part}`]; if (refValue === '') { if (inputValue !== null && typeof inputValue !== 'undefined' && inputValue !== '') { return ''; } return fallback; } if (refValue !== null && typeof refValue !== 'undefined') { return refValue; } if (inputValue !== null && typeof inputValue !== 'undefined') { return inputValue; } return fallback; }; const getDates = () => ['start', 'end'].reduce((acc, mode) => { acc[`${mode}Date`] = [getInputPart(mode, 'Year', 'yyyy'), getInputPart(mode, 'Month', 'mm'), getInputPart(mode, 'Day', 'dd')].join('-'); return acc; }, { startDate: undefined, endDate: undefined }); const { startDate, endDate } = getDates(); const parsedStartDate = parseISO(startDate); const parsedEndDate = parseISO(endDate); const isStartDateValid = isValidFn(parsedStartDate); const isEndDateValid = isValidFn(parsedEndDate); const { isValid, isValidStartDate, isValidEndDate, ...returnObject } = getReturnObject({ startDate: isStartDateValid ? parsedStartDate : null, endDate: isEndDateValid ? parsedEndDate : null, event, ...invalidDatesRef.current }); const typedDates = { ...(!isRange && isValid === false && { date: startDate }), ...(isRange && isValidStartDate === false && { startDate: startDate }), ...(isRange && isValidEndDate === false && { endDate: endDate }) }; onType === null || onType === void 0 || onType({ isValid, isValidStartDate, isValidEndDate, ...returnObject, ...typedDates }); }, [isRange, getReturnObject, onType, inputDates]); const setDate = useCallback((event, mode, type) => { const value = event.target.value; dateRefs.current[`${mode}${type}`] = value; if (modeDate[`${mode}Date`]) { temporaryDates.current[`${mode}Date`] = modeDate[`${mode}Date`]; } const fallback = temporaryDates.current[`${mode}Date`]; const year = dateRefs.current[`${mode}Year`] || fallback && fallback.getFullYear(); const month = dateRefs.current[`${mode}Month`] || fallback && fallback.getMonth() + 1; const day = dateRefs.current[`${mode}Day`] || fallback && fallback.getDate(); const date = new Date(parseFloat(String(year)), parseFloat(String(month)) - 1, parseFloat(String(day))); const [yStr, mStr, dStr] = [String(dateRefs.current[`${mode}Year`] || ''), String(dateRefs.current[`${mode}Month`] || ''), String(dateRefs.current[`${mode}Day`] || '')]; const hasAnyTypedValue = Boolean(yStr || mStr || dStr); const fullyTyped = /^\d{4}$/.test(yStr) && /^\d{2}$/.test(mStr) && /^\d{2}$/.test(dStr); const y = Number(yStr); const m = Number(mStr); const d = Number(dStr); const dt = new Date(y, m - 1, d); const isValidDate = fullyTyped && dt.getFullYear() === y && dt.getMonth() + 1 === m && dt.getDate() === d; isDateFullyFilledOutRef.current = fullyTyped; if (isValidDate) { invalidDatesRef.current = { ...invalidDatesRef.current, ...(mode === 'start' ? { invalidStartDate: null } : { invalidEndDate: null }) }; callOnChange({ [`${mode}Date`]: date, event }); } else { updateDates({ [`${mode}Date`]: null }); updateInputDates({ [`${mode}Day`]: dateRefs.current[`${mode}Day`] || null, [`${mode}Month`]: dateRefs.current[`${mode}Month`] || null, [`${mode}Year`]: dateRefs.current[`${mode}Year`] || null }); const dateString = hasAnyTypedValue ? `${y}-${m}-${d}` : null; invalidDatesRef.current = { ...invalidDatesRef.current, ...(mode === 'start' ? { invalidStartDate: dateString } : { invalidEndDate: dateString }) }; callOnChangeAsInvalid({ [`${mode}Date`]: null, event }); } callOnType({ event }); }, [updateDates, callOnChange, callOnChangeAsInvalid, callOnType, modeDate, dateRefs, temporaryDates, updateInputDates]); const onMultiChange = useCallback((mode, values) => { const prev = getValues(mode); let changed = null; if (values.day !== prev.day) { changed = 'day'; } if (values.month !== prev.month) { changed = 'month'; } if (values.year !== prev.year) { changed = 'year'; } if (!changed) { return; } dateRefs.current[`${mode}Day`] = values.day; dateRefs.current[`${mode}Month`] = values.month; dateRefs.current[`${mode}Year`] = values.year; lastMaskValuesRef.current[mode] = { day: values.day, month: values.month, year: values.year }; const Type = changed === 'day' ? 'Day' : changed === 'month' ? 'Month' : 'Year'; const synthetic = { persist: () => null, target: { value: values[changed] } }; setDate(synthetic, mode, Type); }, [setDate, getValues]); const onMultiChangeStart = useCallback(values => { onMultiChange('start', values); }, [onMultiChange]); const onMultiChangeEnd = useCallback(values => { onMultiChange('end', values); }, [onMultiChange]); const getDerivedDatesFromInputs = useCallback(() => { const deriveDate = mode => { const year = String(dateRefs.current[`${mode}Year`] || ''); const month = String(dateRefs.current[`${mode}Month`] || ''); const day = String(dateRefs.current[`${mode}Day`] || ''); const hasAnyValue = Boolean(year || month || day); if (!hasAnyValue) { return mode === 'start' ? startDate : endDate; } const isComplete = /^\d{4}$/.test(year) && /^\d{2}$/.test(month) && /^\d{2}$/.test(day); if (!isComplete) { return null; } const parsedDate = new Date(Number(year), Number(month) - 1, Number(day)); const isValidDate = parsedDate.getFullYear() === Number(year) && parsedDate.getMonth() + 1 === Number(month) && parsedDate.getDate() === Number(day); return isValidDate ? parsedDate : null; }; return { startDate: deriveDate('start'), endDate: deriveDate('end') }; }, [endDate, startDate]); const scopeRef = useRef(null); const renderInputElement = useCallback(() => { return _jsxs("span", { id: `${id}-input`, className: "dnb-date-picker__input__wrapper", ref: scopeRef, children: [_jsx(SegmentedField, { id: `${id}-start`, _omitInputShellClass: true, size: size, status: !open ? status : null, statusState: statusState, inputs: buildInputs('start'), values: getValues('start'), delimiter: delimiter, disabled: disabled || skeleton, onChange: onMultiChangeStart, scopeRef: scopeRef, onFocus: () => { focusMode.current = 'start'; setFocusState('focus'); onFocus === null || onFocus === void 0 || onFocus({ ...getReturnObject({ event: null, ...getDerivedDatesFromInputs(), ...invalidDatesRef.current }) }); }, onBlur: () => { focusMode.current = null; setFocusState('blur'); onBlur === null || onBlur === void 0 || onBlur({ ...getReturnObject({ event: null, ...getDerivedDatesFromInputs(), ...invalidDatesRef.current }) }); }, ...attributes }), isRange && (_span || (_span = _jsx("span", { className: "dnb-date-picker--separator", "aria-hidden": true, children: ' – ' }))), isRange && _jsx(SegmentedField, { id: `${id}-end`, _omitInputShellClass: true, size: size, status: !open ? status : null, statusState: statusState, inputs: buildInputs('end'), values: getValues('end'), delimiter: delimiter, disabled: disabled || skeleton, onChange: onMultiChangeEnd, scopeRef: scopeRef, onFocus: () => { focusMode.current = 'end'; setFocusState('focus'); onFocus === null || onFocus === void 0 || onFocus({ ...getReturnObject({ event: null, ...getDerivedDatesFromInputs(), ...invalidDatesRef.current }) }); }, onBlur: () => { focusMode.current = null; setFocusState('blur'); onBlur === null || onBlur === void 0 || onBlur({ ...getReturnObject({ event: null, ...getDerivedDatesFromInputs(), ...invalidDatesRef.current }) }); }, ...attributes })] }); }, [id, size, buildInputs, getValues, delimiter, disabled, skeleton, open, status, statusState, attributes, isRange, onFocus, onBlur, getReturnObject, getDerivedDatesFromInputs, onMultiChangeStart, onMultiChangeEnd]); const ariaLabel = useMemo(() => selectedDateTitle ? `${selectedDateTitle}, ${translation.openPickerText}` : translation.openPickerText, [selectedDateTitle, translation]); validateDOMAttributes(props, attributes); validateDOMAttributes(null, submitAttributes); const SubmitElement = useMemo(() => showInput ? SubmitButton : Button, [showInput]); return _jsxs("fieldset", { className: "dnb-date-picker__fieldset", lang: lang, children: [label && _jsx("legend", { className: "dnb-sr-only", children: label }), _jsx(Input, { id: `${id}__input`, inputState: disabled ? 'disabled' : focusState, inputElement: inputElement && typeof inputElement !== 'string' ? typeof inputElement === 'function' ? inputElement(props) : inputElement : renderInputElement, disabled: disabled || skeleton, skeleton: skeleton, size: size, status: !open ? status : null, statusState: statusState, ...statusProps, submitElement: _jsx(SubmitElement, { id: id, disabled: disabled, skeleton: skeleton, className: showInput ? 'dnb-button--input-button' : "", selected: open, "aria-label": ariaLabel, title: title, size: size, status: status, statusState: statusState, type: "button", icon: "calendar", variant: "secondary", onSubmit: onSubmit, onClick: onSubmit, ...submitAttributes, ...statusProps }), lang: lang, _omitInputShellClass: _omitInputShellClass, ...attributes })] }); } export default DatePickerInput; function hasFullDateStructure(value) { const trimmed = String(value).trim(); if (!trimmed) { return false; } return /^(\d{1,4})[./-](\d{1,2})[./-](\d{1,4})$/.test(trimmed); } function getCopyValue(date, locale) { if (locale === 'en-US') { return new Intl.DateTimeFormat(locale, { month: '2-digit', day: '2-digit', year: 'numeric' }).format(date); } return formatDate(date, { locale }); } function syncDateRefs(dateRefs, inputDates) { for (const date in dateRefs.current) { const dateRefValue = dateRefs.current[date]; const inputDateValue = inputDates[date]; if (dateRefValue !== inputDateValue) { dateRefs.current[date] = inputDateValue; } } } //# sourceMappingURL=DatePickerInput.js.map