react-dates
Version:
A responsive and accessible date range picker component built with React
713 lines (606 loc) • 23.5 kB
JavaScript
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
import _objectAssign from 'object.assign';
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
import React from 'react';
import moment from 'moment';
import { css, withStyles, withStylesPropTypes } from 'react-with-styles';
import Portal from 'react-portal';
import { forbidExtraProps } from 'airbnb-prop-types';
import { addEventListener } from 'consolidated-events';
import isTouchDevice from 'is-touch-device';
import SingleDatePickerShape from '../shapes/SingleDatePickerShape';
import { SingleDatePickerPhrases } from '../defaultPhrases';
import OutsideClickHandler from './OutsideClickHandler';
import toMomentObject from '../utils/toMomentObject';
import toLocalizedDateString from '../utils/toLocalizedDateString';
import getResponsiveContainerStyles from '../utils/getResponsiveContainerStyles';
import SingleDatePickerInput from './SingleDatePickerInput';
import DayPickerSingleDateController from './DayPickerSingleDateController';
import CloseButton from './CloseButton';
import isInclusivelyAfterDay from '../utils/isInclusivelyAfterDay';
import { HORIZONTAL_ORIENTATION, VERTICAL_ORIENTATION, ANCHOR_LEFT, ANCHOR_RIGHT, OPEN_DOWN, OPEN_UP, DAY_SIZE, ICON_BEFORE_POSITION } from '../constants';
var propTypes = forbidExtraProps(_objectAssign({}, withStylesPropTypes, SingleDatePickerShape));
var defaultProps = {
// required props for a functional interactive SingleDatePicker
date: null,
focused: false,
// input related props
id: 'date',
placeholder: 'Date',
disabled: false,
required: false,
readOnly: false,
screenReaderInputMessage: '',
showClearDate: false,
showDefaultInputIcon: false,
inputIconPosition: ICON_BEFORE_POSITION,
customInputIcon: null,
customCloseIcon: null,
// calendar presentation and interaction related props
orientation: HORIZONTAL_ORIENTATION,
anchorDirection: ANCHOR_LEFT,
openDirection: OPEN_DOWN,
horizontalMargin: 0,
withPortal: false,
withFullScreenPortal: false,
initialVisibleMonth: null,
firstDayOfWeek: null,
numberOfMonths: 2,
keepOpenOnDateSelect: false,
reopenPickerOnClearDate: false,
renderCalendarInfo: null,
hideKeyboardShortcutsPanel: false,
daySize: DAY_SIZE,
isRTL: false,
verticalHeight: null,
// navigation related props
navPrev: null,
navNext: null,
onPrevMonthClick: function () {
function onPrevMonthClick() {}
return onPrevMonthClick;
}(),
onNextMonthClick: function () {
function onNextMonthClick() {}
return onNextMonthClick;
}(),
onClose: function () {
function onClose() {}
return onClose;
}(),
// month presentation and interaction related props
renderMonth: null,
// day presentation and interaction related props
renderDay: null,
enableOutsideDays: false,
isDayBlocked: function () {
function isDayBlocked() {
return false;
}
return isDayBlocked;
}(),
isOutsideRange: function () {
function isOutsideRange(day) {
return !isInclusivelyAfterDay(day, moment());
}
return isOutsideRange;
}(),
isDayHighlighted: function () {
function isDayHighlighted() {}
return isDayHighlighted;
}(),
// internationalization props
displayFormat: function () {
function displayFormat() {
return moment.localeData().longDateFormat('L');
}
return displayFormat;
}(),
monthFormat: 'MMMM YYYY',
weekDayFormat: 'dd',
phrases: SingleDatePickerPhrases
};
var SingleDatePicker = function (_React$Component) {
_inherits(SingleDatePicker, _React$Component);
function SingleDatePicker(props) {
_classCallCheck(this, SingleDatePicker);
var _this = _possibleConstructorReturn(this, (SingleDatePicker.__proto__ || Object.getPrototypeOf(SingleDatePicker)).call(this, props));
_this.isTouchDevice = false;
_this.state = {
dayPickerContainerStyles: {},
isDayPickerFocused: false,
isInputFocused: false,
showKeyboardShortcuts: false
};
_this.onDayPickerFocus = _this.onDayPickerFocus.bind(_this);
_this.onDayPickerBlur = _this.onDayPickerBlur.bind(_this);
_this.showKeyboardShortcutsPanel = _this.showKeyboardShortcutsPanel.bind(_this);
_this.onChange = _this.onChange.bind(_this);
_this.onFocus = _this.onFocus.bind(_this);
_this.onClearFocus = _this.onClearFocus.bind(_this);
_this.clearDate = _this.clearDate.bind(_this);
_this.responsivizePickerPosition = _this.responsivizePickerPosition.bind(_this);
_this.setDayPickerContainerRef = _this.setDayPickerContainerRef.bind(_this);
return _this;
}
/* istanbul ignore next */
_createClass(SingleDatePicker, [{
key: 'componentDidMount',
value: function () {
function componentDidMount() {
this.removeEventListener = addEventListener(window, 'resize', this.responsivizePickerPosition, { passive: true });
this.responsivizePickerPosition();
if (this.props.focused) {
this.setState({
isInputFocused: true
});
}
this.isTouchDevice = isTouchDevice();
}
return componentDidMount;
}()
}, {
key: 'componentDidUpdate',
value: function () {
function componentDidUpdate(prevProps) {
if (!prevProps.focused && this.props.focused) {
this.responsivizePickerPosition();
}
}
return componentDidUpdate;
}()
/* istanbul ignore next */
}, {
key: 'componentWillUnmount',
value: function () {
function componentWillUnmount() {
if (this.removeEventListener) this.removeEventListener();
}
return componentWillUnmount;
}()
}, {
key: 'onChange',
value: function () {
function onChange(dateString) {
var _props = this.props,
isOutsideRange = _props.isOutsideRange,
keepOpenOnDateSelect = _props.keepOpenOnDateSelect,
onDateChange = _props.onDateChange,
onFocusChange = _props.onFocusChange,
onClose = _props.onClose;
var newDate = toMomentObject(dateString, this.getDisplayFormat());
var isValid = newDate && !isOutsideRange(newDate);
if (isValid) {
onDateChange(newDate);
if (!keepOpenOnDateSelect) {
onFocusChange({ focused: false });
onClose({ date: newDate });
}
} else {
onDateChange(null);
}
}
return onChange;
}()
}, {
key: 'onFocus',
value: function () {
function onFocus() {
var _props2 = this.props,
disabled = _props2.disabled,
onFocusChange = _props2.onFocusChange,
withPortal = _props2.withPortal,
withFullScreenPortal = _props2.withFullScreenPortal;
var moveFocusToDayPicker = withPortal || withFullScreenPortal || this.isTouchDevice;
if (moveFocusToDayPicker) {
this.onDayPickerFocus();
} else {
this.onDayPickerBlur();
}
if (!disabled) {
onFocusChange({ focused: true });
}
}
return onFocus;
}()
}, {
key: 'onClearFocus',
value: function () {
function onClearFocus() {
var _props3 = this.props,
date = _props3.date,
focused = _props3.focused,
onFocusChange = _props3.onFocusChange,
onClose = _props3.onClose;
if (!focused) return;
this.setState({
isInputFocused: false,
isDayPickerFocused: false
});
onFocusChange({ focused: false });
onClose({ date: date });
}
return onClearFocus;
}()
}, {
key: 'onDayPickerFocus',
value: function () {
function onDayPickerFocus() {
this.setState({
isInputFocused: false,
isDayPickerFocused: true,
showKeyboardShortcuts: false
});
}
return onDayPickerFocus;
}()
}, {
key: 'onDayPickerBlur',
value: function () {
function onDayPickerBlur() {
this.setState({
isInputFocused: true,
isDayPickerFocused: false,
showKeyboardShortcuts: false
});
}
return onDayPickerBlur;
}()
}, {
key: 'getDateString',
value: function () {
function getDateString(date) {
var displayFormat = this.getDisplayFormat();
if (date && displayFormat) {
return date && date.format(displayFormat);
}
return toLocalizedDateString(date);
}
return getDateString;
}()
}, {
key: 'getDisplayFormat',
value: function () {
function getDisplayFormat() {
var displayFormat = this.props.displayFormat;
return typeof displayFormat === 'string' ? displayFormat : displayFormat();
}
return getDisplayFormat;
}()
}, {
key: 'setDayPickerContainerRef',
value: function () {
function setDayPickerContainerRef(ref) {
this.dayPickerContainer = ref;
}
return setDayPickerContainerRef;
}()
}, {
key: 'clearDate',
value: function () {
function clearDate() {
var _props4 = this.props,
onDateChange = _props4.onDateChange,
reopenPickerOnClearDate = _props4.reopenPickerOnClearDate,
onFocusChange = _props4.onFocusChange;
onDateChange(null);
if (reopenPickerOnClearDate) {
onFocusChange({ focused: true });
}
}
return clearDate;
}()
/* istanbul ignore next */
}, {
key: 'responsivizePickerPosition',
value: function () {
function responsivizePickerPosition() {
// It's possible the portal props have been changed in response to window resizes
// So let's ensure we reset this back to the base state each time
this.setState({ dayPickerContainerStyles: {} });
var _props5 = this.props,
anchorDirection = _props5.anchorDirection,
horizontalMargin = _props5.horizontalMargin,
withPortal = _props5.withPortal,
withFullScreenPortal = _props5.withFullScreenPortal,
focused = _props5.focused;
var dayPickerContainerStyles = this.state.dayPickerContainerStyles;
if (!focused) {
return;
}
var isAnchoredLeft = anchorDirection === ANCHOR_LEFT;
if (!withPortal && !withFullScreenPortal) {
var containerRect = this.dayPickerContainer.getBoundingClientRect();
var currentOffset = dayPickerContainerStyles[anchorDirection] || 0;
var containerEdge = isAnchoredLeft ? containerRect[ANCHOR_RIGHT] : containerRect[ANCHOR_LEFT];
this.setState({
dayPickerContainerStyles: getResponsiveContainerStyles(anchorDirection, currentOffset, containerEdge, horizontalMargin)
});
}
}
return responsivizePickerPosition;
}()
}, {
key: 'showKeyboardShortcutsPanel',
value: function () {
function showKeyboardShortcutsPanel() {
this.setState({
isInputFocused: false,
isDayPickerFocused: true,
showKeyboardShortcuts: true
});
}
return showKeyboardShortcutsPanel;
}()
}, {
key: 'maybeRenderDayPickerWithPortal',
value: function () {
function maybeRenderDayPickerWithPortal() {
var _props6 = this.props,
focused = _props6.focused,
withPortal = _props6.withPortal,
withFullScreenPortal = _props6.withFullScreenPortal;
if (!focused) {
return null;
}
if (withPortal || withFullScreenPortal) {
return React.createElement(
Portal,
{ isOpened: true },
this.renderDayPicker()
);
}
return this.renderDayPicker();
}
return maybeRenderDayPickerWithPortal;
}()
}, {
key: 'renderDayPicker',
value: function () {
function renderDayPicker() {
var _props7 = this.props,
anchorDirection = _props7.anchorDirection,
openDirection = _props7.openDirection,
onDateChange = _props7.onDateChange,
date = _props7.date,
onFocusChange = _props7.onFocusChange,
focused = _props7.focused,
enableOutsideDays = _props7.enableOutsideDays,
numberOfMonths = _props7.numberOfMonths,
orientation = _props7.orientation,
monthFormat = _props7.monthFormat,
navPrev = _props7.navPrev,
navNext = _props7.navNext,
onPrevMonthClick = _props7.onPrevMonthClick,
onNextMonthClick = _props7.onNextMonthClick,
onClose = _props7.onClose,
withPortal = _props7.withPortal,
withFullScreenPortal = _props7.withFullScreenPortal,
keepOpenOnDateSelect = _props7.keepOpenOnDateSelect,
initialVisibleMonth = _props7.initialVisibleMonth,
renderMonth = _props7.renderMonth,
renderDay = _props7.renderDay,
renderCalendarInfo = _props7.renderCalendarInfo,
hideKeyboardShortcutsPanel = _props7.hideKeyboardShortcutsPanel,
firstDayOfWeek = _props7.firstDayOfWeek,
customCloseIcon = _props7.customCloseIcon,
phrases = _props7.phrases,
daySize = _props7.daySize,
isRTL = _props7.isRTL,
isOutsideRange = _props7.isOutsideRange,
isDayBlocked = _props7.isDayBlocked,
isDayHighlighted = _props7.isDayHighlighted,
weekDayFormat = _props7.weekDayFormat,
styles = _props7.styles,
verticalHeight = _props7.verticalHeight;
var _state = this.state,
dayPickerContainerStyles = _state.dayPickerContainerStyles,
isDayPickerFocused = _state.isDayPickerFocused,
showKeyboardShortcuts = _state.showKeyboardShortcuts;
var onOutsideClick = !withFullScreenPortal && withPortal ? this.onClearFocus : undefined;
var closeIcon = customCloseIcon || React.createElement(CloseButton, null);
return React.createElement(
'div',
_extends({ // eslint-disable-line jsx-a11y/no-static-element-interactions
ref: this.setDayPickerContainerRef
}, css(styles.SingleDatePicker_picker, anchorDirection === ANCHOR_LEFT && styles.SingleDatePicker_picker__directionLeft, anchorDirection === ANCHOR_RIGHT && styles.SingleDatePicker_picker__directionRight, openDirection === OPEN_DOWN && styles.SingleDatePicker_picker__openDown, openDirection === OPEN_UP && styles.SingleDatePicker_picker__openUp, orientation === HORIZONTAL_ORIENTATION && styles.SingleDatePicker_picker__horizontal, orientation === VERTICAL_ORIENTATION && styles.SingleDatePicker_picker__vertical, (withPortal || withFullScreenPortal) && styles.SingleDatePicker_picker__portal, withFullScreenPortal && styles.SingleDatePicker_picker__fullScreenPortal, isRTL && styles.SingleDatePicker_picker__rtl), {
style: dayPickerContainerStyles,
onClick: onOutsideClick
}),
React.createElement(DayPickerSingleDateController, {
date: date,
onDateChange: onDateChange,
onFocusChange: onFocusChange,
orientation: orientation,
enableOutsideDays: enableOutsideDays,
numberOfMonths: numberOfMonths,
monthFormat: monthFormat,
withPortal: withPortal || withFullScreenPortal,
focused: focused,
keepOpenOnDateSelect: keepOpenOnDateSelect,
hideKeyboardShortcutsPanel: hideKeyboardShortcutsPanel,
initialVisibleMonth: initialVisibleMonth,
navPrev: navPrev,
navNext: navNext,
onPrevMonthClick: onPrevMonthClick,
onNextMonthClick: onNextMonthClick,
onClose: onClose,
renderMonth: renderMonth,
renderDay: renderDay,
renderCalendarInfo: renderCalendarInfo,
isFocused: isDayPickerFocused,
showKeyboardShortcuts: showKeyboardShortcuts,
onBlur: this.onDayPickerBlur,
phrases: phrases,
daySize: daySize,
isRTL: isRTL,
isOutsideRange: isOutsideRange,
isDayBlocked: isDayBlocked,
isDayHighlighted: isDayHighlighted,
firstDayOfWeek: firstDayOfWeek,
weekDayFormat: weekDayFormat,
verticalHeight: verticalHeight
}),
withFullScreenPortal && React.createElement(
'button',
{
'aria-label': phrases.closeDatePicker,
className: 'SingleDatePicker__close',
type: 'button',
onClick: this.onClearFocus
},
React.createElement(
'div',
{ className: 'SingleDatePicker__close-icon' },
closeIcon
)
)
);
}
return renderDayPicker;
}()
}, {
key: 'render',
value: function () {
function render() {
var _props8 = this.props,
id = _props8.id,
placeholder = _props8.placeholder,
disabled = _props8.disabled,
focused = _props8.focused,
required = _props8.required,
readOnly = _props8.readOnly,
openDirection = _props8.openDirection,
showClearDate = _props8.showClearDate,
showDefaultInputIcon = _props8.showDefaultInputIcon,
inputIconPosition = _props8.inputIconPosition,
customCloseIcon = _props8.customCloseIcon,
customInputIcon = _props8.customInputIcon,
date = _props8.date,
phrases = _props8.phrases,
withPortal = _props8.withPortal,
withFullScreenPortal = _props8.withFullScreenPortal,
screenReaderInputMessage = _props8.screenReaderInputMessage,
isRTL = _props8.isRTL,
styles = _props8.styles;
var isInputFocused = this.state.isInputFocused;
var displayValue = this.getDateString(date);
var onOutsideClick = !withPortal && !withFullScreenPortal ? this.onClearFocus : undefined;
return React.createElement(
'div',
css(styles.SingleDatePicker),
React.createElement(
OutsideClickHandler,
{ onOutsideClick: onOutsideClick },
React.createElement(SingleDatePickerInput, {
id: id,
placeholder: placeholder,
focused: focused,
isFocused: isInputFocused,
disabled: disabled,
required: required,
readOnly: readOnly,
openDirection: openDirection,
showCaret: !withPortal && !withFullScreenPortal,
onClearDate: this.clearDate,
showClearDate: showClearDate,
showDefaultInputIcon: showDefaultInputIcon,
inputIconPosition: inputIconPosition,
customCloseIcon: customCloseIcon,
customInputIcon: customInputIcon,
displayValue: displayValue,
onChange: this.onChange,
onFocus: this.onFocus,
onKeyDownShiftTab: this.onClearFocus,
onKeyDownTab: this.onClearFocus,
onKeyDownArrowDown: this.onDayPickerFocus,
onKeyDownQuestionMark: this.showKeyboardShortcutsPanel,
screenReaderMessage: screenReaderInputMessage,
phrases: phrases,
isRTL: isRTL
}),
this.maybeRenderDayPickerWithPortal()
)
);
}
return render;
}()
}]);
return SingleDatePicker;
}(React.Component);
SingleDatePicker.propTypes = propTypes;
SingleDatePicker.defaultProps = defaultProps;
export { SingleDatePicker as PureSingleDatePicker };
export default withStyles(function (_ref) {
var _ref$reactDates = _ref.reactDates,
color = _ref$reactDates.color,
spacing = _ref$reactDates.spacing,
zIndex = _ref$reactDates.zIndex;
return {
SingleDatePicker: {
position: 'relative',
display: 'inline-block'
},
SingleDatePicker_picker: {
zIndex: zIndex + 1,
backgroundColor: color.background,
position: 'absolute'
},
SingleDatePicker_picker__rtl: {
direction: 'rtl'
},
SingleDatePicker_picker__directionLeft: {
left: 0
},
SingleDatePicker_picker__directionRight: {
right: 0
},
SingleDatePicker_picker__openDown: {
top: spacing.inputMarginBottom
},
SingleDatePicker_picker__openUp: {
bottom: spacing.inputMarginBottom
},
SingleDatePicker_picker__portal: {
backgroundColor: 'rgba(0, 0, 0, 0.3)',
position: 'fixed',
top: 0,
left: 0,
height: '100%',
width: '100%'
},
SingleDatePicker_picker__fullScreenPortal: {
backgroundColor: color.background
},
SingleDatePicker_closeButton: {
background: 'none',
border: 0,
color: 'inherit',
font: 'inherit',
lineHeight: 'normal',
overflow: 'visible',
cursor: 'pointer',
position: 'absolute',
top: 0,
right: 0,
padding: 15,
zIndex: zIndex + 2,
':hover': {
color: 'darken(' + String(color.core.grayLighter) + ', 10%)',
textDecoration: 'none'
},
':focus': {
color: 'darken(' + String(color.core.grayLighter) + ', 10%)',
textDecoration: 'none'
}
},
SingleDatePicker_closeButton_svg: {
height: 15,
width: 15,
fill: color.core.grayLighter
}
};
})(SingleDatePicker);