diff --git a/README.md b/README.md index 9d559dc81..8000f73c8 100644 --- a/README.md +++ b/README.md @@ -156,6 +156,18 @@ Useful recipes for UI patterns appearing in FOLIO modules. * [Show/Hide Columns in MCL](guides/patterns/ColumnSelector.stories.mdx) -- Give users the ability to select only the data they need to see. * [Accessible Routing](guides/patterns/AccessibleRouting.stories.mdx) -- Detail the approaches to implementing accessible focus management. +## Working with dates/times in UI-Modules + +We provide a handful of components and utilities for date/time functionality. + +* **Datepicker, Timepicker, DateRangeWrapper components** - UI-widgets for accepting date/time input. +* **FormattedDate, FormattedUTCDate, FormattedTime** - Cross-browser convenience components for displaying localized representations of system ISO8601 timestamps. +* [dateTimeUtils](util/DateUtils_readme.md) - A handful of utility functions for working with date/time code in application logic. +* **Hooks** + * useFormatDate - presentational date-formatting. + * useFormatTime - presentational time-formatting. + * useDynamicLocale - loading DayJS locale information within functional components (also available in component form, via `DynamicLocaleRenderer`). + ## Testing Stripes Components' tests are automated browser tests powered by [Karma](http://karma-runner.github.io) and written using diff --git a/hooks/useDynamicLocale/DynamicLocaleRenderer.js b/hooks/useDynamicLocale/DynamicLocaleRenderer.js new file mode 100644 index 000000000..7eb9d2d73 --- /dev/null +++ b/hooks/useDynamicLocale/DynamicLocaleRenderer.js @@ -0,0 +1,20 @@ +import { useEffect } from 'react'; +import PropTypes from 'prop-types'; +import useDynamicLocale from './useDynamicLocale'; + +const DynamicLocaleRenderer = ({ children, onLoaded }) => { + const { localeLoaded, isEnglishLang } = useDynamicLocale(); + useEffect(() => { + if (localeLoaded) { + onLoaded({ isEnglishLang }); + } + }, [localeLoaded, onLoaded, isEnglishLang]); + return localeLoaded ? children : null; +}; + +DynamicLocaleRenderer.propTypes = { + children: PropTypes.node, + onLoaded: PropTypes.func, +}; + +export default DynamicLocaleRenderer; diff --git a/hooks/useDynamicLocale/index.js b/hooks/useDynamicLocale/index.js new file mode 100644 index 000000000..698dc5b4b --- /dev/null +++ b/hooks/useDynamicLocale/index.js @@ -0,0 +1,3 @@ +export { default as useDynamicLocale } from './useDynamicLocale'; +export { default as DynamicLocaleRenderer } from './DynamicLocaleRenderer'; + diff --git a/hooks/useDynamicLocale/useDynamicLocale.js b/hooks/useDynamicLocale/useDynamicLocale.js new file mode 100644 index 000000000..1c7ffb2e2 --- /dev/null +++ b/hooks/useDynamicLocale/useDynamicLocale.js @@ -0,0 +1,47 @@ +import React from 'react'; +import { IntlContext } from 'react-intl'; +import { loadDayJSLocale } from '../../util/dateTimeUtils'; + +const isEnglishLang = (locale) => { + return /^en/.test(locale); +}; + +/** + * useDynamicLocale - + * React hook that loads a DayJS locale, returns an + * @date 12/15/2023 - 1:58:58 PM + * + * @param {Object} hookParams + * @param {String} hookParams.locale - locale string ex : 'en-SE' + */ +const useDynamicLocale = ({ locale : localeProp } = {}) => { + const { locale: localeContext } = React.useContext(IntlContext); + const [localeLoaded, setLocaleLoaded] = React.useState( + localeProp ? isEnglishLang(localeProp) : + isEnglishLang(localeContext) + ); + const [prevLocale, updatePrevLocale] = React.useState(localeProp || localeContext); + const locale = localeProp || localeContext; + + React.useEffect(() => { + const localeCallback = (loadedLocale, err) => { + if (!err) { + setLocaleLoaded(true); + } + }; + + loadDayJSLocale(locale, localeCallback); + }, [localeLoaded, locale, prevLocale]); + + if (locale !== prevLocale) { + updatePrevLocale(localeProp || localeContext); + setLocaleLoaded(localeProp ? isEnglishLang(localeProp) : isEnglishLang(localeContext)); + } + + return { + localeLoaded, + isEnglish: localeProp ? localeProp === 'en' : localeContext === 'en' + }; +}; + +export default useDynamicLocale; diff --git a/index.js b/index.js index 0a385a345..433c942a2 100644 --- a/index.js +++ b/index.js @@ -12,7 +12,6 @@ export { staticFirstWeekDay, staticLangCountryCodes } from './lib/Datepicker'; -export { getLocaleDateFormat, getLocalizedTimeFormatInfo } from './util/dateTimeUtils'; export { default as DateRangeWrapper } from './lib/DateRangeWrapper'; export { default as FormattedDate } from './lib/FormattedDate'; export { default as FormattedTime } from './lib/FormattedTime'; @@ -142,6 +141,13 @@ export { default as ExportCsv } from './lib/ExportCsv'; export { default as exportToCsv } from './lib/ExportCsv/exportToCsv'; /* utilities */ +export { + getLocaleDateFormat, + getLocalizedTimeFormatInfo, + dayjs, + DayRange, + loadDayJSLocale, +} from './util/dateTimeUtils'; export { default as RootCloseWrapper } from './util/RootCloseWrapper'; export { default as omitProps } from './util/omitProps'; export { diff --git a/lib/Datepicker/Calendar.js b/lib/Datepicker/Calendar.js index e013d392d..d265a5dff 100644 --- a/lib/Datepicker/Calendar.js +++ b/lib/Datepicker/Calendar.js @@ -2,48 +2,55 @@ * Display calendar UI for datepicker. * Sync the cursor to the selected date or default to today. * Month is rendered based on cursor date. -* Handles date math via moment. +* Handles date math via dayjs. */ import React from 'react'; import { FormattedMessage, injectIntl } from 'react-intl'; import PropTypes from 'prop-types'; -import Moment from 'moment'; -import { extendMoment } from 'moment-range'; +import { isArray } from 'lodash'; +import { dayjs, DayRange } from '../../util/dateTimeUtils'; import IconButton from '../IconButton'; import MonthSelect from './MonthSelect'; - +import DynamicLocaleRenderer from '../../hooks/useDynamicLocale/DynamicLocaleRenderer'; import css from './Calendar.css'; import staticFirstWeekday from './staticFirstWeekDay'; import staticRegions from './staticLangCountryCodes'; -const moment = extendMoment(Moment); +const getRange = (start, end) => { + const range = []; + let current = typeof start === 'string' ? dayjs(start) : start; + while (current.isBefore(end)) { + range.push(current.clone()); + current = current.add(1, 'day'); + } + return range; +}; function getCalendar(year, month, offset = 0) { - const startDate = moment([year, month]); - const firstDay = moment(startDate).startOf('month'); - const endDay = moment(startDate).endOf('month'); - const monthRange = moment.range(firstDay, endDay); + const startDate = dayjs([year, month]); + const firstDay = startDate.startOf('month'); + const endDay = startDate.endOf('month'); + const monthRange = new DayRange(firstDay, endDay).expand(); const weeks = []; const calendar = []; const rowStartArray = []; const rowEndArray = []; - const weekdays = Array.from(monthRange.by('days')); - weekdays.forEach((mo) => { + monthRange.forEach((mo) => { const ref = mo.week(); if (weeks.indexOf(ref) < 0) { weeks.push(mo.week()); - const endClone = moment(mo); - rowStartArray.push(mo.weekday(offset + 0)); - rowEndArray.push(endClone.weekday(offset + 6)); + const endClone = dayjs(mo); + rowStartArray.push(mo.day(offset).toISOString()); + rowEndArray.push(endClone.day(offset + 6).add(1, 'day')); } }); for (let i = 0; i < weeks.length; i += 1) { - const weekRange = moment.range(rowStartArray[i], rowEndArray[i]); + const weekRange = new DayRange(rowStartArray[i], rowEndArray[i]).expand(); calendar.push(weekRange); } @@ -87,8 +94,17 @@ function getFirstWeekday(locale) { return dayMap[firstDay]; } +function getAdjustedLocale(locale) { + let adjustedLocale = locale; + if (adjustedLocale.length === 2) { + const regionDefault = staticRegions[adjustedLocale]; + adjustedLocale = `${adjustedLocale}-${regionDefault}`; + } + return adjustedLocale; +} + const propTypes = { - dateFormat: PropTypes.string, + dateFormat: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]), exclude: PropTypes.func, fillParent: PropTypes.bool, firstFieldRef: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), @@ -124,19 +140,22 @@ class Calendar extends React.Component { constructor(props) { super(props); - moment.locale(this.props.intl.locale || this.props.locale); - const { selectedDate, dateFormat } = this.props; - this.selectedMoment = new moment(); // eslint-disable-line new-cap + this.selectedDate = dayjs(); let cursorDate; if (!selectedDate) { - cursorDate = new moment(); // eslint-disable-line new-cap - } else if (moment(selectedDate, dateFormat, true).isValid()) { - this.selectedMoment = new moment(selectedDate, dateFormat, true); // eslint-disable-line new-cap - cursorDate = this.selectedMoment; + cursorDate = dayjs(); + } else if (dayjs(selectedDate, dateFormat, true).isValid()) { + if (selectedDate.isMoment) { + this.selectedDate = dayjs(selectedDate.toISOString(), dateFormat, true); + console.warn('Calendar component passed a MomentJS object for selected date. Migration to Dayjs is required!') + } else { + this.selectedDate = dayjs(selectedDate, dateFormat, true); + } + cursorDate = this.selectedDate; } else { // no pre-selected date, datestring invalid, init as 'today'. - cursorDate = new moment(); // eslint-disable-line new-cap + cursorDate = dayjs(); } // if the stripes locale has no region (only 2 letters), it needs to be normalized to a @@ -146,21 +165,12 @@ class Calendar extends React.Component { // Hopefully it will be implemented in browser Intl API soon.. // but until then... - let dayOffset = 0; - let adjustedLocale = props.intl.locale || props.locale; - if (adjustedLocale.length === 2) { - const regionDefault = staticRegions[adjustedLocale]; - adjustedLocale = `${adjustedLocale}-${regionDefault}`; - } + const adjustedLocale = getAdjustedLocale(props.intl.locale || props.locale); - // if moment doesn't have the requested locale from above (intl/stripes), it falls back to 'en'. If this - // is the case, we need to set an offset value for correct calendar day rendering - - // otherwise, the calendar columns will be off, resulting misaligned weekdays/calendar days. - if (moment.locale() === 'en') { - dayOffset = getFirstWeekday(adjustedLocale); - } + this.weekDays = getWeekDays(props.intl); + const dayOffset = getFirstWeekday(adjustedLocale); - const base = new moment(cursorDate); // eslint-disable-line new-cap + const base = dayjs(cursorDate); const month = base.month(); const year = base.year(); @@ -169,14 +179,13 @@ class Calendar extends React.Component { this.state = { cursorDate, - date: this.selectedMoment, + date: this.selectedDate, month, year, - calendar: getCalendar(year, month), + calendar: getCalendar(year, month, dayOffset), dayOffset }; - this.weekDays = getWeekDays(props.intl); this.months = getMonths(props.intl); this.firstWeekDay = getFirstWeekday(adjustedLocale); this.firstField = props.firstFieldRef || React.createRef(); @@ -190,8 +199,9 @@ class Calendar extends React.Component { // When the selected date has changed, update the state with it let stateUpdate; - if (nextProps.selectedDate !== prevState.selectedDate) { - const moDate = new moment(nextProps.selectedDate, nextProps.dateFormat, true); // eslint-disable-line new-cap + if (nextProps.selectedDate && + nextProps.selectedDate !== prevState.selectedDate) { + const moDate = dayjs(nextProps.selectedDate, nextProps.dateFormat, true); if (moDate.isValid()) { if (moDate !== prevState.date) { // const moDate = moment(nextProps.selectedDate); @@ -208,7 +218,7 @@ class Calendar extends React.Component { }; } } else { // fix navigation issue on null or invalid date - const fallbackDate = new moment(); // eslint-disable-line new-cap + const fallbackDate = dayjs(); const month = fallbackDate.month(); const year = fallbackDate.year(); stateUpdate = { @@ -283,8 +293,8 @@ class Calendar extends React.Component { moveCursor = (op) => { const curDate = this.state.cursorDate; - op(curDate); // eslint-disable-line new-cap - this.updateCursorDate(curDate); + const newDate = op(curDate); + this.updateCursorDate(newDate); } focusTrap = { @@ -305,8 +315,12 @@ class Calendar extends React.Component { } return newState; }, () => { - const { id, dateFormat } = this.props; + const { id, dateFormat: dateFormatProp } = this.props; const { cursorDate } = this.state; + let dateFormat = dateFormatProp; + if (isArray(dateFormatProp)) { + dateFormat = dateFormatProp[0]; + } const cursorString = cursorDate.format(dateFormat); const nextButtonElem = document.getElementById(`datepicker-choose-date-button-${cursorString}-${id}`); nextButtonElem?.focus(); // eslint-disable-line no-unused-expressions @@ -328,15 +342,18 @@ class Calendar extends React.Component { } isDateSelected = (day) => { + const { + dateFormat + } = this.props; + const format = isArray(dateFormat) ? dateFormat[0] : dateFormat; if (this.props.selectedDate) { - return day.format(this.props.dateFormat) === - new moment(this.props.selectedDate, this.props.dateFormat, true) // eslint-disable-line new-cap - .format(this.props.dateFormat); + return day.format(format) === dayjs(this.props.selectedDate, dateFormat, true) + .format(format); } if (this.state.date) { - return day.format(this.props.dateFormat) === - new moment(this.state.date, this.props.dateFormat, true) // eslint-disable-line new-cap - .format(this.props.dateFormat); + return day.format(format) === + dayjs(this.state.date, dateFormat, true) // eslint-disable-line new-cap + .format(format); } return false; } @@ -357,10 +374,10 @@ class Calendar extends React.Component { this.setState(curState => { const { dayOffset } = curState; let cursorDate = ''; - if (month === this.selectedMoment?.month()) { - cursorDate = this.selectedMoment; + if (month === this.selectedDate?.month()) { + cursorDate = this.selectedDate; } else { - cursorDate = new moment().month(month).date(1).year(curState.year); // eslint-disable-line new-cap + cursorDate = dayjs().month(month).date(1).year(curState.year); // eslint-disable-line new-cap } return { month, @@ -373,7 +390,7 @@ class Calendar extends React.Component { updateYear = (e) => { if (e.target.value) { const year = e.target.value; - if (new moment(year, 'YYYY', true).isValid()) { // eslint-disable-line new-cap + if (dayjs(year, 'YYYY', true).isValid()) { // eslint-disable-line new-cap this.setState(curState => ({ year, calendar: getCalendar(year, curState.month, curState.dayOffset), @@ -403,6 +420,20 @@ class Calendar extends React.Component { }); } + handleLocaleLoaded = ({ isEnglish }) => { + const { intl, locale } = this.props; + if (!isEnglish) { + this.setState(cur => { + const adjustedLocale = getAdjustedLocale(intl.locale || locale); + const dayOffset = getFirstWeekday(adjustedLocale); + return { + calendar: getCalendar(cur.year, cur.month, dayOffset), + dayOffset + }; + }); + } + } + render() { const { id, rootRef, fillParent, trapFocus } = this.props; @@ -412,16 +443,16 @@ class Calendar extends React.Component { (week) => { weekCount += 1; const dayList = []; - const weekDays = Array.from(week.by('days')); - weekDays.forEach((day) => { dayList.push(day); }); + week.forEach((day) => { dayList.push(day); }); const days = dayList.map( (day) => { const { month, cursorDate } = this.state; - const { intl, exclude, dateFormat } = this.props; - const dayMonth = day.month() + 1; - const isCurrentMonth = dayMonth === month + 1; - const isToday = day.isSame(moment(), 'day'); + const { intl, exclude, dateFormat: dateFormatProp } = this.props; + const dateFormat = isArray(dateFormatProp) ? dateFormatProp[0] : dateFormatProp; + const dayMonth = day.month(); + const isCurrentMonth = dayMonth === month; + const isToday = day.isSame(dayjs(), 'day'); const isSelected = this.isDateSelected(day); const isCursored = day.isSame(cursorDate, 'day'); @@ -448,7 +479,8 @@ class Calendar extends React.Component { numericDay = numericDay.replace('MM', new Intl.NumberFormat( intl.locale, { minimumIntegerDigits: 2 } - ).format(dayMonth)) + // JS months are a 0-based index, so for correctly ISO-parseable dates, 1 needs to be added. + ).format(dayMonth + 1)) .replace('YYYY', new Intl.NumberFormat(intl.locale, { style: 'decimal', useGrouping: false }) @@ -512,112 +544,114 @@ class Calendar extends React.Component { } return ( - // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -
{/* eslint-disable jsx-a11y/no-noninteractive-tabindex */} - { trapFocus &&
} -
-
- - { ([ariaLabel]) => ( - this.moveDate('subtract', 'year')} - data-test-calendar-previous-year - aria-label={ariaLabel} - /> - )} - - - { ([ariaLabel]) => ( - this.moveDate('subtract', 'month')} - data-test-calendar-previous-month - aria-label={ariaLabel} - /> - )} - - - { ([ariaLabel]) => ( - - )} - - - { ([ariaLabel]) => ( - - )} - - - { ([ariaLabel]) => ( - this.moveDate('add', 'month')} - data-test-calendar-next-month - aria-label={ariaLabel} - /> - )} - - - { ([ariaLabel]) => ( - this.moveDate('add', 'year')} - data-test-calendar-next-year - aria-label={ariaLabel} - /> - )} + + {/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions */} +
{/* eslint-disable jsx-a11y/no-noninteractive-tabindex */} + {trapFocus &&
} +
+
+ + {([ariaLabel]) => ( + this.moveDate('subtract', 'year')} + data-test-calendar-previous-year + aria-label={ariaLabel} + /> + )} + + + {([ariaLabel]) => ( + this.moveDate('subtract', 'month')} + data-test-calendar-previous-month + aria-label={ariaLabel} + /> + )} + + + {([ariaLabel]) => ( + + )} + + + {([ariaLabel]) => ( + + )} + + + {([ariaLabel]) => ( + this.moveDate('add', 'month')} + data-test-calendar-next-month + aria-label={ariaLabel} + /> + )} + + + {([ariaLabel]) => ( + this.moveDate('add', 'year')} + data-test-calendar-next-year + aria-label={ariaLabel} + /> + )} + +
+
    + {daysOfWeek} +
