UNPKG

9.77 kBJSXView Raw
1import React from 'react';
2import PropTypes from 'prop-types';
3import moment from 'moment';
4
5import momentPropTypes from 'react-moment-proptypes';
6import { forbidExtraProps, nonNegativeInteger } from 'airbnb-prop-types';
7import openDirectionShape from '../shapes/OpenDirectionShape';
8
9import { DateRangePickerInputPhrases } from '../defaultPhrases';
10import getPhrasePropTypes from '../utils/getPhrasePropTypes';
11
12import DateRangePickerInput from './DateRangePickerInput';
13
14import IconPositionShape from '../shapes/IconPositionShape';
15import DisabledShape from '../shapes/DisabledShape';
16
17import toMomentObject from '../utils/toMomentObject';
18import toLocalizedDateString from '../utils/toLocalizedDateString';
19
20import isInclusivelyAfterDay from '../utils/isInclusivelyAfterDay';
21import isBeforeDay from '../utils/isBeforeDay';
22
23import {
24 START_DATE,
25 END_DATE,
26 ICON_BEFORE_POSITION,
27 OPEN_DOWN,
28} from '../constants';
29
30const propTypes = forbidExtraProps({
31 children: PropTypes.node,
32
33 startDate: momentPropTypes.momentObj,
34 startDateId: PropTypes.string,
35 startDatePlaceholderText: PropTypes.string,
36 isStartDateFocused: PropTypes.bool,
37 startDateAriaLabel: PropTypes.string,
38
39 endDate: momentPropTypes.momentObj,
40 endDateId: PropTypes.string,
41 endDatePlaceholderText: PropTypes.string,
42 isEndDateFocused: PropTypes.bool,
43 endDateAriaLabel: PropTypes.string,
44
45 screenReaderMessage: PropTypes.string,
46 showClearDates: PropTypes.bool,
47 showCaret: PropTypes.bool,
48 showDefaultInputIcon: PropTypes.bool,
49 inputIconPosition: IconPositionShape,
50 disabled: DisabledShape,
51 required: PropTypes.bool,
52 readOnly: PropTypes.bool,
53 openDirection: openDirectionShape,
54 noBorder: PropTypes.bool,
55 block: PropTypes.bool,
56 small: PropTypes.bool,
57 regular: PropTypes.bool,
58 verticalSpacing: nonNegativeInteger,
59
60 keepOpenOnDateSelect: PropTypes.bool,
61 reopenPickerOnClearDates: PropTypes.bool,
62 withFullScreenPortal: PropTypes.bool,
63 minimumNights: nonNegativeInteger,
64 isOutsideRange: PropTypes.func,
65 displayFormat: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
66
67 onFocusChange: PropTypes.func,
68 onClose: PropTypes.func,
69 onDatesChange: PropTypes.func,
70 onKeyDownArrowDown: PropTypes.func,
71 onKeyDownQuestionMark: PropTypes.func,
72
73 customInputIcon: PropTypes.node,
74 customArrowIcon: PropTypes.node,
75 customCloseIcon: PropTypes.node,
76
77 // accessibility
78 isFocused: PropTypes.bool,
79
80 // i18n
81 phrases: PropTypes.shape(getPhrasePropTypes(DateRangePickerInputPhrases)),
82
83 isRTL: PropTypes.bool,
84});
85
86const defaultProps = {
87 children: null,
88
89 startDate: null,
90 startDateId: START_DATE,
91 startDatePlaceholderText: 'Start Date',
92 isStartDateFocused: false,
93 startDateAriaLabel: undefined,
94
95 endDate: null,
96 endDateId: END_DATE,
97 endDatePlaceholderText: 'End Date',
98 isEndDateFocused: false,
99 endDateAriaLabel: undefined,
100
101 screenReaderMessage: '',
102 showClearDates: false,
103 showCaret: false,
104 showDefaultInputIcon: false,
105 inputIconPosition: ICON_BEFORE_POSITION,
106 disabled: false,
107 required: false,
108 readOnly: false,
109 openDirection: OPEN_DOWN,
110 noBorder: false,
111 block: false,
112 small: false,
113 regular: false,
114 verticalSpacing: undefined,
115
116 keepOpenOnDateSelect: false,
117 reopenPickerOnClearDates: false,
118 withFullScreenPortal: false,
119 minimumNights: 1,
120 isOutsideRange: (day) => !isInclusivelyAfterDay(day, moment()),
121 displayFormat: () => moment.localeData().longDateFormat('L'),
122
123 onFocusChange() {},
124 onClose() {},
125 onDatesChange() {},
126 onKeyDownArrowDown() {},
127 onKeyDownQuestionMark() {},
128
129 customInputIcon: null,
130 customArrowIcon: null,
131 customCloseIcon: null,
132
133 // accessibility
134 isFocused: false,
135
136 // i18n
137 phrases: DateRangePickerInputPhrases,
138
139 isRTL: false,
140};
141
142export default class DateRangePickerInputController extends React.PureComponent {
143 constructor(props) {
144 super(props);
145
146 this.onClearFocus = this.onClearFocus.bind(this);
147 this.onStartDateChange = this.onStartDateChange.bind(this);
148 this.onStartDateFocus = this.onStartDateFocus.bind(this);
149 this.onEndDateChange = this.onEndDateChange.bind(this);
150 this.onEndDateFocus = this.onEndDateFocus.bind(this);
151 this.clearDates = this.clearDates.bind(this);
152 }
153
154 onClearFocus() {
155 const {
156 onFocusChange,
157 onClose,
158 startDate,
159 endDate,
160 } = this.props;
161
162 onFocusChange(null);
163 onClose({ startDate, endDate });
164 }
165
166 onEndDateChange(endDateString) {
167 const {
168 startDate,
169 isOutsideRange,
170 minimumNights,
171 keepOpenOnDateSelect,
172 onDatesChange,
173 } = this.props;
174
175 const endDate = toMomentObject(endDateString, this.getDisplayFormat());
176
177 const isEndDateValid = endDate
178 && !isOutsideRange(endDate)
179 && !(startDate && isBeforeDay(endDate, startDate.clone().add(minimumNights, 'days')));
180 if (isEndDateValid) {
181 onDatesChange({ startDate, endDate });
182 if (!keepOpenOnDateSelect) this.onClearFocus();
183 } else {
184 onDatesChange({
185 startDate,
186 endDate: null,
187 });
188 }
189 }
190
191 onEndDateFocus() {
192 const {
193 startDate,
194 onFocusChange,
195 withFullScreenPortal,
196 disabled,
197 } = this.props;
198
199 if (!startDate && withFullScreenPortal && (!disabled || disabled === END_DATE)) {
200 // When the datepicker is full screen, we never want to focus the end date first
201 // because there's no indication that that is the case once the datepicker is open and it
202 // might confuse the user
203 onFocusChange(START_DATE);
204 } else if (!disabled || disabled === START_DATE) {
205 onFocusChange(END_DATE);
206 }
207 }
208
209 onStartDateChange(startDateString) {
210 let { endDate } = this.props;
211 const {
212 isOutsideRange,
213 minimumNights,
214 onDatesChange,
215 onFocusChange,
216 disabled,
217 } = this.props;
218
219 const startDate = toMomentObject(startDateString, this.getDisplayFormat());
220 const isEndDateBeforeStartDate = startDate
221 && isBeforeDay(endDate, startDate.clone().add(minimumNights, 'days'));
222 const isStartDateValid = startDate
223 && !isOutsideRange(startDate)
224 && !(disabled === END_DATE && isEndDateBeforeStartDate);
225
226 if (isStartDateValid) {
227 if (isEndDateBeforeStartDate) {
228 endDate = null;
229 }
230
231 onDatesChange({ startDate, endDate });
232 onFocusChange(END_DATE);
233 } else {
234 onDatesChange({
235 startDate: null,
236 endDate,
237 });
238 }
239 }
240
241 onStartDateFocus() {
242 const { disabled, onFocusChange } = this.props;
243
244 if (!disabled || disabled === END_DATE) {
245 onFocusChange(START_DATE);
246 }
247 }
248
249 getDisplayFormat() {
250 const { displayFormat } = this.props;
251 return typeof displayFormat === 'string' ? displayFormat : displayFormat();
252 }
253
254 getDateString(date) {
255 const displayFormat = this.getDisplayFormat();
256 if (date && displayFormat) {
257 return date && date.format(displayFormat);
258 }
259 return toLocalizedDateString(date);
260 }
261
262 clearDates() {
263 const { onDatesChange, reopenPickerOnClearDates, onFocusChange } = this.props;
264 onDatesChange({ startDate: null, endDate: null });
265 if (reopenPickerOnClearDates) {
266 onFocusChange(START_DATE);
267 }
268 }
269
270 render() {
271 const {
272 children,
273 startDate,
274 startDateId,
275 startDatePlaceholderText,
276 isStartDateFocused,
277 startDateAriaLabel,
278 endDate,
279 endDateId,
280 endDatePlaceholderText,
281 endDateAriaLabel,
282 isEndDateFocused,
283 screenReaderMessage,
284 showClearDates,
285 showCaret,
286 showDefaultInputIcon,
287 inputIconPosition,
288 customInputIcon,
289 customArrowIcon,
290 customCloseIcon,
291 disabled,
292 required,
293 readOnly,
294 openDirection,
295 isFocused,
296 phrases,
297 onKeyDownArrowDown,
298 onKeyDownQuestionMark,
299 isRTL,
300 noBorder,
301 block,
302 small,
303 regular,
304 verticalSpacing,
305 } = this.props;
306
307 const startDateString = this.getDateString(startDate);
308 const endDateString = this.getDateString(endDate);
309
310 return (
311 <DateRangePickerInput
312 startDate={startDateString}
313 startDateId={startDateId}
314 startDatePlaceholderText={startDatePlaceholderText}
315 isStartDateFocused={isStartDateFocused}
316 startDateAriaLabel={startDateAriaLabel}
317 endDate={endDateString}
318 endDateId={endDateId}
319 endDatePlaceholderText={endDatePlaceholderText}
320 isEndDateFocused={isEndDateFocused}
321 endDateAriaLabel={endDateAriaLabel}
322 isFocused={isFocused}
323 disabled={disabled}
324 required={required}
325 readOnly={readOnly}
326 openDirection={openDirection}
327 showCaret={showCaret}
328 showDefaultInputIcon={showDefaultInputIcon}
329 inputIconPosition={inputIconPosition}
330 customInputIcon={customInputIcon}
331 customArrowIcon={customArrowIcon}
332 customCloseIcon={customCloseIcon}
333 phrases={phrases}
334 onStartDateChange={this.onStartDateChange}
335 onStartDateFocus={this.onStartDateFocus}
336 onStartDateShiftTab={this.onClearFocus}
337 onEndDateChange={this.onEndDateChange}
338 onEndDateFocus={this.onEndDateFocus}
339 showClearDates={showClearDates}
340 onClearDates={this.clearDates}
341 screenReaderMessage={screenReaderMessage}
342 onKeyDownArrowDown={onKeyDownArrowDown}
343 onKeyDownQuestionMark={onKeyDownQuestionMark}
344 isRTL={isRTL}
345 noBorder={noBorder}
346 block={block}
347 small={small}
348 regular={regular}
349 verticalSpacing={verticalSpacing}
350 >
351 {children}
352 </DateRangePickerInput>
353 );
354 }
355}
356
357DateRangePickerInputController.propTypes = propTypes;
358DateRangePickerInputController.defaultProps = defaultProps;