-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #24528 from pasyukevich/feature/ios-date-picker-re…
…factor Refactor DatePicker component for ios
- Loading branch information
Showing
1 changed file
with
102 additions
and
118 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,163 +1,147 @@ | ||
import React from 'react'; | ||
// eslint-disable-next-line no-restricted-imports | ||
import React, {useState, useRef, useCallback, useEffect} from 'react'; | ||
import {Button, View, Keyboard} from 'react-native'; | ||
import RNDatePicker from '@react-native-community/datetimepicker'; | ||
import moment from 'moment'; | ||
import _ from 'underscore'; | ||
import compose from '../../libs/compose'; | ||
import isFunction from 'lodash/isFunction'; | ||
import TextInput from '../TextInput'; | ||
import withLocalize, {withLocalizePropTypes} from '../withLocalize'; | ||
import Popover from '../Popover'; | ||
import CONST from '../../CONST'; | ||
import styles from '../../styles/styles'; | ||
import themeColors from '../../styles/themes/default'; | ||
import {propTypes, defaultProps} from './datepickerPropTypes'; | ||
import withKeyboardState, {keyboardStatePropTypes} from '../withKeyboardState'; | ||
import useKeyboardState from '../../hooks/useKeyboardState'; | ||
import useLocalize from '../../hooks/useLocalize'; | ||
|
||
const datepickerPropTypes = { | ||
...propTypes, | ||
...withLocalizePropTypes, | ||
...keyboardStatePropTypes, | ||
}; | ||
function DatePicker({value, defaultValue, innerRef, onInputChange, preferredLocale, minDate, maxDate, label, disabled, onBlur, placeholder, containerStyles, errorText}) { | ||
const [isPickerVisible, setIsPickerVisible] = useState(false); | ||
const [selectedDate, setSelectedDate] = useState(moment(value || defaultValue).toDate()); | ||
const {isKeyboardShown} = useKeyboardState(); | ||
const {translate} = useLocalize(); | ||
const initialValue = useRef(null); | ||
const inputRef = useRef(null); | ||
|
||
class DatePicker extends React.Component { | ||
constructor(props) { | ||
super(props); | ||
|
||
this.state = { | ||
isPickerVisible: false, | ||
selectedDate: props.value || props.defaultValue ? moment(props.value || props.defaultValue).toDate() : new Date(), | ||
}; | ||
|
||
this.showPicker = this.showPicker.bind(this); | ||
this.reset = this.reset.bind(this); | ||
this.selectDate = this.selectDate.bind(this); | ||
this.updateLocalDate = this.updateLocalDate.bind(this); | ||
} | ||
|
||
showPicker() { | ||
this.initialValue = this.state.selectedDate; | ||
const showPicker = useCallback(() => { | ||
initialValue.current = selectedDate; | ||
|
||
// Opens the popover only after the keyboard is hidden to avoid a "blinking" effect where the keyboard was on iOS | ||
// See https://github.com/Expensify/App/issues/14084 for more context | ||
if (!this.props.isKeyboardShown) { | ||
this.setState({isPickerVisible: true}); | ||
if (!isKeyboardShown) { | ||
setIsPickerVisible(true); | ||
return; | ||
} | ||
|
||
const listener = Keyboard.addListener('keyboardDidHide', () => { | ||
this.setState({isPickerVisible: true}); | ||
setIsPickerVisible(true); | ||
listener.remove(); | ||
}); | ||
Keyboard.dismiss(); | ||
} | ||
}, [isKeyboardShown, selectedDate]); | ||
|
||
useEffect(() => { | ||
if (!isFunction(innerRef)) { | ||
return; | ||
} | ||
|
||
const input = inputRef.current; | ||
|
||
if (input && input.focus && isFunction(input.focus)) { | ||
innerRef({...input, focus: showPicker}); | ||
return; | ||
} | ||
|
||
innerRef(input); | ||
}, [innerRef, showPicker]); | ||
|
||
/** | ||
* Reset the date spinner to the initial value | ||
*/ | ||
reset() { | ||
this.setState({selectedDate: this.initialValue}); | ||
} | ||
const reset = () => { | ||
setSelectedDate(initialValue.current); | ||
}; | ||
|
||
/** | ||
* Accept the current spinner changes, close the spinner and propagate the change | ||
* to the parent component (props.onInputChange) | ||
* to the parent component (onInputChange) | ||
*/ | ||
selectDate() { | ||
this.setState({isPickerVisible: false}); | ||
const asMoment = moment(this.state.selectedDate, true); | ||
this.props.onInputChange(asMoment.format(CONST.DATE.MOMENT_FORMAT_STRING)); | ||
} | ||
const selectDate = () => { | ||
setIsPickerVisible(false); | ||
const asMoment = moment(selectedDate, true); | ||
onInputChange(asMoment.format(CONST.DATE.MOMENT_FORMAT_STRING)); | ||
}; | ||
|
||
/** | ||
* @param {Event} event | ||
* @param {Date} selectedDate | ||
* @param {Date} date | ||
*/ | ||
updateLocalDate(event, selectedDate) { | ||
this.setState({selectedDate}); | ||
} | ||
const updateLocalDate = (event, date) => { | ||
setSelectedDate(date); | ||
}; | ||
|
||
render() { | ||
const dateAsText = this.props.value || this.props.defaultValue ? moment(this.props.value || this.props.defaultValue).format(CONST.DATE.MOMENT_FORMAT_STRING) : ''; | ||
return ( | ||
<> | ||
<TextInput | ||
forceActiveLabel | ||
label={this.props.label} | ||
accessibilityLabel={this.props.label} | ||
accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT} | ||
value={dateAsText} | ||
placeholder={this.props.placeholder} | ||
errorText={this.props.errorText} | ||
containerStyles={this.props.containerStyles} | ||
textInputContainerStyles={this.state.isPickerVisible ? [styles.borderColorFocus] : []} | ||
onPress={this.showPicker} | ||
editable={false} | ||
disabled={this.props.disabled} | ||
onBlur={this.props.onBlur} | ||
ref={(el) => { | ||
if (!_.isFunction(this.props.innerRef)) { | ||
return; | ||
} | ||
if (el && el.focus && typeof el.focus === 'function') { | ||
let inputRef = {...el}; | ||
inputRef = {...inputRef, focus: this.showPicker}; | ||
this.props.innerRef(inputRef); | ||
return; | ||
} | ||
const dateAsText = value || defaultValue ? moment(value || defaultValue).format(CONST.DATE.MOMENT_FORMAT_STRING) : ''; | ||
|
||
this.props.innerRef(el); | ||
}} | ||
/> | ||
<Popover | ||
isVisible={this.state.isPickerVisible} | ||
onClose={this.selectDate} | ||
> | ||
<View style={[styles.flexRow, styles.justifyContentBetween, styles.borderBottom, styles.pb1, styles.ph4]}> | ||
<Button | ||
title={this.props.translate('common.reset')} | ||
color={themeColors.textError} | ||
onPress={this.reset} | ||
/> | ||
<Button | ||
title={this.props.translate('common.done')} | ||
color={themeColors.link} | ||
onPress={this.selectDate} | ||
/> | ||
</View> | ||
<RNDatePicker | ||
value={this.state.selectedDate} | ||
mode="date" | ||
display="spinner" | ||
themeVariant="dark" | ||
onChange={this.updateLocalDate} | ||
locale={this.props.preferredLocale} | ||
maximumDate={this.props.maxDate} | ||
minimumDate={this.props.minDate} | ||
return ( | ||
<> | ||
<TextInput | ||
forceActiveLabel | ||
label={label} | ||
accessibilityLabel={label} | ||
accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT} | ||
value={dateAsText} | ||
placeholder={placeholder} | ||
errorText={errorText} | ||
containerStyles={containerStyles} | ||
textInputContainerStyles={[isPickerVisible && styles.borderColorFocus]} | ||
onPress={showPicker} | ||
editable={false} | ||
disabled={disabled} | ||
onBlur={onBlur} | ||
ref={inputRef} | ||
/> | ||
<Popover | ||
isVisible={isPickerVisible} | ||
onClose={selectDate} | ||
> | ||
<View style={[styles.flexRow, styles.justifyContentBetween, styles.borderBottom, styles.pb1, styles.ph4]}> | ||
<Button | ||
title={translate('common.reset')} | ||
color={themeColors.textError} | ||
onPress={reset} | ||
/> | ||
<Button | ||
title={translate('common.done')} | ||
color={themeColors.link} | ||
onPress={selectDate} | ||
/> | ||
</Popover> | ||
</> | ||
); | ||
} | ||
</View> | ||
<RNDatePicker | ||
value={selectedDate} | ||
mode="date" | ||
display="spinner" | ||
themeVariant="dark" | ||
onChange={updateLocalDate} | ||
locale={preferredLocale} | ||
maximumDate={maxDate} | ||
minimumDate={minDate} | ||
/> | ||
</Popover> | ||
</> | ||
); | ||
} | ||
|
||
DatePicker.propTypes = datepickerPropTypes; | ||
DatePicker.propTypes = propTypes; | ||
DatePicker.defaultProps = defaultProps; | ||
DatePicker.displayName = 'DatePicker'; | ||
|
||
/** | ||
* We're applying localization here because we present a modal (with buttons) ourselves | ||
* Furthermore we're passing the locale down so that the modal and the date spinner are in the same | ||
* locale. Otherwise the spinner would be present in the system locale and it would be weird if it happens | ||
* that the modal buttons are in one locale (app) while the (spinner) month names are another (system) | ||
*/ | ||
export default compose( | ||
withLocalize, | ||
withKeyboardState, | ||
)( | ||
React.forwardRef((props, ref) => ( | ||
<DatePicker | ||
// eslint-disable-next-line react/jsx-props-no-spreading | ||
{...props} | ||
innerRef={ref} | ||
/> | ||
)), | ||
); | ||
export default React.forwardRef((props, ref) => ( | ||
<DatePicker | ||
// eslint-disable-next-line react/jsx-props-no-spreading | ||
{...props} | ||
innerRef={ref} | ||
/> | ||
)); |