diff --git a/package.json b/package.json index 45fc1102cc..6991f54a83 100644 --- a/package.json +++ b/package.json @@ -121,6 +121,7 @@ "@emoji-mart/data": "^1.2.1", "@emoji-mart/react": "^1.1.1", "@expo/html-elements": "^0.12.5", + "@expo/ui": "~57.0.11", "@expo/webpack-config": "^19.0.1", "@floating-ui/dom": "^1.6.3", "@floating-ui/react-dom": "^2.0.8", @@ -230,7 +231,6 @@ "react-is": "19", "react-keyed-flatten-children": "^5.0.0", "react-native": "0.86.0", - "react-native-date-picker": "^5.0.13", "react-native-device-attest": "^0.1.6", "react-native-drawer-layout": "^4.2.3", "react-native-edge-to-edge": "^1.8.1", diff --git a/patches/react-native-date-picker@5.0.13.patch b/patches/react-native-date-picker@5.0.13.patch deleted file mode 100644 index 494cdb9320..0000000000 --- a/patches/react-native-date-picker@5.0.13.patch +++ /dev/null @@ -1,31 +0,0 @@ -diff --git a/ios/RNDatePicker.h b/ios/RNDatePicker.h -index 480746eb7acfbe86f67547d9e1de7a5be4d5faf2..13d30cb547195993dfcb4005cc0d248de9ac391a 100644 ---- a/ios/RNDatePicker.h -+++ b/ios/RNDatePicker.h -@@ -15,6 +15,7 @@ NS_ASSUME_NONNULL_END - #else - #import "DatePicker.h" - #import -+#include - - @interface RNDatePicker : DatePicker - -@@ -22,4 +23,3 @@ NS_ASSUME_NONNULL_END - @end - - #endif -- -diff --git a/package.json b/package.json -index 469386dc2e81ded8994818dd4340409da1396488..36460e4e303ae56534c4d389471fc827433c7028 100644 ---- a/package.json -+++ b/package.json -@@ -70,9 +70,6 @@ - "ios": { - "componentProvider": { - "RNDatePicker": "RNDatePicker" -- }, -- "modulesProvider": { -- "RNDatePicker": "RNDatePickerManager" - } - } - } diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 1bdc6c2632..a0c9250b17 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -38,7 +38,6 @@ patchedDependencies: 'expo-notifications@57.0.7': patches/expo-notifications@57.0.7.patch 'expo-updates@57.0.10': patches/expo-updates@57.0.10.patch expo@57.0.8: patches/expo@57.0.8.patch - 'react-native-date-picker@5.0.13': patches/react-native-date-picker@5.0.13.patch 'react-native-dotenv@3.4.11': patches/react-native-dotenv@3.4.11.patch 'react-native-drawer-layout@4.2.3': patches/react-native-drawer-layout@4.2.3.patch 'react-native-keyboard-controller@1.21.9': patches/react-native-keyboard-controller@1.21.9.patch diff --git a/src/components/forms/DateField/index.android.tsx b/src/components/forms/DateField/index.android.tsx index e906c16355..008f87587a 100644 --- a/src/components/forms/DateField/index.android.tsx +++ b/src/components/forms/DateField/index.android.tsx @@ -1,11 +1,13 @@ import {useCallback, useImperativeHandle, useState} from 'react' import {Keyboard} from 'react-native' -import DatePicker from 'react-native-date-picker' -import {useLingui} from '@lingui/react' +import {DatePickerDialog, Host} from '@expo/ui/jetpack-compose' import {useTheme} from '#/alf' import {type DateFieldProps} from '#/components/forms/DateField/types' -import {toSimpleDateString} from '#/components/forms/DateField/utils' +import { + toLocalMidnight, + toSimpleDateString, +} from '#/components/forms/DateField/utils' import * as TextField from '#/components/forms/TextField' import {DateFieldButton} from './index.shared' @@ -20,20 +22,18 @@ export function DateField({ placeholder, label, isInvalid, - testID, accessibilityHint, maximumDate, minimumDate, }: DateFieldProps) { - const {i18n} = useLingui() const t = useTheme() const [open, setOpen] = useState(false) /* * The picker requires a valid date, so when value is empty we open at * maximumDate (if set) or today. Normalize through toSimpleDateString so a - * date-only value is parsed as UTC midnight, consistent with the picker's - * timeZoneOffsetInMinutes={0} and the maximumDate below. + * date-only value is parsed as UTC midnight, which is what the Material 3 + * picker expects for its selected date (it works in UTC day millis). */ const initialDate = value === '' @@ -42,6 +42,19 @@ export function DateField({ : new Date() : new Date(toSimpleDateString(value)) + /* + * Unlike the selection, the selectable range is resolved by the native side + * in the device time zone, so pass local midnight to keep the intended + * calendar day for devices west of UTC. + */ + const selectableDates = + minimumDate || maximumDate + ? { + start: minimumDate ? toLocalMidnight(minimumDate) : undefined, + end: maximumDate ? toLocalMidnight(maximumDate) : undefined, + } + : undefined + const onChangeInternal = useCallback( (date: Date) => { setOpen(false) @@ -86,31 +99,15 @@ export function DateField({ accessibilityHint={accessibilityHint} /> {open && ( - // Android implementation of DatePicker currently does not change default button colors according to theme and only takes hex values for buttonColor - // Can remove the buttonColor setting if/when this PR is merged: https://github.com/henninghall/react-native-date-picker/pull/871 - + + + )} ) diff --git a/src/components/forms/DateField/index.tsx b/src/components/forms/DateField/index.tsx index 3c909fb335..3031e47b48 100644 --- a/src/components/forms/DateField/index.tsx +++ b/src/components/forms/DateField/index.tsx @@ -1,6 +1,6 @@ import {useCallback, useImperativeHandle, useState} from 'react' import {Keyboard, View} from 'react-native' -import DatePicker from 'react-native-date-picker' +import {DateTimePicker} from '@expo/ui/community/datetime-picker' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' @@ -16,6 +16,15 @@ import {DateFieldButton} from './index.shared' export * as utils from '#/components/forms/DateField/utils' export const LabelText = TextField.LabelText +/** + * SwiftUI only accepts identifiers from `Locale.availableIdentifiers`, which + * use underscores (`pt_BR`) rather than BCP 47 hyphens (`pt-BR`). Unknown + * identifiers make the picker fall back to the system locale. + */ +function toAppleLocale(locale: string): string { + return locale.replace(/-/g, '_') +} + /** * Date-only input. Accepts a string in the format YYYY-MM-DD, or a Date object. * Date objects are converted to strings in the format YYYY-MM-DD. @@ -78,6 +87,7 @@ export function DateField({ }), [control, value, fallbackDate], ) + console.log('locale===', i18n.locale, toAppleLocale(i18n.locale)) return ( <> @@ -101,17 +111,16 @@ export function DateField({ - onChangeInternal(date)} mode="date" - locale={i18n.locale} + display="spinner" + timeZoneName="UTC" + themeVariant={t.scheme} + locale={toAppleLocale(i18n.locale)} testID={`${testID}-datepicker`} - aria-label={label} - accessibilityLabel={label} - accessibilityHint={accessibilityHint} maximumDate={ maximumDate ? new Date(toSimpleDateString(maximumDate)) diff --git a/src/components/forms/DateField/utils.ts b/src/components/forms/DateField/utils.ts index 04bb482ce1..06fa2f55a3 100644 --- a/src/components/forms/DateField/utils.ts +++ b/src/components/forms/DateField/utils.ts @@ -3,3 +3,13 @@ export function toSimpleDateString(date: Date | string): string { const _date = typeof date === 'string' ? new Date(date) : date return _date.toISOString().split('T')[0] } + +/** + * Returns a Date at local midnight of the given calendar day. Use when a + * native API resolves timestamps in the device time zone rather than UTC, so + * the intended YYYY-MM-DD is preserved regardless of the device offset. + */ +export function toLocalMidnight(date: Date | string): Date { + const [year, month, day] = toSimpleDateString(date).split('-').map(Number) + return new Date(year, month - 1, day) +}