react-cm-ui
Version:
React UI for Healthy Church
640 lines (543 loc) • 18.4 kB
JSX
import {
indexOf,
isEmpty,
isEqual,
isFunction,
isNil,
} from 'lodash';
import ClassNames from 'classnames';
import moment from 'moment-timezone';
import MomentPropTypes from 'react-moment-proptypes';
import PropTypes from 'prop-types';
import React from 'react';
import TetherComponent from 'react-tether';
import DatePickerUtils from '../../utils/datePickerUtils';
import DateUtils from '../../utils/dateUtils';
import Icon from '../../dataDisplay/icon';
import Input from '../input';
import DatePickerCalendarOnClickOutside from '../datePickerCalendar/datePickerCalendarOnClickOutside';
import withStyles from '../../styles/withStyles';
const propTypes = {
/**
* Override or extend the styles applied to DatePickerInput.
*/
classes: PropTypes.shape({
isFluid: PropTypes.string,
root: PropTypes.string,
}),
/**
* Assign additional class names to DatePickerInput.
*/
className: PropTypes.string,
/**
* Used for DOM testing. https://testing-library.com/docs/queries/bytestid/
*/
dataTestId: PropTypes.string,
/**
* Single date value. Moment object.
*/
date: MomentPropTypes.momentObj,
/**
* 'From' ('Start') Date for a Date Range. Moment object.
*/
dateFrom: MomentPropTypes.momentObj,
/**
* 'To' ('End') Date for a Date Range. Moment object.
*/
dateTo: MomentPropTypes.momentObj,
/**
* Deprecated prop. Please use `disable` instead.
*/
disable: PropTypes.bool,
/**
* A DatePickerInput can be disabled.
*/
disabled: PropTypes.bool,
/**
* String specifying the way the date value will be formatted.
*/
displayFormat: PropTypes.string,
/**
* Indicate that there is a validation error for this DatePickerInput control.
*/
errorMessage: PropTypes.string,
/**
* Indicates dates that should be highlighted on the DatePickerInput's calendar control. Array of Moment objects.
*/
events: PropTypes.arrayOf(MomentPropTypes.momentObj),
/**
* Specifies a range of dates that are not selectable. Array of Moment objects.
*/
excludeDates: PropTypes.arrayOf(MomentPropTypes.momentObj),
/**
* Function that is used to filter out dates that are to not be selectable.
*/
filterDates: PropTypes.func,
/**
* The DatePickerInput will be resized to its parent container's width.
*/
fluid: PropTypes.bool,
/**
* Forwarded Ref
*/
forwardedRef: PropTypes.oneOfType([
// Either a function
PropTypes.func,
// Or the instance of a DOM native element (see the note about SSR)
PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
]),
/**
* Specify an element ID this DatePickerInput control.
*/
id: PropTypes.oneOfType([
PropTypes.number,
PropTypes.string,
]),
/**
* Specifies a range of dates that are selectable. Array of Moment objects.
*/
includeDates: PropTypes.arrayOf(MomentPropTypes.momentObj),
/**
* Specifies a label for the DatePickerInput control.
*/
label: PropTypes.string,
/**
* Specifies which locale will be used when formatting date values.
*/
locale: PropTypes.string,
/**
* Specifies maximum selectable date. Moment object.
*/
maxDate: MomentPropTypes.momentObj,
/**
* Specifies minimum selectable date. Moment object.
*/
minDate: MomentPropTypes.momentObj,
/**
* DatePickerInput can handle an onBlur event from parent.
*/
onBlur: PropTypes.func,
/**
* Event handler for consumer to control state outside of the DatePickerInput.
*/
onChange: PropTypes.func,
/**
* Event handler called when the month in the DatePickerInput's calendar control changes.
*/
onMonthChange: PropTypes.func,
/**
* If true, specifies that the DatePickerInput represents the 'From' (or 'Start') date of a date range.
*/
rangeFrom: PropTypes.bool,
/**
* If true, specifies that the DatePickerInput represents the 'To' (or 'End') date of a date range.
*/
rangeTo: PropTypes.bool,
/**
* Indicates whether the DatePickerInput control represents a required field.
*/
required: PropTypes.bool,
/**
* Supply any inline styles to the DatePickerInput's container. Mainly used for padding and margins.
*/
style: PropTypes.shape({}),
/**
* Allows the DatePickerInput to be focused via the Tab key.
*/
tabIndex: PropTypes.number,
};
const defaultProps = {
classes: null,
className: null,
dataTestId: undefined,
date: null,
dateFrom: null,
dateTo: null,
disable: false,
disabled: false,
displayFormat: 'MM/DD/YYYY',
errorMessage: null,
events: null,
excludeDates: null,
filterDates: null,
fluid: false,
forwardedRef: undefined,
id: null,
includeDates: null,
label: null,
locale: 'en-US',
maxDate: null,
minDate: null,
onBlur: null,
onChange: null,
onMonthChange: null,
rangeFrom: false,
rangeTo: false,
required: false,
style: null,
tabIndex: -1,
};
const styles = (theme) => ({
root: {
display: 'inline-block',
'&$isFluid': {
width: '100%',
},
},
isFluid: {},
'@global': {
'.date-picker-tether-element': {
zIndex: theme.zIndex.datePickerInputCalendar,
},
},
});
/**
* The DatePickerInput represents an input for storing a date value.
*
* When clicked, it can display an interactive calendar control for selecting
* the date.
*
* A date value can also be typed into the input manually.
*/
class DatePickerInput extends React.PureComponent {
constructor(props) {
super(props);
const isDateRange = props.rangeFrom || props.rangeTo;
const newDate = isDateRange ? undefined : props.date;
const newDateFrom = !isDateRange ? undefined : props.dateFrom;
const newDateTo = !isDateRange ? undefined : props.dateTo;
let inputValue;
if (props.rangeFrom) {
inputValue = this.safeDateFormat(newDateFrom, props.locale);
} else if (props.rangeTo) {
inputValue = this.safeDateFormat(newDateTo, props.locale);
} else {
inputValue = this.safeDateFormat(newDate, props.locale);
}
this.state = {
date: newDate,
dateFrom: newDateFrom,
dateTo: newDateTo,
isCalendarOpen: false,
inputValue,
isFocused: false,
};
this.dateFormats = this.getAllowedDateFormats('MM/DD/YYYY');
this.getMaxDate = this.getMaxDate.bind(this);
this.getMinDate = this.getMinDate.bind(this);
this.onCalendarChange = this.onCalendarChange.bind(this);
this.onCalendarClickOutside = this.onCalendarClickOutside.bind(this);
this.onIconClick = this.onIconClick.bind(this);
this.onInputBlur = this.onInputBlur.bind(this);
this.onInputChange = this.onInputChange.bind(this);
this.onInputClick = this.onInputClick.bind(this);
this.onInputFocus = this.onInputFocus.bind(this);
this.onInputKeyDown = this.onInputKeyDown.bind(this);
this.onMonthChange = this.onMonthChange.bind(this);
this.datePickerInput = props.forwardedRef ?? React.createRef();
}
componentDidMount() {
const { onChange, rangeFrom, rangeTo } = this.props;
if ((rangeFrom || rangeTo) && !isFunction(onChange)) {
// eslint-disable-next-line no-console
console.error('The onChange prop is required when using the rangeFrom or rangeTo props');
}
}
componentDidUpdate(prevProps) {
const {
date,
dateFrom,
dateTo,
locale,
rangeFrom,
rangeTo,
} = this.props;
if (!DatePickerUtils.isSameDay(date, prevProps.date) ||
!DatePickerUtils.isSameDay(dateFrom, prevProps.dateFrom) ||
!DatePickerUtils.isSameDay(dateTo, prevProps.dateTo) ||
!isEqual(locale, prevProps.locale)
) {
const isDateRange = rangeFrom || rangeTo;
const newDate = isDateRange ? undefined : date;
const newDateFrom = !isDateRange ? undefined : dateFrom;
const newDateTo = !isDateRange ? undefined : dateTo;
let newInputValue;
if (rangeFrom) {
newInputValue = this.safeDateFormat(newDateFrom, locale);
} else if (rangeTo) {
newInputValue = this.safeDateFormat(newDateTo, locale);
} else {
newInputValue = this.safeDateFormat(newDate, locale);
}
this.setState({
date: newDate,
dateFrom: newDateFrom,
dateTo: newDateTo,
inputValue: newInputValue,
});
}
}
onCalendarChange({ date, dateFrom, dateTo }) {
const {
disable,
disabled,
onChange,
} = this.props;
const isNotDisabled = !disable && !disabled;
const isOnChangeFunc = isFunction(onChange);
if (isNotDisabled && isOnChangeFunc) {
onChange({ date, dateFrom, dateTo });
}
if (isNotDisabled && !isOnChangeFunc) {
const { locale } = this.props;
this.setState({
date,
inputValue: this.safeDateFormat(date, locale),
});
}
this.setOpen(false);
}
onCalendarClickOutside() {
this.setOpen(false);
}
onIconClick() {
const {
disable,
disabled,
} = this.props;
const isNotDisabled = !disable && !disabled;
if (isNotDisabled) {
this.datePickerInput.current.inputElement.focus();
}
}
onInputBlur(event) {
const { onBlur } = this.props;
if (isFunction(onBlur)) {
onBlur(event);
}
this.setState({ isFocused: false });
}
onInputChange(value) {
const {
disable,
disabled,
locale,
rangeFrom,
rangeTo,
} = this.props;
const { dateFrom, dateTo } = this.state;
const date = moment(value, this.dateFormats, locale || moment.locale(), true);
const isNotDisabled = !disable && !disabled;
const isValidValueChange = isNotDisabled &&
date.isValid() &&
!DatePickerUtils.isDayDisabled(date, this.props);
const isValidUpdateToNull = !isValidValueChange &&
isNotDisabled &&
(value === '' || value === '__/__/____');
const onChangeParam = {};
const updatedDateValue = isValidValueChange ? date : null;
if (rangeFrom) {
onChangeParam.dateFrom = updatedDateValue;
onChangeParam.dateTo = dateTo;
} else if (rangeTo) {
onChangeParam.dateFrom = dateFrom;
onChangeParam.dateTo = updatedDateValue;
} else {
onChangeParam.date = updatedDateValue;
}
if (isValidValueChange || isValidUpdateToNull) {
this.onCalendarChange(onChangeParam);
}
}
onInputClick() {
this.setState({ isFocused: true });
}
onInputFocus() {
this.setOpen(true);
this.setState({ isFocused: true });
}
onInputKeyDown(event) {
if (event.keyCode === 9 || event.keyCode === 13) {
this.setOpen(false);
}
}
onMonthChange(month, year) {
const { onMonthChange } = this.props;
if (isFunction(onMonthChange)) {
onMonthChange(month, year);
}
}
getAllowedDateFormats(specifiedFormat) {
let formats = DateUtils.getAllowedDateFormats();
if (indexOf(formats, specifiedFormat) < 0) {
formats = [specifiedFormat, ...formats];
}
return formats;
}
getMaxDate() {
const { maxDate } = this.props;
return maxDate;
}
getMinDate() {
const { minDate } = this.props;
return minDate;
}
setOpen(open) {
this.setState({ isCalendarOpen: open });
}
safeDateFormat(date, locale, format = 'MM/DD/YYYY') {
if (date && date.isValid()) {
return date.clone().locale(locale || moment.locale()).format(format);
}
if (isNil(date)) {
return '';
}
return null;
}
render() {
const {
classes,
className,
errorMessage,
dataTestId,
disable,
disabled,
displayFormat,
events,
excludeDates,
filterDates,
fluid,
id,
includeDates,
label,
locale,
rangeFrom,
rangeTo,
required,
style,
tabIndex,
} = this.props;
const {
date,
dateFrom,
dateTo,
isCalendarOpen,
inputValue,
isFocused,
} = this.state;
const rootClasses = ClassNames(
'ui',
'date-picker-input',
classes.root,
className,
{
[classes.isFluid]: fluid,
},
);
let iconColor;
if (isCalendarOpen) {
iconColor = 'highlight';
}
const isDisabled = disable || disabled;
if (isDisabled) {
iconColor = 'primary';
}
let inputString = inputValue;
if (!isFocused) {
const dateFormat = this.safeDateFormat(date, locale, displayFormat);
if (!isEmpty(dateFormat)) {
inputString = dateFormat;
}
}
return (
<div
className={rootClasses}
id={id}
style={style}
>
<TetherComponent
attachment="top left"
classPrefix="date-picker-tether"
constraints={[{
to: 'window',
attachment: 'together',
}]}
renderElement={(ref) => isCalendarOpen && (
<div ref={ref}>
<DatePickerCalendarOnClickOutside
controls="dropdowns"
date={date}
dateFrom={dateFrom}
dateTo={dateTo}
events={events}
excludeDates={excludeDates}
filterDates={filterDates}
includeDates={includeDates}
locale={locale}
maxDate={this.getMaxDate()}
minDate={this.getMinDate()}
mode="input"
onChange={this.onCalendarChange}
onClose={this.onCalendarClickOutside}
onMonthChange={this.onMonthChange}
rangeFrom={rangeFrom}
rangeTo={rangeTo}
/>
</div>
)}
renderTarget={(ref) => (
<div ref={ref}>
<Input
autoComplete="off"
data-parsley-error-message={errorMessage}
dataTestId={dataTestId}
disable={isDisabled}
fluid={fluid}
guide
icon={(
<Icon
color={iconColor}
compact
disable={isDisabled}
onClick={this.onIconClick}
type="calendar"
/>
)}
keepCharPositions
label={label}
mask={[/\d/, /\d/, '/', /\d/, /\d/, '/', /\d/, /\d/, /\d/, /\d/]}
onBlur={this.onInputBlur}
onChange={this.onInputChange}
onClick={this.onInputClick}
onFocus={this.onInputFocus}
onKeyDown={this.onInputKeyDown}
placeholder="mm/dd/yyyy"
required={required}
ref={this.datePickerInput}
tabIndex={tabIndex}
type="text"
value={inputString}
/>
</div>
)}
targetAttachment="bottom left"
targetOffset="11px 0"
/>
</div>
);
}
}
DatePickerInput.propTypes = propTypes;
DatePickerInput.defaultProps = defaultProps;
const DatePickerInputWrapper = React.forwardRef((props, ref) => ((
// eslint-disable-next-line react/jsx-props-no-spreading
<DatePickerInput {...props} forwardedRef={ref} />
)));
const wrapperPropTypes = { ...propTypes };
delete wrapperPropTypes.forwardedRef;
const wrapperDefaultProps = { ...defaultProps };
delete wrapperDefaultProps.forwardedRef;
DatePickerInputWrapper.propTypes = wrapperPropTypes;
DatePickerInputWrapper.defaultProps = wrapperDefaultProps;
DatePickerInputWrapper.displayName = 'DatePickerInput';
export default withStyles(styles)(DatePickerInputWrapper);