Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Date input improvements (#7639)

* add max date, use modern field for birthday input

* rm legacy date input

* handle simplifying to simpleDateString internally

* update jsdoc

authored by

Samuel Newman and committed by
GitHub
23e62b18 25991af7

+37 -203
+6 -10
src/components/dialogs/BirthDateSettings.tsx
··· 12 12 usePreferencesSetBirthDateMutation, 13 13 } from '#/state/queries/preferences' 14 14 import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' 15 - import {DateInput} from '#/view/com/util/forms/DateInput' 16 15 import {atoms as a, useTheme} from '#/alf' 17 16 import * as Dialog from '#/components/Dialog' 17 + import {DateField} from '#/components/forms/DateField' 18 18 import {Loader} from '#/components/Loader' 19 + import {Text} from '#/components/Typography' 19 20 import {Button, ButtonIcon, ButtonText} from '../Button' 20 - import {Text} from '../Typography' 21 21 22 22 export function BirthDateSettingsDialog({ 23 23 control, ··· 95 95 return ( 96 96 <View style={a.gap_lg} testID="birthDateSettingsDialog"> 97 97 <View style={isIOS && [a.w_full, a.align_center]}> 98 - <DateInput 99 - handleAsUTC 98 + <DateField 100 99 testID="birthdayInput" 101 100 value={date} 102 - onChange={setDate} 103 - buttonType="default-light" 104 - buttonStyle={[a.rounded_sm]} 105 - buttonLabelType="lg" 106 - accessibilityLabel={_(msg`Birthday`)} 101 + onChangeDate={newDate => setDate(new Date(newDate))} 102 + label={_(msg`Birthday`)} 107 103 accessibilityHint={_(msg`Enter your birth date`)} 108 - accessibilityLabelledBy="birthDate" 104 + maximumDate={new Date()} 109 105 /> 110 106 </View> 111 107
+4
src/components/forms/DateField/index.android.tsx
··· 17 17 isInvalid, 18 18 testID, 19 19 accessibilityHint, 20 + maximumDate, 20 21 }: DateFieldProps) { 21 22 const t = useTheme() 22 23 const [open, setOpen] = React.useState(false) ··· 67 68 aria-label={label} 68 69 accessibilityLabel={label} 69 70 accessibilityHint={accessibilityHint} 71 + maximumDate={ 72 + maximumDate ? new Date(toSimpleDateString(maximumDate)) : undefined 73 + } 70 74 /> 71 75 )} 72 76 </>
+1 -1
src/components/forms/DateField/index.shared.tsx
··· 19 19 accessibilityHint, 20 20 }: { 21 21 label: string 22 - value: string 22 + value: string | Date 23 23 onPress: () => void 24 24 isInvalid?: boolean 25 25 accessibilityHint?: string
+17 -7
src/components/forms/DateField/index.tsx
··· 16 16 export const LabelText = TextField.LabelText 17 17 18 18 /** 19 - * Date-only input. Accepts a date in the format YYYY-MM-DD, and reports date 20 - * changes in the same format. 19 + * Date-only input. Accepts a string in the format YYYY-MM-DD, or a Date object. 20 + * Date objects are converted to strings in the format YYYY-MM-DD. 21 + * Returns a string in the format YYYY-MM-DD. 21 22 * 22 - * For dates of unknown format, convert with the 23 + * To generate a string in the format YYYY-MM-DD from a Date object, use the 23 24 * `utils.toSimpleDateString(Date)` export of this file. 24 25 */ 25 26 export function DateField({ ··· 29 30 label, 30 31 isInvalid, 31 32 accessibilityHint, 33 + maximumDate, 32 34 }: DateFieldProps) { 33 35 const {_} = useLingui() 34 36 const t = useTheme() ··· 56 58 isInvalid={isInvalid} 57 59 accessibilityHint={accessibilityHint} 58 60 /> 59 - <Dialog.Outer control={control} testID={testID}> 61 + <Dialog.Outer 62 + control={control} 63 + testID={testID} 64 + nativeOptions={{preventExpansion: true}}> 60 65 <Dialog.Handle /> 61 - <Dialog.Inner label={label}> 66 + <Dialog.ScrollableInner label={label}> 62 67 <View style={a.gap_lg}> 63 68 <View style={[a.relative, a.w_full, a.align_center]}> 64 69 <DatePicker 65 70 timeZoneOffsetInMinutes={0} 66 71 theme={t.name === 'light' ? 'light' : 'dark'} 67 - date={new Date(value)} 72 + date={new Date(toSimpleDateString(value))} 68 73 onDateChange={onChangeInternal} 69 74 mode="date" 70 75 testID={`${testID}-datepicker`} 71 76 aria-label={label} 72 77 accessibilityLabel={label} 73 78 accessibilityHint={accessibilityHint} 79 + maximumDate={ 80 + maximumDate 81 + ? new Date(toSimpleDateString(maximumDate)) 82 + : undefined 83 + } 74 84 /> 75 85 </View> 76 86 <Button ··· 84 94 </ButtonText> 85 95 </Button> 86 96 </View> 87 - </Dialog.Inner> 97 + </Dialog.ScrollableInner> 88 98 </Dialog.Outer> 89 99 </> 90 100 )
+5 -2
src/components/forms/DateField/index.web.tsx
··· 1 1 import React from 'react' 2 2 import {StyleSheet, TextInput, TextInputProps} from 'react-native' 3 - // @ts-ignore 3 + // @ts-expect-error untyped 4 4 import {unstable_createElement} from 'react-native-web' 5 5 6 6 import {DateFieldProps} from '#/components/forms/DateField/types' ··· 39 39 isInvalid, 40 40 testID, 41 41 accessibilityHint, 42 + maximumDate, 42 43 }: DateFieldProps) { 43 44 const handleOnChange = React.useCallback( 44 45 (e: any) => { ··· 56 57 <TextField.Root isInvalid={isInvalid}> 57 58 <TextField.Icon icon={CalendarDays} /> 58 59 <Input 59 - value={value} 60 + value={toSimpleDateString(value)} 60 61 label={label} 61 62 onChange={handleOnChange} 62 63 onChangeText={() => {}} 63 64 testID={testID} 64 65 accessibilityHint={accessibilityHint} 66 + // @ts-expect-error not typed as <input type="date"> even though it is one 67 + max={maximumDate ? toSimpleDateString(maximumDate) : undefined} 65 68 /> 66 69 </TextField.Root> 67 70 )
+2 -1
src/components/forms/DateField/types.ts
··· 1 1 export type DateFieldProps = { 2 - value: string 2 + value: string | Date 3 3 onChangeDate: (date: string) => void 4 4 label: string 5 5 isInvalid?: boolean 6 6 testID?: string 7 7 accessibilityHint?: string 8 + maximumDate?: string | Date 8 9 }
+2 -1
src/screens/Signup/StepInfo/index.tsx
··· 206 206 </DateField.LabelText> 207 207 <DateField.DateField 208 208 testID="date" 209 - value={DateField.utils.toSimpleDateString(state.dateOfBirth)} 209 + value={state.dateOfBirth} 210 210 onChangeDate={date => { 211 211 dispatch({ 212 212 type: 'setDateOfBirth', ··· 215 215 }} 216 216 label={_(msg`Date of birth`)} 217 217 accessibilityHint={_(msg`Select your date of birth`)} 218 + maximumDate={new Date()} 218 219 /> 219 220 </View> 220 221 <Policies
-105
src/view/com/util/forms/DateInput.tsx
··· 1 - import {useCallback, useState} from 'react' 2 - import {StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native' 3 - import DatePicker from 'react-native-date-picker' 4 - import { 5 - FontAwesomeIcon, 6 - FontAwesomeIconStyle, 7 - } from '@fortawesome/react-native-fontawesome' 8 - import {useLingui} from '@lingui/react' 9 - 10 - import {usePalette} from '#/lib/hooks/usePalette' 11 - import {TypographyVariant} from '#/lib/ThemeContext' 12 - import {useTheme} from '#/lib/ThemeContext' 13 - import {isAndroid, isIOS} from '#/platform/detection' 14 - import {Text} from '../text/Text' 15 - import {Button, ButtonType} from './Button' 16 - 17 - interface Props { 18 - testID?: string 19 - value: Date 20 - onChange: (date: Date) => void 21 - buttonType?: ButtonType 22 - buttonStyle?: StyleProp<ViewStyle> 23 - buttonLabelType?: TypographyVariant 24 - buttonLabelStyle?: StyleProp<TextStyle> 25 - accessibilityLabel: string 26 - accessibilityHint: string 27 - accessibilityLabelledBy?: string 28 - handleAsUTC?: boolean 29 - } 30 - 31 - export function DateInput(props: Props) { 32 - const {i18n} = useLingui() 33 - const [show, setShow] = useState(false) 34 - const theme = useTheme() 35 - const pal = usePalette('default') 36 - 37 - const onChangeInternal = useCallback( 38 - (date: Date) => { 39 - setShow(false) 40 - props.onChange(date) 41 - }, 42 - [setShow, props], 43 - ) 44 - 45 - const onPress = useCallback(() => { 46 - setShow(true) 47 - }, [setShow]) 48 - 49 - const onCancel = useCallback(() => { 50 - setShow(false) 51 - }, []) 52 - 53 - return ( 54 - <View> 55 - {isAndroid && ( 56 - <Button 57 - type={props.buttonType} 58 - style={props.buttonStyle} 59 - onPress={onPress} 60 - accessibilityLabel={props.accessibilityLabel} 61 - accessibilityHint={props.accessibilityHint} 62 - accessibilityLabelledBy={props.accessibilityLabelledBy}> 63 - <View style={styles.button}> 64 - <FontAwesomeIcon 65 - icon={['far', 'calendar']} 66 - style={pal.textLight as FontAwesomeIconStyle} 67 - /> 68 - <Text 69 - type={props.buttonLabelType} 70 - style={[pal.text, props.buttonLabelStyle]}> 71 - {i18n.date(props.value, { 72 - timeZone: props.handleAsUTC ? 'UTC' : undefined, 73 - })} 74 - </Text> 75 - </View> 76 - </Button> 77 - )} 78 - {(isIOS || show) && ( 79 - <DatePicker 80 - timeZoneOffsetInMinutes={0} 81 - modal={isAndroid} 82 - open={isAndroid} 83 - theme={theme.colorScheme} 84 - date={props.value} 85 - onDateChange={onChangeInternal} 86 - onConfirm={onChangeInternal} 87 - onCancel={onCancel} 88 - mode="date" 89 - testID={props.testID ? `${props.testID}-datepicker` : undefined} 90 - accessibilityLabel={props.accessibilityLabel} 91 - accessibilityHint={props.accessibilityHint} 92 - accessibilityLabelledBy={props.accessibilityLabelledBy} 93 - /> 94 - )} 95 - </View> 96 - ) 97 - } 98 - 99 - const styles = StyleSheet.create({ 100 - button: { 101 - flexDirection: 'row', 102 - alignItems: 'center', 103 - gap: 10, 104 - }, 105 - })
-76
src/view/com/util/forms/DateInput.web.tsx
··· 1 - import {useCallback, useState} from 'react' 2 - import {StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native' 3 - // @ts-ignore types not available -prf 4 - import {unstable_createElement} from 'react-native-web' 5 - 6 - import {usePalette} from '#/lib/hooks/usePalette' 7 - 8 - interface Props { 9 - testID?: string 10 - value: Date 11 - onChange: (date: Date) => void 12 - buttonType?: string 13 - buttonStyle?: StyleProp<ViewStyle> 14 - buttonLabelType?: string 15 - buttonLabelStyle?: StyleProp<TextStyle> 16 - accessibilityLabel: string 17 - accessibilityHint: string 18 - accessibilityLabelledBy?: string 19 - } 20 - 21 - export function DateInput(props: Props) { 22 - const pal = usePalette('default') 23 - const [value, setValue] = useState(toDateInputValue(props.value)) 24 - 25 - const onChangeInternal = useCallback( 26 - (v: Date) => { 27 - if (!v) { 28 - return 29 - } 30 - setValue(toDateInputValue(v)) 31 - props.onChange(v) 32 - }, 33 - [setValue, props], 34 - ) 35 - 36 - return ( 37 - <View style={[pal.borderDark, styles.container]}> 38 - {unstable_createElement('input', { 39 - type: 'date', 40 - testID: props.testID, 41 - value, 42 - onChange: (e: any) => onChangeInternal(e.currentTarget.valueAsDate), 43 - style: [pal.text, pal.view, pal.border, styles.textInput], 44 - placeholderTextColor: pal.colors.textLight, 45 - accessibilityLabel: props.accessibilityLabel, 46 - accessibilityHint: props.accessibilityHint, 47 - accessibilityLabelledBy: props.accessibilityLabelledBy, 48 - })} 49 - </View> 50 - ) 51 - } 52 - 53 - // we need the date in the form yyyy-MM-dd to pass to the input 54 - function toDateInputValue(d: Date): string { 55 - return d.toISOString().split('T')[0] 56 - } 57 - 58 - const styles = StyleSheet.create({ 59 - container: { 60 - flexDirection: 'row', 61 - paddingHorizontal: 4, 62 - borderWidth: 1, 63 - borderRadius: 10, 64 - }, 65 - textInput: { 66 - flex: 1, 67 - width: '100%', 68 - paddingVertical: 10, 69 - paddingHorizontal: 10, 70 - fontSize: 17, 71 - letterSpacing: 0.25, 72 - fontWeight: '400', 73 - borderRadius: 10, 74 - borderWidth: 0, 75 - }, 76 - })