1 | import React from 'react';
|
2 | import PropTypes from 'prop-types';
|
3 | import moment from 'moment';
|
4 |
|
5 | import momentPropTypes from 'react-moment-proptypes';
|
6 | import { forbidExtraProps, nonNegativeInteger } from 'airbnb-prop-types';
|
7 | import openDirectionShape from '../shapes/OpenDirectionShape';
|
8 |
|
9 | import { DateRangePickerInputPhrases } from '../defaultPhrases';
|
10 | import getPhrasePropTypes from '../utils/getPhrasePropTypes';
|
11 |
|
12 | import DateRangePickerInput from './DateRangePickerInput';
|
13 |
|
14 | import IconPositionShape from '../shapes/IconPositionShape';
|
15 | import DisabledShape from '../shapes/DisabledShape';
|
16 |
|
17 | import toMomentObject from '../utils/toMomentObject';
|
18 | import toLocalizedDateString from '../utils/toLocalizedDateString';
|
19 |
|
20 | import isInclusivelyAfterDay from '../utils/isInclusivelyAfterDay';
|
21 | import isBeforeDay from '../utils/isBeforeDay';
|
22 |
|
23 | import {
|
24 | START_DATE,
|
25 | END_DATE,
|
26 | ICON_BEFORE_POSITION,
|
27 | OPEN_DOWN,
|
28 | } from '../constants';
|
29 |
|
30 | const 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 |
|
78 | isFocused: PropTypes.bool,
|
79 |
|
80 |
|
81 | phrases: PropTypes.shape(getPhrasePropTypes(DateRangePickerInputPhrases)),
|
82 |
|
83 | isRTL: PropTypes.bool,
|
84 | });
|
85 |
|
86 | const 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 |
|
134 | isFocused: false,
|
135 |
|
136 |
|
137 | phrases: DateRangePickerInputPhrases,
|
138 |
|
139 | isRTL: false,
|
140 | };
|
141 |
|
142 | export 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 |
|
201 |
|
202 |
|
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 |
|
357 | DateRangePickerInputController.propTypes = propTypes;
|
358 | DateRangePickerInputController.defaultProps = defaultProps;
|