Skip to content

Commit 7d2ed73

Browse files
committed
fix(ui-date-input): make DateInput2 date parsing work in every locale and timezone
1 parent d140fe3 commit 7d2ed73

File tree

5 files changed

+106
-39
lines changed

5 files changed

+106
-39
lines changed

packages/ui-date-input/src/DateInput2/index.tsx

Lines changed: 12 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
import {
2626
useState,
2727
useEffect,
28-
useContext,
2928
forwardRef,
3029
ForwardedRef,
3130
ValidationMap
@@ -41,8 +40,7 @@ import {
4140
import { Popover } from '@instructure/ui-popover'
4241
import { TextInput } from '@instructure/ui-text-input'
4342
import { callRenderProp, passthroughProps } from '@instructure/ui-react-utils'
44-
45-
import { ApplyLocaleContext, Locale } from '@instructure/ui-i18n'
43+
import { getLocale, getTimezone } from '@instructure/ui-i18n'
4644

4745
import { propTypes } from './props'
4846
import type { DateInput2Props } from './props'
@@ -93,11 +91,6 @@ function parseLocaleDate(
9391
// create utc date from year, month (zero indexed) and day
9492
const date = new Date(Date.UTC(year, month - 1, day))
9593

96-
if (date.getMonth() !== month - 1 || date.getDate() !== day) {
97-
// Check if the Date object adjusts the values. If it does, the input is invalid.
98-
return null
99-
}
100-
10194
// Format date string in the provided timezone. The locale here is irrelevant, we only care about how to time is adjusted for the timezone.
10295
const parts = new Intl.DateTimeFormat('en-US', {
10396
timeZone,
@@ -166,27 +159,8 @@ const DateInput2 = forwardRef(
166159
}: DateInput2Props,
167160
ref: ForwardedRef<TextInput>
168161
) => {
169-
const localeContext = useContext(ApplyLocaleContext)
170-
171-
const getLocale = () => {
172-
if (locale) {
173-
return locale
174-
} else if (localeContext.locale) {
175-
return localeContext.locale
176-
}
177-
// default to the system's locale
178-
return Locale.browserLocale()
179-
}
180-
181-
const getTimezone = () => {
182-
if (timezone) {
183-
return timezone
184-
} else if (localeContext.timezone) {
185-
return localeContext.timezone
186-
}
187-
// default to the system's timezone
188-
return Intl.DateTimeFormat().resolvedOptions().timeZone
189-
}
162+
const userLocale = locale || getLocale()
163+
const userTimezone = timezone || getTimezone()
190164

191165
const [inputMessages, setInputMessages] = useState<FormMessage[]>(
192166
messages || []
@@ -213,28 +187,28 @@ const DateInput2 = forwardRef(
213187
if (dateFormat) {
214188
if (typeof dateFormat === 'string') {
215189
// use dateFormat instead of the user locale
216-
date = parseLocaleDate(dateString, dateFormat, getTimezone())
190+
date = parseLocaleDate(dateString, dateFormat, userTimezone)
217191
} else if (dateFormat.parser) {
218192
date = dateFormat.parser(dateString)
219193
}
220194
} else {
221195
// no dateFormat prop passed, use locale for formatting
222-
date = parseLocaleDate(dateString, getLocale(), getTimezone())
196+
date = parseLocaleDate(dateString, userLocale, userTimezone)
223197
}
224198
return date ? [formatDate(date), date.toISOString()] : ['', '']
225199
}
226200

227201
const formatDate = (
228202
date: Date,
229-
timeZone: string = getTimezone()
203+
timeZone: string = userTimezone
230204
): string => {
231205
// use formatter function if provided
232206
if (typeof dateFormat !== 'string' && dateFormat?.formatter) {
233207
return dateFormat.formatter(date)
234208
}
235209
// if dateFormat set to a locale, use that, otherwise default to the user's locale
236210
return date.toLocaleDateString(
237-
typeof dateFormat === 'string' ? dateFormat : getLocale(),
211+
typeof dateFormat === 'string' ? dateFormat : userLocale,
238212
{
239213
timeZone,
240214
calendar: 'gregory',
@@ -253,9 +227,9 @@ const DateInput2 = forwardRef(
253227
}
254228

255229
// Replace the matched number with the same number of dashes
256-
const year = `${exampleDate.getFullYear()}`
257-
const month = `${exampleDate.getMonth() + 1}`
258-
const day = `${exampleDate.getDate()}`
230+
const year = '2024'
231+
const month = '9'
232+
const day = '1'
259233
return formattedDate
260234
.replace(regex(year), (match) => 'Y'.repeat(match.length))
261235
.replace(regex(month), (match) => 'M'.repeat(match.length))
@@ -340,8 +314,8 @@ const DateInput2 = forwardRef(
340314
selectedDate={selectedDate}
341315
disabledDates={disabledDates}
342316
visibleMonth={selectedDate}
343-
locale={getLocale()}
344-
timezone={getTimezone()}
317+
locale={userLocale}
318+
timezone={userTimezone}
345319
renderNextMonthButton={
346320
<IconButton
347321
size="small"

packages/ui-i18n/src/Locale.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
* SOFTWARE.
2323
*/
2424

25+
// TODO: there is an improved replacement for this helper at `ui-i18n/src/getLocale.ts`
26+
// all uses of this should be updated and this helper deleted
2527
import { canUseDOM } from '@instructure/ui-dom-utils'
2628

2729
const defaultLocale = 'en-US'

packages/ui-i18n/src/getLocale.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* The MIT License (MIT)
3+
*
4+
* Copyright (c) 2015 - present Instructure, Inc.
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in all
14+
* copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
* SOFTWARE.
23+
*/
24+
import { useContext } from 'react'
25+
import { ApplyLocaleContext } from '.'
26+
27+
// TODO: this is a better replacement for `ui-i18n/src/Locale.ts` which should be deleted in the future
28+
export function getLocale(defaultLocale = 'en-US') {
29+
const localeContext = useContext(ApplyLocaleContext)
30+
if (localeContext.locale) {
31+
return localeContext.locale
32+
}
33+
34+
if (typeof navigator !== 'undefined') {
35+
if (navigator.languages && navigator.languages.length) {
36+
return navigator.languages[0]
37+
}
38+
if (navigator.language) {
39+
return navigator.language
40+
}
41+
}
42+
try {
43+
// This is generally reliable if Intl is supported
44+
return new Intl.DateTimeFormat().resolvedOptions().locale
45+
} catch (e) {
46+
console.warn(
47+
'Intl.DateTimeFormat().resolvedOptions().locale failed, using fallback.'
48+
)
49+
// Fall through to default
50+
}
51+
return defaultLocale
52+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* The MIT License (MIT)
3+
*
4+
* Copyright (c) 2015 - present Instructure, Inc.
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in all
14+
* copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
* SOFTWARE.
23+
*/
24+
import { useContext } from 'react'
25+
import { ApplyLocaleContext } from '.'
26+
27+
// TODO: this is a better replacement for `ui-i18n/src/Locale.ts` which should be deleted in the future
28+
export function getTimezone() {
29+
const localeContext = useContext(ApplyLocaleContext)
30+
if (localeContext.timezone) {
31+
return localeContext.timezone
32+
}
33+
34+
return Intl.DateTimeFormat().resolvedOptions().timeZone
35+
}

packages/ui-i18n/src/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,11 @@ export { textDirectionContextConsumer } from './textDirectionContextConsumer'
2929
export { DateTime } from './DateTime'
3030
export { getTextDirection } from './getTextDirection'
3131
export { I18nPropTypes } from './I18nPropTypes'
32-
export { Locale } from './Locale'
32+
33+
export { Locale } from './Locale' // TODO delete this and only keep the ones below
34+
export { getLocale } from './getLocale'
35+
export { getTimezone } from './getTimezone'
36+
3337
export { DIRECTION, TextDirectionContext } from './TextDirectionContext'
3438

3539
export type { Moment } from './DateTime'

0 commit comments

Comments
 (0)