Migrate to expo/ui DatePicker

This commit is contained in:
Oleksii Bulenok
2026-08-18 14:42:19 +02:00
parent 48f96d6421
commit 227ab7c99a
6 changed files with 59 additions and 75 deletions
+1 -1
View File
@@ -121,6 +121,7 @@
"@emoji-mart/data": "^1.2.1", "@emoji-mart/data": "^1.2.1",
"@emoji-mart/react": "^1.1.1", "@emoji-mart/react": "^1.1.1",
"@expo/html-elements": "^0.12.5", "@expo/html-elements": "^0.12.5",
"@expo/ui": "~57.0.11",
"@expo/webpack-config": "^19.0.1", "@expo/webpack-config": "^19.0.1",
"@floating-ui/dom": "^1.6.3", "@floating-ui/dom": "^1.6.3",
"@floating-ui/react-dom": "^2.0.8", "@floating-ui/react-dom": "^2.0.8",
@@ -230,7 +231,6 @@
"react-is": "19", "react-is": "19",
"react-keyed-flatten-children": "^5.0.0", "react-keyed-flatten-children": "^5.0.0",
"react-native": "0.86.0", "react-native": "0.86.0",
"react-native-date-picker": "^5.0.13",
"react-native-device-attest": "^0.1.6", "react-native-device-attest": "^0.1.6",
"react-native-drawer-layout": "^4.2.3", "react-native-drawer-layout": "^4.2.3",
"react-native-edge-to-edge": "^1.8.1", "react-native-edge-to-edge": "^1.8.1",
@@ -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 <UIKit/UIKit.h>
+#include <string>
@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"
}
}
}
-1
View File
@@ -38,7 +38,6 @@ patchedDependencies:
'expo-notifications@57.0.7': patches/expo-notifications@57.0.7.patch '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-updates@57.0.10': patches/expo-updates@57.0.10.patch
expo@57.0.8: patches/expo@57.0.8.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-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-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 'react-native-keyboard-controller@1.21.9': patches/react-native-keyboard-controller@1.21.9.patch
@@ -1,11 +1,13 @@
import {useCallback, useImperativeHandle, useState} from 'react' import {useCallback, useImperativeHandle, useState} from 'react'
import {Keyboard} from 'react-native' import {Keyboard} from 'react-native'
import DatePicker from 'react-native-date-picker' import {DatePickerDialog, Host} from '@expo/ui/jetpack-compose'
import {useLingui} from '@lingui/react'
import {useTheme} from '#/alf' import {useTheme} from '#/alf'
import {type DateFieldProps} from '#/components/forms/DateField/types' 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 * as TextField from '#/components/forms/TextField'
import {DateFieldButton} from './index.shared' import {DateFieldButton} from './index.shared'
@@ -20,20 +22,18 @@ export function DateField({
placeholder, placeholder,
label, label,
isInvalid, isInvalid,
testID,
accessibilityHint, accessibilityHint,
maximumDate, maximumDate,
minimumDate, minimumDate,
}: DateFieldProps) { }: DateFieldProps) {
const {i18n} = useLingui()
const t = useTheme() const t = useTheme()
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
/* /*
* The picker requires a valid date, so when value is empty we open at * The picker requires a valid date, so when value is empty we open at
* maximumDate (if set) or today. Normalize through toSimpleDateString so a * maximumDate (if set) or today. Normalize through toSimpleDateString so a
* date-only value is parsed as UTC midnight, consistent with the picker's * date-only value is parsed as UTC midnight, which is what the Material 3
* timeZoneOffsetInMinutes={0} and the maximumDate below. * picker expects for its selected date (it works in UTC day millis).
*/ */
const initialDate = const initialDate =
value === '' value === ''
@@ -42,6 +42,19 @@ export function DateField({
: new Date() : new Date()
: new Date(toSimpleDateString(value)) : 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( const onChangeInternal = useCallback(
(date: Date) => { (date: Date) => {
setOpen(false) setOpen(false)
@@ -86,31 +99,15 @@ export function DateField({
accessibilityHint={accessibilityHint} accessibilityHint={accessibilityHint}
/> />
{open && ( {open && (
// Android implementation of DatePicker currently does not change default button colors according to theme and only takes hex values for buttonColor <Host colorScheme={t.scheme}>
// Can remove the buttonColor setting if/when this PR is merged: https://github.com/henninghall/react-native-date-picker/pull/871 <DatePickerDialog
<DatePicker initialDate={initialDate.toISOString()}
modal selectableDates={selectableDates}
open showVariantToggle={false}
timeZoneOffsetInMinutes={0} onDateSelected={onChangeInternal}
theme={t.scheme} onDismissRequest={onCancel}
buttonColor={t.name === 'light' ? '#000000' : '#ffffff'} />
date={initialDate} </Host>
onConfirm={onChangeInternal}
onCancel={onCancel}
mode="date"
locale={i18n.locale}
is24hourSource="locale"
testID={`${testID}-datepicker`}
aria-label={label}
accessibilityLabel={label}
accessibilityHint={accessibilityHint}
maximumDate={
maximumDate ? new Date(toSimpleDateString(maximumDate)) : undefined
}
minimumDate={
minimumDate ? new Date(toSimpleDateString(minimumDate)) : undefined
}
/>
)} )}
</> </>
) )
+19 -10
View File
@@ -1,6 +1,6 @@
import {useCallback, useImperativeHandle, useState} from 'react' import {useCallback, useImperativeHandle, useState} from 'react'
import {Keyboard, View} from 'react-native' 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 {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro' import {Trans} from '@lingui/react/macro'
@@ -16,6 +16,15 @@ import {DateFieldButton} from './index.shared'
export * as utils from '#/components/forms/DateField/utils' export * as utils from '#/components/forms/DateField/utils'
export const LabelText = TextField.LabelText 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-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. * Date objects are converted to strings in the format YYYY-MM-DD.
@@ -78,6 +87,7 @@ export function DateField({
}), }),
[control, value, fallbackDate], [control, value, fallbackDate],
) )
console.log('locale===', i18n.locale, toAppleLocale(i18n.locale))
return ( return (
<> <>
@@ -101,17 +111,16 @@ export function DateField({
<Dialog.ScrollableInner label={label}> <Dialog.ScrollableInner label={label}>
<View style={a.gap_lg}> <View style={a.gap_lg}>
<View style={[a.relative, a.w_full, a.align_center]}> <View style={[a.relative, a.w_full, a.align_center]}>
<DatePicker <DateTimePicker
timeZoneOffsetInMinutes={0} style={a.w_full}
theme={t.scheme} value={new Date(draft)}
date={new Date(draft)} onValueChange={(_event, date) => onChangeInternal(date)}
onDateChange={onChangeInternal}
mode="date" mode="date"
locale={i18n.locale} display="spinner"
timeZoneName="UTC"
themeVariant={t.scheme}
locale={toAppleLocale(i18n.locale)}
testID={`${testID}-datepicker`} testID={`${testID}-datepicker`}
aria-label={label}
accessibilityLabel={label}
accessibilityHint={accessibilityHint}
maximumDate={ maximumDate={
maximumDate maximumDate
? new Date(toSimpleDateString(maximumDate)) ? new Date(toSimpleDateString(maximumDate))
+10
View File
@@ -3,3 +3,13 @@ export function toSimpleDateString(date: Date | string): string {
const _date = typeof date === 'string' ? new Date(date) : date const _date = typeof date === 'string' ? new Date(date) : date
return _date.toISOString().split('T')[0] 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)
}