UNPKG

@dnb/eufemia

Version:

DNB Eufemia Design System UI Library

624 lines (623 loc) 23.6 kB
"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"]; import "core-js/modules/es.string.replace.js"; import "core-js/modules/web.dom-collections.iterator.js"; 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(_ref => { let { startDate, endDate, event } = _ref; 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(_ref2 => { let { event } = _ref2; 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(_ref3 => { let { current } = _ref3; return 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) { let position = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; let options = arguments.length > 2 ? arguments[2] : undefined; 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(_ref4) { let { className, value } = _ref4, props = _objectWithoutProperties(_ref4, _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