Skip to content

Commit 8b0ae90

Browse files
authored
fix(ui): timezone issue related to date only fields in Pacific timezones (#11203)
Fixes #10962 This fix addresses fields with timezones enabled specifically for not time pickers. If all you want to do is pick a date such as 14th Feb, it would store the incorrect version and display a date in the future for people in the Pacific. This is because Auckland is +12 offset, but +13 with Daylight Savings Time. In our date picker we try to normalise date pickers with no time to 12pm and so half the year we ended up pushing dates visually to the next day for people in the pacific only. Other regions were not affected by this because their offset would be less than 12. This PR fixes this by ensuring that our dates are always normalised to selected timezone's 12pm date to UTC. There's also additional tests for these two fields from 3 main locations to cover a wider range of possible timezones.
1 parent 80b33ad commit 8b0ae90

File tree

3 files changed

+845
-100
lines changed

3 files changed

+845
-100
lines changed

docs/fields/date.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,4 +242,6 @@ You can customise the available list of timezones in the [global admin config](.
242242
<Banner type='info'>
243243
**Good to know:**
244244
The date itself will be stored in UTC so it's up to you to handle the conversion to the user's timezone when displaying the date in your frontend.
245+
246+
Dates without a specific time are normalised to 12:00 in the selected timezone.
245247
</Banner>

packages/ui/src/fields/DateTime/index.tsx

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ const DateTimeFieldComponent: DateFieldClientComponent = (props) => {
3838
validate,
3939
} = props
4040

41+
const pickerAppearance = datePickerProps?.pickerAppearance || 'default'
42+
4143
// Get the user timezone so we can adjust the displayed value against it
4244
const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone
4345

@@ -59,15 +61,19 @@ const DateTimeFieldComponent: DateFieldClientComponent = (props) => {
5961
setValue,
6062
showError,
6163
value,
62-
} = useField<Date>({
64+
} = useField<string>({
6365
path,
6466
validate: memoizedValidate,
6567
})
6668

6769
const timezonePath = path + '_tz'
6870
const timezoneField = useFormFields(([fields, _]) => fields?.[timezonePath])
6971
const supportedTimezones = config.admin.timezones.supportedTimezones
70-
72+
/**
73+
* Date appearance doesn't include timestamps,
74+
* which means we need to pin the time to always 12:00 for the selected date
75+
*/
76+
const isDateOnly = ['dayOnly', 'default', 'monthOnly'].includes(pickerAppearance)
7177
const selectedTimezone = timezoneField?.value as string
7278

7379
// The displayed value should be the original value, adjusted to the user's timezone
@@ -99,15 +105,28 @@ const DateTimeFieldComponent: DateFieldClientComponent = (props) => {
99105
if (!readOnly) {
100106
if (timezone && selectedTimezone && incomingDate) {
101107
// Create TZDate instances for the selected timezone
102-
const tzDateWithUTC = TZDate.tz(selectedTimezone)
103-
104-
// Creates a TZDate instance for the user's timezone — this is default behaviour of TZDate as it wraps the Date constructor
105-
const dateToUserTz = new TZDate(incomingDate)
106-
107-
// Transpose the date to the selected timezone
108-
const dateWithTimezone = transpose(dateToUserTz, tzDateWithUTC)
109-
110-
setValue(dateWithTimezone.toISOString() || null)
108+
const TZDateWithSelectedTz = TZDate.tz(selectedTimezone)
109+
110+
if (isDateOnly) {
111+
// We need to offset this hardcoded hour offset from the DatePicker elemenent
112+
// this can be removed in 4.0 when we remove the hardcoded offset as it is a breaking change
113+
// const tzOffset = incomingDate.getTimezoneOffset() / 60
114+
const incomingOffset = incomingDate.getTimezoneOffset() / 60
115+
const originalHour = incomingDate.getHours() + incomingOffset
116+
incomingDate.setHours(originalHour)
117+
118+
// Convert the original date as picked into the desired timezone.
119+
const dateToSelectedTz = transpose(incomingDate, TZDateWithSelectedTz)
120+
121+
setValue(dateToSelectedTz.toISOString() || null)
122+
} else {
123+
// Creates a TZDate instance for the user's timezone — this is default behaviour of TZDate as it wraps the Date constructor
124+
const dateToUserTz = new TZDate(incomingDate)
125+
// Transpose the date to the selected timezone
126+
const dateWithTimezone = transpose(dateToUserTz, TZDateWithSelectedTz)
127+
128+
setValue(dateWithTimezone.toISOString() || null)
129+
}
111130
} else {
112131
setValue(incomingDate?.toISOString() || null)
113132
}
@@ -173,7 +192,6 @@ const DateTimeFieldComponent: DateFieldClientComponent = (props) => {
173192
selectedTimezone={selectedTimezone}
174193
/>
175194
)}
176-
177195
{AfterInput}
178196
</div>
179197
<RenderCustomComponent

0 commit comments

Comments
 (0)