@dnb/eufemia
Version:
DNB Eufemia Design System UI Library
615 lines (614 loc) • 23.2 kB
JavaScript
"use client";
import _extends from "@babel/runtime/helpers/esm/extends";
import _objectWithoutProperties from "@babel/runtime/helpers/esm/objectWithoutProperties";
import _defineProperty from "@babel/runtime/helpers/esm/defineProperty";
var _span;
const _excluded = ["isRange", "maskOrder", "separatorRegExp", "id", "title", "submitAttributes", "maskPlaceholder", "onFocus", "onBlur", "onChange", "onSubmit", "selectedDateTitle", "showInput", "inputElement", "lang", "disabled", "skeleton", "opened", "size", "status", "statusState", "statusProps"],
_excluded2 = ["is_valid", "is_valid_start_date", "is_valid_end_date"],
_excluded3 = ["className", "value"];
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
import React, { useCallback, useContext, useMemo, useRef, useState } from 'react';
import isValid from 'date-fns/isValid';
import parseISO from 'date-fns/parseISO';
import classnames from 'classnames';
import TextMask from '../input-masked/TextMask';
import Button from '../button/Button';
import Input, { SubmitButton } from '../input/Input';
import { warn, validateDOMAttributes, toCapitalized } from '../../shared/component-helper';
import { IS_ANDROID, IS_IOS } from '../../shared/helpers';
import { convertStringToDate } from './DatePickerCalc';
import DatePickerContext from './DatePickerContext';
import { useTranslation } from '../../shared';
import usePartialDates from './hooks/usePartialDates';
import useInputDates from './hooks/useInputDates';
const defaultProps = {
maskOrder: 'dd/mm/yyyy',
maskPlaceholder: 'dd/mm/åååå',
separatorRegExp: /[-/ ]/g,
statusState: 'error',
opened: false
};
function DatePickerInput(externalProps) {
const props = _objectSpread(_objectSpread({}, defaultProps), externalProps);
const {
isRange,
maskOrder,
separatorRegExp,
id,
title,
submitAttributes,
maskPlaceholder,
onFocus,
onBlur,
onChange,
onSubmit,
selectedDateTitle,
showInput,
inputElement,
lang,
disabled,
skeleton,
opened,
size,
status,
statusState,
statusProps
} = props,
attributes = _objectWithoutProperties(props, _excluded);
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,
correctInvalidDate
}
} = useContext(DatePickerContext);
const {
inputDates,
updateInputDates
} = useInputDates({
startDate,
endDate
});
const translation = useTranslation().DatePicker;
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;
}
acc.push(cur);
if (separators.length > 0) {
acc.push(separators.shift());
}
return acc;
}, []);
}, [maskOrder, separatorRegExp]);
const pasteHandler = useCallback(async event => {
if (!focusMode.current) {
return;
}
const success = (event.clipboardData || typeof window !== 'undefined' && window['clipboardData']).getData('text');
if (!success) {
return;
}
event.preventDefault();
try {
const separators = ['.', '/'];
const possibleFormats = ['yyyy-MM-dd'];
possibleFormats.forEach(date => {
separators.forEach(sep => {
possibleFormats.push(date.replace(/-/g, sep));
});
});
possibleFormats.forEach(date => {
possibleFormats.push(date.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(state => {
updateDates({
hoverDate: null
}, dates => {
if (isDateFullyFilledOutRef.current || hasHadValidDate) {
const {
startDate,
endDate,
event
} = _objectSpread(_objectSpread({}, state), dates);
callOnChangeHandler(_objectSpread({
startDate,
endDate,
event
}, invalidDatesRef.current));
}
});
}, [updateDates, callOnChangeHandler, hasHadValidDate]);
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(_objectSpread(_objectSpread({
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(_objectSpread({
partialStartDate: startDate
}, isRange && {
partialEndDate: endDate
}));
const parsedStartDate = parseISO(startDate);
const parsedEndDate = parseISO(endDate);
const isStartDateValid = isValid(parsedStartDate);
const isEndDateValid = isValid(parsedEndDate);
const _getReturnObject = getReturnObject(_objectSpread(_objectSpread({
startDate: isStartDateValid ? parsedStartDate : null,
endDate: isEndDateValid ? parsedEndDate : null,
event
}, partialDatesRef.current), invalidDatesRef.current)),
{
is_valid,
is_valid_start_date,
is_valid_end_date
} = _getReturnObject,
returnObject = _objectWithoutProperties(_getReturnObject, _excluded2);
const typedDates = _objectSpread(_objectSpread(_objectSpread({}, !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 ? void 0 : onType(_objectSpread(_objectSpread({
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 = _objectSpread(_objectSpread({}, invalidDatesRef.current), mode === 'start' ? {
invalidStartDate: null
} : {
invalidEndDate: null
});
callOnChange({
[`${mode}Date`]: date,
event
});
} else {
updateDates({
[`${mode}Date`]: null
});
updateInputDates({
[`${mode}${type}`]: value
});
invalidDatesRef.current = _objectSpread(_objectSpread({}, 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 ? void 0 : onFocus(_objectSpread(_objectSpread({}, 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 ? void 0 : onBlur(_objectSpread(_objectSpread({}, event), getReturnObject(_objectSpread({
event
}, partialDatesRef.current))));
}, [onBlur, getReturnObject, partialDatesRef]);
const onKeyDownHandler = useCallback(async event => {
const keyCode = event.key;
const target = event.target;
if (correctInvalidDate && target.selectionStart !== target.selectionEnd) {
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 = /[0-9]/g.test(keyCode);
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}`](_objectSpread(_objectSpread({
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
});
}
}
}
}, [correctInvalidDate, dateSetters]);
const onInputHandler = useCallback(event => {
const target = event.currentTarget;
if (IS_ANDROID && event.nativeEvent.inputType === 'deleteContentBackward' && target.selectionStart === 0 && target.selectionEnd === 0) {
onKeyDownHandler(_objectSpread(_objectSpread({}, 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) => {
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 = _objectSpread(_objectSpread({}, element), {}, {
onInput: onInputHandler,
onKeyDown: onKeyDownHandler,
onPaste: pasteHandler,
onFocus: e => {
focusMode.current = mode;
onFocusHandler(e);
},
onBlur: onBlurHandler,
placeholderChar
});
}
const DateField = inputElement && React.isValidElement(inputElement) ? inputElement.type : InputElement;
const inputSizeClassName = size && `dnb-date-picker__input--${size}`;
switch (state) {
case 'd':
refList.current.push(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':
refList.current.push(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':
refList.current.push(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, pasteHandler, onBlurHandler, onFocusHandler, 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(_ref) {
let {
className,
value
} = _ref,
props = _objectWithoutProperties(_ref, _excluded3);
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