+ + {([description]) =>
{description}
}
+ + + {weeks} + +
-
    - {daysOfWeek} -
- - { ([description]) =>
{description}
} -
- - - {weeks} - -
+ {trapFocus &&
} + {/* eslint-enable jsx-a11y/no-noninteractive-tabindex */}
- {trapFocus &&
} - {/* eslint-enable jsx-a11y/no-noninteractive-tabindex */} -
+ ); } } diff --git a/lib/Datepicker/Datepicker.js b/lib/Datepicker/Datepicker.js index dd334bbfa..dd7035bd6 100644 --- a/lib/Datepicker/Datepicker.js +++ b/lib/Datepicker/Datepicker.js @@ -1,7 +1,6 @@ import React, { useState, useRef, useEffect } from 'react'; import { FormattedMessage, injectIntl } from 'react-intl'; import PropTypes from 'prop-types'; -import moment from 'moment-timezone'; import uniqueId from 'lodash/uniqueId'; import pick from 'lodash/pick'; import RootCloseWrapper from '../../util/RootCloseWrapper'; @@ -12,25 +11,29 @@ import IconButton from '../IconButton'; import TextField from '../TextField'; import Calendar from './Calendar'; import css from './Calendar.css'; -import { getLocaleDateFormat } from '../../util/dateTimeUtils'; +import { dayjs, getLocaleDateFormat, dateCanBeParsed } from '../../util/dateTimeUtils'; +import { useDynamicLocale } from '../../hooks/useDynamicLocale'; const pickDataProps = (props) => pick(props, (v, key) => key.indexOf('data-test') !== -1); -// Controls the formatting from the value prop to what displays in the UI. +const containsUTCOffset = (value) => { + const offsetRegex = /T[\d.:]+[+-][\d]+$/; + const offsetRE2 = /T[\d:]+[-+][\d:]+\d{2}$/; // sans milliseconds + return offsetRegex.test(value) || offsetRE2.test(value); +}; + +// Controls the formatting from the value prop to what displays in the text input. // need to judge the breakage factor in adopting a spread syntax for these parameters... const defaultParser = (value, timeZone, uiFormat, outputFormats) => { if (!value || value === '') { return value; } - - const offsetRegex = /T[\d.:]+[+-][\d]+$/; - const offsetRE2 = /T[\d:]+[-+][\d:]+\d{2}$/; // sans milliseconds - let inputMoment; - // if date string contains a utc offset, we can parse it as utc time and convert it to selected timezone. - if (offsetRegex.test(value) || offsetRE2.test(value)) { - inputMoment = moment.tz(value, timeZone); + let inputDate; + if (containsUTCOffset(value)) { + inputDate = dayjs.utc(value).tz(timeZone); } else { - inputMoment = moment.tz(value, [uiFormat, ...outputFormats], timeZone); + inputDate = dayjs(value, [uiFormat, ...outputFormats]); } - const inputValue = inputMoment.format(uiFormat); + + const inputValue = inputDate.format(uiFormat); return inputValue; }; @@ -55,10 +58,10 @@ const defaultParser = (value, timeZone, uiFormat, outputFormats) => { */ export const defaultOutputFormatter = ({ backendDateStandard, value, uiFormat, outputFormats, timeZone }) => { if (!value || value === '') { return value; } - const parsed = new moment.tz(value, [uiFormat, ...outputFormats], timeZone); // eslint-disable-line + const parsed = dateCanBeParsed(value, [backendDateStandard, uiFormat, ...outputFormats]); - if (/8601/.test(backendDateStandard)) { - return parsed.toISOString(); + if (!parsed.isValid) { + return ''; } // Use `.locale('en')` before `.format(...)` to get Arabic/"Latn" numerals. @@ -72,17 +75,24 @@ export const defaultOutputFormatter = ({ backendDateStandard, value, uiFormat, o // and about how the locale string may be parsed at // https://www.rfc-editor.org/rfc/rfc5646.html + if (parsed.isValid && /8601/.test(backendDateStandard)) { + if (!timeZone || timeZone?.toLowerCase() === 'utc') { + return dayjs.utc(value, parsed.validFormat).locale('en').format('YYYY-MM-DDTHH:mm:ss.SSS[Z]'); + } + return dayjs.tz(value, parsed.validFormat, timeZone).locale('en').toISOString(); + } + // for support of the RFC2822 format (rare thus far and support may soon be deprecated.) - if (/2822/.test(backendDateStandard)) { + if (parsed.isValid && /2822/.test(backendDateStandard)) { const DATE_RFC2822 = 'ddd, DD MMM YYYY HH:mm:ss ZZ'; - return parsed.locale('en').format(DATE_RFC2822); + return dayjs.tz(value, parsed.validFormat, timeZone).locale('en').format(DATE_RFC2822); } // if a localized string dateformat has been passed, normalize the date first... // otherwise, localized strings could be submitted to the backend. - const normalizedDate = moment.utc(value, [uiFormat, ...outputFormats]); + const normalizedDate = dayjs.utc(value, [backendDateStandard, uiFormat, ...outputFormats]).toISOString(); - return new moment(normalizedDate, 'YYYY-MM-DD').locale('en').format(backendDateStandard); // eslint-disable-line + return dayjs(normalizedDate, 'YYYY-MM-DD').locale('en').format(backendDateStandard); // eslint-disable-line }; const propTypes = { @@ -122,7 +132,7 @@ const propTypes = { const getBackendDateStandard = (standard, use) => { if (!use) return undefined; - if (standard === 'ISO8601') return ['YYYY-MM-DDTHH:mm:ss.sssZ', 'YYYY-MM-DDTHH:mm:ssZ']; + if (standard === 'ISO8601') return ['YYYY-MM-DDTHH:mm:ss.SSS[Z]', 'YYYY-MM-DDTHH:mm:ss[Z]']; if (standard === 'RFC2822') return ['ddd, DD MMM YYYY HH:mm:ss ZZ']; return [standard, 'YYYY-MM-DDTHH:mm:ss.sssZ', 'ddd, DD MMM YYYY HH:mm:ss ZZ']; }; @@ -186,6 +196,7 @@ const Datepicker = ( outputFormats: getBackendDateStandard(backendDateStandard, true) }) : null }); + const { localeLoaded } = useDynamicLocale({ locale }); // since updating the Datepair object isn't quite enough to prompt a re-render when its only partially // updated, need to maintain a 2nd field containing only the displayed value. // this resolves issue with the clearIcon not showing up. @@ -205,7 +216,10 @@ const Datepicker = ( // handle value changes that originate outside of the component. useEffect(() => { - if (typeof valueProp !== 'undefined' && valueProp !== datePair.dateString && valueProp !== datePair.formatted) { + if (input.current + && typeof valueProp !== 'undefined' + && valueProp !== datePair.dateString + && valueProp !== datePair.formatted) { payload.current = Object.assign(payload.current, maybeUpdateValue(valueProp)); nativeChangeField(input, false, payload.current.dateString); } @@ -222,13 +236,20 @@ const Datepicker = ( return blankDates; } - // use strict mode to check validity - incomplete dates, anything not conforming to the format will be invalid + let valueMoment; const backendStandard = getBackendDateStandard(backendDateStandard, outputBackendValue); - const valueMoment = new moment(// eslint-disable-line new-cap - value, - [format, ...backendStandard], // pass array of possible formats () - true - ); + + if (containsUTCOffset(value)) { + valueMoment = dayjs(value); + } else { + // use strict mode to check validity - incomplete dates, anything not conforming to the format will be invalid + valueMoment = dayjs( + value, + [format, ...backendStandard], // pass array of possible formats () + true + ); + } + const isValid = valueMoment.isValid(); let dates; @@ -258,7 +279,7 @@ const Datepicker = ( return dates; } return {}; - } else if (value !== datePair.dateString) { + } else if (value !== datePair.dateString) { // if the date's not valid, we just update the datestring... dates = { dateString: value, formatted: '' @@ -374,7 +395,7 @@ const Datepicker = (