@dnb/eufemia
Version:
DNB Eufemia Design System UI Library
628 lines (627 loc) • 20.4 kB
JavaScript
"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