@dnb/eufemia
Version:
DNB Eufemia Design System UI Library
659 lines (658 loc) • 22.7 kB
JavaScript
"use client";
import _extends from "@babel/runtime-corejs3/helpers/esm/extends";
var _span;
import _pushInstanceProperty from "core-js-pure/stable/instance/push.js";
import React, { useCallback, useContext, useMemo, useRef, useState } from 'react';
import { isValid, parseISO } from 'date-fns';
import classnames from 'classnames';
import TextMask from "../input-masked/TextMask.js";
import Button from "../button/Button.js";
import Input, { SubmitButton } from "../input/Input.js";
import { warn, validateDOMAttributes, toCapitalized } from "../../shared/component-helper.js";
import { IS_ANDROID, IS_IOS } from "../../shared/helpers.js";
import { convertStringToDate } from "./DatePickerCalc.js";
import DatePickerContext from "./DatePickerContext.js";
import { Context, useTranslation } from "../../shared/index.js";
import usePartialDates from "./hooks/usePartialDates.js";
import useInputDates from "./hooks/useInputDates.js";
import { formatDate } from "../date-format/DateFormatUtils.js";
const defaultProps = {
separatorRegExp: /[-/ ]/g,
statusState: 'error',
opened: false
};
function DatePickerInput(externalProps) {
const props = {
...defaultProps,
...externalProps
};
const {
maskOrder: defaultMaskOrder,
maskPlaceholder: defaultMaskPlaceholder
} = useTranslation().DatePicker;
const {
isRange,
maskOrder = defaultMaskOrder,
separatorRegExp,
id,
title,
submitAttributes,
maskPlaceholder = defaultMaskPlaceholder,
onFocus,
onBlur,
onChange,
onSubmit,
selectedDateTitle,
showInput,
inputElement,
lang,
disabled,
skeleton,
opened,
size,
status,
statusState,
statusProps,
...attributes
} = props;
const [focusState, setFocusState] = useState('virgin');
const {
partialDatesRef,
setPartialDates
} = usePartialDates();
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 hasHadValidDate = isValid(startDate) || isValid(endDate);
const modeDate = useMemo(() => ({
startDate,
endDate
}), [startDate, endDate]);
const inputRefs = useRef({
startDayRef: {
current: undefined
},
startMonthRef: {
current: undefined
},
startYearRef: {
current: undefined
},
endDayRef: {
current: undefined
},
endMonthRef: {
current: undefined
},
endYearRef: {
current: undefined
}
});
const dateRefs = useRef({
startDay: '',
startMonth: '',
startYear: '',
endDay: '',
endMonth: '',
endYear: ''
});
syncDateRefs(dateRefs, inputDates);
const temporaryDates = useRef({
startDate: undefined,
endDate: undefined
});
const refList = useRef();
const focusMode = useRef();
const maskList = useMemo(() => {
const separators = maskOrder.match(separatorRegExp);
return maskOrder.split(separatorRegExp).reduce((acc, cur) => {
if (!cur) {
return acc;
}
_pushInstanceProperty(acc).call(acc, cur);
if (separators.length > 0) {
_pushInstanceProperty(acc).call(acc, separators.shift());
}
return acc;
}, []);
}, [maskOrder, separatorRegExp]);
const copyHandler = useCallback((event, mode) => {
const date = mode === 'end' ? endDate : startDate;
if (isValid(date)) {
event.preventDefault();
const valueToCopy = formatDate(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;
}
event.preventDefault();
try {
const separators = ['.', '/'];
const possibleFormats = ['yyyy-MM-dd'];
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]
});
if (date) {
break;
}
}
const mode = focusMode.current === 'start' ? 'startDate' : 'endDate';
if (date) {
updateDates({
[mode]: date
});
}
} catch (error) {
warn(error);
}
}, [updateDates]);
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' && isValid(startDate)) {
state['startDate'] = startDate;
}
if (!isRange) {
endDate = startDate;
}
if (typeof endDate !== 'undefined' && isValid(endDate)) {
state['endDate'] = endDate;
}
updateDates(state, dates => {
if (typeof startDate !== 'undefined' && isValid(startDate) || typeof endDate !== 'undefined' && isValid(endDate)) {
callOnChangeHandler({
event,
...dates,
...invalidDatesRef.current
});
}
});
}, [updateDates, callOnChangeHandler, isRange]);
const callOnType = useCallback(({
event
}) => {
const getDates = () => ['start', 'end'].reduce((acc, mode) => {
acc[`${mode}Date`] = [dateRefs.current[`${mode}Year`] || inputDates[`${mode}Year`] || 'yyyy', dateRefs.current[`${mode}Month`] || inputDates[`${mode}Month`] || 'mm', dateRefs.current[`${mode}Day`] || inputDates[`${mode}Day`] || 'dd'].join('-');
return acc;
}, {
startDate: undefined,
endDate: undefined
});
const {
startDate,
endDate
} = getDates();
setPartialDates({
partialStartDate: startDate,
...(isRange && {
partialEndDate: endDate
})
});
const parsedStartDate = parseISO(startDate);
const parsedEndDate = parseISO(endDate);
const isStartDateValid = isValid(parsedStartDate);
const isEndDateValid = isValid(parsedEndDate);
const {
is_valid,
is_valid_start_date,
is_valid_end_date,
...returnObject
} = getReturnObject({
startDate: isStartDateValid ? parsedStartDate : null,
endDate: isEndDateValid ? parsedEndDate : null,
event,
...partialDatesRef.current,
...invalidDatesRef.current
});
const typedDates = {
...(!isRange && is_valid === false && {
date: startDate
}),
...(isRange && is_valid_start_date === false && {
start_date: startDate
}),
...(isRange && is_valid_end_date === false && {
end_date: endDate
})
};
onType === null || onType === void 0 || onType({
is_valid,
is_valid_start_date,
is_valid_end_date,
...returnObject,
...typedDates
});
}, [setPartialDates, isRange, getReturnObject, partialDatesRef, onType, inputDates]);
const setDate = useCallback((event, mode, type) => {
event.persist();
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 isValidDate = !/[^0-9]/.test(String(day)) && !/[^0-9]/.test(String(month)) && !/[^0-9]/.test(String(year)) && isValid(date) && date.getDate() === parseFloat(String(day)) && date.getMonth() + 1 === parseFloat(String(month)) && date.getFullYear() === parseFloat(String(year));
const dateString = `${year}-${month}-${day}`;
isDateFullyFilledOutRef.current = /^[0-9]{4}-[0-9]{2}-[0-9]{2}$/.test(dateString);
if (isValidDate) {
invalidDatesRef.current = {
...invalidDatesRef.current,
...(mode === 'start' ? {
invalidStartDate: null
} : {
invalidEndDate: null
})
};
callOnChange({
[`${mode}Date`]: date,
event
});
} else {
updateDates({
[`${mode}Date`]: null
});
updateInputDates({
[`${mode}${type}`]: value
});
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 dateSetters = useMemo(() => ({
set_startDay: event => {
setDate(event, 'start', 'Day');
},
set_startMonth: event => {
setDate(event, 'start', 'Month');
},
set_startYear: event => {
setDate(event, 'start', 'Year');
},
set_endDay: event => {
setDate(event, 'end', 'Day');
},
set_endMonth: event => {
setDate(event, 'end', 'Month');
},
set_endYear: event => {
setDate(event, 'end', 'Year');
}
}), [setDate]);
const onFocusHandler = useCallback(event => {
setFocusState('focus');
onFocus === null || onFocus === void 0 || onFocus({
...event,
...getReturnObject({
event
})
});
if (isNaN(parseFloat(event.target.value))) {
setCursorPosition(event.target);
}
}, [getReturnObject, onFocus]);
const onBlurHandler = useCallback(event => {
focusMode.current = null;
setFocusState('blur');
onBlur === null || onBlur === void 0 || onBlur({
...event,
...getReturnObject({
event,
...partialDatesRef.current
})
});
}, [onBlur, getReturnObject, partialDatesRef]);
const onKeyDownHandler = useCallback(async event => {
const keyCode = event.key;
const target = event.target;
const isNumberKey = /[0-9]/g.test(keyCode);
if (target.selectionStart !== target.selectionEnd && !isNumberKey) {
setCursorPosition(target);
}
const size = parseFloat(target.getAttribute('size'));
const firstSelectionStart = target.selectionStart;
const firstSelectionEnd = target.selectionEnd;
await wait(IS_IOS ? 10 : 1);
const secondSelectionStart = target.selectionStart;
const isValid = isNumberKey;
const refListArray = refList.current;
const index = refListArray.findIndex(({
current
}) => current === target);
const isLastChar = secondSelectionStart === size;
const isFirstChar = firstSelectionStart === size;
const isMovingForward = keyCode !== 'ArrowLeft' && keyCode !== 'Backspace' && isValid && isLastChar;
const isExplicitForward = (keyCode === 'ArrowRight' || keyCode === 'Enter') && isFirstChar;
const hasNextField = index < refListArray.length - 1;
if (hasNextField && (isMovingForward || isExplicitForward)) {
var _refListArray;
if (!refListArray[index + 1].current) {
return;
}
const nextSibling = (_refListArray = refListArray[index + 1]) === null || _refListArray === void 0 ? void 0 : _refListArray.current;
if (nextSibling) {
setCursorPosition(nextSibling, 0, {
withoutDelay: true
});
}
if (parseFloat(keyCode) <= 9 && firstSelectionStart === target.size) {
const name = toCapitalized(nextSibling.getAttribute('class').match(/__input--(day|month|year)($|\s)/)[1]);
const mode = nextSibling.getAttribute('id').match(/-(start|end)-(day|month|year)/)[1];
dateSetters[`set_${mode}${name}`]({
persist: () => null,
...event,
target: {
value: keyCode + nextSibling.value.slice(1)
}
});
setCursorPosition(nextSibling, 1);
}
} else if (index > 0 && firstSelectionStart === firstSelectionEnd) {
const isMovingBackward = keyCode === 'ArrowLeft' && firstSelectionStart === 0;
const isPressingBackspace = keyCode === 'Backspace' && firstSelectionStart <= 1;
if (isMovingBackward || isPressingBackspace) {
var _refListArray2;
const prevSibling = (_refListArray2 = refListArray[index - 1]) === null || _refListArray2 === void 0 ? void 0 : _refListArray2.current;
if (prevSibling) {
const endPos = prevSibling.value.length;
setCursorPosition(prevSibling, endPos, {
withoutDelay: true
});
}
}
}
}, [dateSetters]);
const onInputHandler = useCallback(event => {
const target = event.currentTarget;
if (IS_ANDROID && event.nativeEvent.inputType === 'deleteContentBackward' && target.selectionStart === 0 && target.selectionEnd === 0) {
onKeyDownHandler({
...event,
key: 'Backspace'
});
}
}, [onKeyDownHandler]);
const getPlaceholderChar = useCallback(value => {
const index = maskOrder.indexOf(value);
return maskPlaceholder[index];
}, [maskOrder, maskPlaceholder]);
const generateDateList = useCallback((element, mode) => {
return maskList.map((value, i) => {
var _context, _context2, _context3;
const state = value.slice(0, 1);
const placeholderChar = getPlaceholderChar(value);
const {
day,
month,
year
} = translation;
const isRangeLabel = isRange ? `${translation[mode]} ` : '';
if (!separatorRegExp.test(value)) {
if (!inputElement) {
element = {
...element,
onInput: onInputHandler,
onKeyDown: onKeyDownHandler,
onFocus: e => {
focusMode.current = mode;
onFocusHandler(e);
},
onBlur: onBlurHandler,
onPaste: pasteHandler,
onCopy: event => {
copyHandler(event, mode);
},
placeholderChar
};
}
const DateField = inputElement && React.isValidElement(inputElement) ? inputElement.type : InputElement;
const inputSizeClassName = size && `dnb-date-picker__input--${size}`;
switch (state) {
case 'd':
_pushInstanceProperty(_context = refList.current).call(_context, inputRefs.current[`${mode}DayRef`]);
return React.createElement(React.Fragment, {
key: 'dd' + i
}, React.createElement(DateField, _extends({}, element, {
id: `${id}-${mode}-day`,
key: 'di' + i,
className: classnames("dnb-date-picker__input dnb-date-picker__input--day", element.className, inputSizeClassName),
size: 2,
mask: [/[0-9]/, /[0-9]/],
inputRef: inputRefs.current[`${mode}DayRef`],
onChange: dateSetters[`set_${mode}Day`],
value: inputDates[`${mode}Day`] || '',
"aria-labelledby": `${id}-${mode}-day-label`
})), React.createElement("label", {
key: 'dl' + i,
hidden: true,
id: `${id}-${mode}-day-label`,
htmlFor: `${id}-${mode}-day`
}, isRangeLabel + day));
case 'm':
_pushInstanceProperty(_context2 = refList.current).call(_context2, inputRefs.current[`${mode}MonthRef`]);
return React.createElement(React.Fragment, {
key: 'mm' + i
}, React.createElement(DateField, _extends({}, element, {
id: `${id}-${mode}-month`,
key: 'mi' + i,
className: classnames("dnb-date-picker__input dnb-date-picker__input--month", element.className, inputSizeClassName),
size: 2,
mask: [/[0-9]/, /[0-9]/],
inputRef: inputRefs.current[`${mode}MonthRef`],
onChange: dateSetters[`set_${mode}Month`],
value: inputDates[`${mode}Month`] || '',
"aria-labelledby": `${id}-${mode}-month-label`
})), React.createElement("label", {
key: 'ml' + i,
hidden: true,
id: `${id}-${mode}-month-label`,
htmlFor: `${id}-${mode}-month`
}, isRangeLabel + month));
case 'y':
_pushInstanceProperty(_context3 = refList.current).call(_context3, inputRefs.current[`${mode}YearRef`]);
return React.createElement(React.Fragment, {
key: 'yy' + i
}, React.createElement(DateField, _extends({}, element, {
id: `${id}-${mode}-year`,
key: 'yi' + i,
className: classnames("dnb-date-picker__input dnb-date-picker__input--year", element.className, inputSizeClassName),
size: 4,
mask: [/[0-9]/, /[0-9]/, /[0-9]/, /[0-9]/],
inputRef: inputRefs.current[`${mode}YearRef`],
onChange: dateSetters[`set_${mode}Year`],
value: inputDates[`${mode}Year`] || '',
"aria-labelledby": `${id}-${mode}-year-label`
})), React.createElement("label", {
key: 'yl' + i,
hidden: true,
id: `${id}-${mode}-year-label`,
htmlFor: `${id}-${mode}-year`
}, isRangeLabel + year));
}
}
return React.createElement("span", {
key: 's' + i,
className: "dnb-date-picker--separator",
"aria-hidden": true
}, placeholderChar);
});
}, [maskList, getPlaceholderChar, translation, isRange, separatorRegExp, inputElement, size, onInputHandler, onKeyDownHandler, onBlurHandler, pasteHandler, onFocusHandler, copyHandler, id, dateSetters, inputDates]);
const renderInputElement = useCallback(element => {
refList.current = [];
const startDateList = generateDateList(element, 'start');
const endDateList = generateDateList(element, 'end');
return React.createElement("span", {
id: `${id}-input`,
className: "dnb-date-picker__input__wrapper"
}, startDateList, isRange && (_span || (_span = React.createElement("span", {
className: "dnb-date-picker--separator",
"aria-hidden": true
}, ' – '))), isRange && endDateList);
}, [id, isRange, generateDateList]);
const ariaLabel = useMemo(() => selectedDateTitle ? `${selectedDateTitle}, ${translation.openPickerText}` : translation.openPickerText, [selectedDateTitle, translation]);
validateDOMAttributes(props, attributes);
validateDOMAttributes(null, submitAttributes);
const SubmitElement = useMemo(() => showInput ? SubmitButton : Button, [showInput]);
if (!showInput) {
submitAttributes.innerRef = submitAttributes.ref;
submitAttributes.ref = null;
}
return React.createElement("fieldset", {
className: "dnb-date-picker__fieldset",
lang: lang
}, label && React.createElement("legend", {
className: "dnb-sr-only"
}, label), React.createElement(Input, _extends({
id: `${id}__input`,
input_state: disabled ? 'disabled' : focusState,
input_element: inputElement && typeof inputElement !== 'string' ? typeof inputElement === 'function' ? inputElement(props) : inputElement : renderInputElement,
disabled: disabled || skeleton,
skeleton: skeleton,
size: size,
status: !opened ? status : null,
status_state: statusState
}, statusProps, {
submit_element: React.createElement(SubmitElement, _extends({
id: id,
disabled: disabled,
skeleton: skeleton,
className: classnames(showInput && 'dnb-button--input-button', opened && 'dnb-button--active'),
"aria-label": ariaLabel,
title: title,
size: size,
status: status,
status_state: statusState,
type: "button",
icon: "calendar",
variant: "secondary",
on_submit: onSubmit,
on_click: onSubmit
}, submitAttributes, statusProps)),
lang: lang
}, attributes)));
}
export default DatePickerInput;
function setCursorPosition(target, position = 0, options) {
target.focus();
const select = () => {
target.setSelectionRange(position, position);
};
if (!(options !== null && options !== void 0 && options.withoutDelay) && process.env.NODE_ENV !== 'test') {
setTimeout(select, 0);
} else {
select();
}
}
function InputElement({
className,
value,
...props
}) {
return React.createElement(TextMask, _extends({
guide: true,
inputMode: "numeric",
showMask: true,
keepCharPositions: false,
autoComplete: "off",
autoCapitalize: "none",
spellCheck: false,
autoCorrect: "off",
className: classnames(className, /\d+/.test(String(value)) && 'dnb-date-picker__input--highlight'),
value: value
}, props));
}
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;
}
}
}
const wait = duration => new Promise(r => setTimeout(r, duration));
//# sourceMappingURL=DatePickerInput.js.map