UNPKG

20.7 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3exports.DatePickerBase = void 0;
4var tslib_1 = require("tslib");
5var React = require("react");
6var utilities_1 = require("@fluentui/utilities");
7var Calendar_1 = require("../../Calendar");
8var date_time_utilities_1 = require("@fluentui/date-time-utilities");
9var Callout_1 = require("../../Callout");
10var TextField_1 = require("../../TextField");
11var FocusTrapZone_1 = require("../../FocusTrapZone");
12var react_hooks_1 = require("@fluentui/react-hooks");
13var defaults_1 = require("./defaults");
14var getClassNames = utilities_1.classNamesFunction();
15var DEFAULT_PROPS = {
16 allowTextInput: false,
17 formatDate: function (date) { return (date ? date.toDateString() : ''); },
18 parseDateFromString: function (dateStr) {
19 var date = Date.parse(dateStr);
20 return date ? new Date(date) : null;
21 },
22 firstDayOfWeek: date_time_utilities_1.DayOfWeek.Sunday,
23 initialPickerDate: new Date(),
24 isRequired: false,
25 isMonthPickerVisible: true,
26 showMonthPickerAsOverlay: false,
27 strings: defaults_1.defaultDatePickerStrings,
28 highlightCurrentMonth: false,
29 highlightSelectedMonth: false,
30 borderless: false,
31 pickerAriaLabel: 'Calendar',
32 showWeekNumbers: false,
33 firstWeekOfYear: date_time_utilities_1.FirstWeekOfYear.FirstDay,
34 showGoToToday: true,
35 showCloseButton: false,
36 underlined: false,
37 allFocusable: false,
38};
39function useFocusLogic() {
40 var textFieldRef = React.useRef(null);
41 var preventFocusOpeningPicker = React.useRef(false);
42 var focus = function () {
43 var _a, _b;
44 (_b = (_a = textFieldRef.current) === null || _a === void 0 ? void 0 : _a.focus) === null || _b === void 0 ? void 0 : _b.call(_a);
45 };
46 var preventNextFocusOpeningPicker = function () {
47 preventFocusOpeningPicker.current = true;
48 };
49 return [textFieldRef, focus, preventFocusOpeningPicker, preventNextFocusOpeningPicker];
50}
51function useCalendarVisibility(_a, focus) {
52 var allowTextInput = _a.allowTextInput, onAfterMenuDismiss = _a.onAfterMenuDismiss;
53 var _b = React.useState(false), isCalendarShown = _b[0], setIsCalendarShown = _b[1];
54 var isMounted = React.useRef(false);
55 var async = react_hooks_1.useAsync();
56 React.useEffect(function () {
57 if (isMounted.current && !isCalendarShown) {
58 // In browsers like IE, textfield gets unfocused when datepicker is collapsed
59 if (allowTextInput) {
60 async.requestAnimationFrame(focus);
61 }
62 // If DatePicker's menu (Calendar) is closed, run onAfterMenuDismiss
63 onAfterMenuDismiss === null || onAfterMenuDismiss === void 0 ? void 0 : onAfterMenuDismiss();
64 }
65 isMounted.current = true;
66 // eslint-disable-next-line react-hooks/exhaustive-deps
67 }, [isCalendarShown]);
68 return [isCalendarShown, setIsCalendarShown];
69}
70function useSelectedDate(_a) {
71 var formatDate = _a.formatDate, value = _a.value, onSelectDate = _a.onSelectDate;
72 var _b = react_hooks_1.useControllableValue(value, undefined, function (ev, newValue) {
73 return onSelectDate === null || onSelectDate === void 0 ? void 0 : onSelectDate(newValue);
74 }), selectedDate = _b[0], setSelectedDateState = _b[1];
75 var _c = React.useState(function () { return (value && formatDate ? formatDate(value) : ''); }), formattedDate = _c[0], setFormattedDate = _c[1];
76 var setSelectedDate = function (newDate) {
77 setSelectedDateState(newDate);
78 setFormattedDate(newDate && formatDate ? formatDate(newDate) : '');
79 };
80 React.useEffect(function () {
81 setFormattedDate(value && formatDate ? formatDate(value) : '');
82 }, [formatDate, value]);
83 return [selectedDate, formattedDate, setSelectedDate, setFormattedDate];
84}
85function useErrorMessage(_a, selectedDate, setSelectedDate, inputValue, isCalendarShown) {
86 var isRequired = _a.isRequired, allowTextInput = _a.allowTextInput, strings = _a.strings, parseDateFromString = _a.parseDateFromString, onSelectDate = _a.onSelectDate, formatDate = _a.formatDate, minDate = _a.minDate, maxDate = _a.maxDate;
87 var _b = React.useState(), errorMessage = _b[0], setErrorMessage = _b[1];
88 var _c = React.useState(), statusMessage = _c[0], setStatusMessage = _c[1];
89 var validateTextInput = function (date) {
90 if (date === void 0) { date = null; }
91 if (allowTextInput) {
92 if (inputValue || date) {
93 // Don't parse if the selected date has the same formatted string as what we're about to parse.
94 // The formatted string might be ambiguous (ex: "1/2/3" or "New Year Eve") and the parser might
95 // not be able to come up with the exact same date.
96 if (selectedDate && !errorMessage && formatDate && formatDate(date !== null && date !== void 0 ? date : selectedDate) === inputValue) {
97 return;
98 }
99 date = date || parseDateFromString(inputValue);
100 // Check if date is null, or date is Invalid Date
101 if (!date || isNaN(date.getTime())) {
102 // Reset invalid input field, if formatting is available
103 setSelectedDate(selectedDate);
104 // default the newer isResetStatusMessage string to invalidInputErrorMessage for legacy support
105 var selectedText = formatDate ? formatDate(selectedDate) : '';
106 var statusText = strings.isResetStatusMessage
107 ? utilities_1.format(strings.isResetStatusMessage, inputValue, selectedText)
108 : strings.invalidInputErrorMessage || '';
109 setStatusMessage(statusText);
110 }
111 else {
112 // Check against optional date boundaries
113 if (isDateOutOfBounds(date, minDate, maxDate)) {
114 setErrorMessage(strings.isOutOfBoundsErrorMessage || ' ');
115 }
116 else {
117 setSelectedDate(date);
118 setErrorMessage(undefined);
119 setStatusMessage(undefined);
120 }
121 }
122 }
123 else {
124 // Only show error for empty inputValue if it is a required field
125 setErrorMessage(isRequired ? strings.isRequiredErrorMessage || ' ' : undefined);
126 // If no input date string or input date string is invalid
127 // date variable will be null, callback should expect null value for this case
128 onSelectDate === null || onSelectDate === void 0 ? void 0 : onSelectDate(date);
129 }
130 }
131 else if (isRequired && !inputValue) {
132 // Check when DatePicker is a required field but has NO input value
133 setErrorMessage(strings.isRequiredErrorMessage || ' ');
134 }
135 else {
136 // Cleanup the error message and status message
137 setErrorMessage(undefined);
138 setStatusMessage(undefined);
139 }
140 };
141 React.useEffect(function () {
142 if (isRequired && !selectedDate) {
143 setErrorMessage(strings.isRequiredErrorMessage || ' ');
144 }
145 else if (selectedDate && isDateOutOfBounds(selectedDate, minDate, maxDate)) {
146 setErrorMessage(strings.isOutOfBoundsErrorMessage || ' ');
147 }
148 else {
149 setErrorMessage(undefined);
150 }
151 // eslint-disable-next-line react-hooks/exhaustive-deps
152 }, [
153 // We don't want to compare the date itself, since two instances of date at the same time are not equal
154 // eslint-disable-next-line react-hooks/exhaustive-deps
155 minDate && date_time_utilities_1.getDatePartHashValue(minDate),
156 // eslint-disable-next-line react-hooks/exhaustive-deps
157 maxDate && date_time_utilities_1.getDatePartHashValue(maxDate),
158 // eslint-disable-next-line react-hooks/exhaustive-deps
159 selectedDate && date_time_utilities_1.getDatePartHashValue(selectedDate),
160 isRequired,
161 ]);
162 return [
163 isCalendarShown ? undefined : errorMessage,
164 validateTextInput,
165 setErrorMessage,
166 isCalendarShown ? undefined : statusMessage,
167 setStatusMessage,
168 ];
169}
170exports.DatePickerBase = React.forwardRef(function (propsWithoutDefaults, forwardedRef) {
171 var props = utilities_1.getPropsWithDefaults(DEFAULT_PROPS, propsWithoutDefaults);
172 var firstDayOfWeek = props.firstDayOfWeek, strings = props.strings, label = props.label, theme = props.theme, className = props.className, styles = props.styles, initialPickerDate = props.initialPickerDate, isRequired = props.isRequired, disabled = props.disabled, ariaLabel = props.ariaLabel, pickerAriaLabel = props.pickerAriaLabel, placeholder = props.placeholder, allowTextInput = props.allowTextInput, borderless = props.borderless, minDate = props.minDate, maxDate = props.maxDate, showCloseButton = props.showCloseButton, calendarProps = props.calendarProps, calloutProps = props.calloutProps, textFieldProps = props.textField, underlined = props.underlined, allFocusable = props.allFocusable, _a = props.calendarAs, CalendarType = _a === void 0 ? Calendar_1.Calendar : _a, tabIndex = props.tabIndex, _b = props.disableAutoFocus, disableAutoFocus = _b === void 0 ? true : _b;
173 var id = react_hooks_1.useId('DatePicker', props.id);
174 var calloutId = react_hooks_1.useId('DatePicker-Callout');
175 var calendar = React.useRef(null);
176 var datePickerDiv = React.useRef(null);
177 var _c = useFocusLogic(), textFieldRef = _c[0], focus = _c[1], preventFocusOpeningPicker = _c[2], preventNextFocusOpeningPicker = _c[3];
178 var _d = useCalendarVisibility(props, focus), isCalendarShown = _d[0], setIsCalendarShown = _d[1];
179 var _e = useSelectedDate(props), selectedDate = _e[0], formattedDate = _e[1], setSelectedDate = _e[2], setFormattedDate = _e[3];
180 var _f = useErrorMessage(props, selectedDate, setSelectedDate, formattedDate, isCalendarShown), errorMessage = _f[0], validateTextInput = _f[1], setErrorMessage = _f[2], statusMessage = _f[3], setStatusMessage = _f[4];
181 var showDatePickerPopup = React.useCallback(function () {
182 if (!isCalendarShown) {
183 preventNextFocusOpeningPicker();
184 setIsCalendarShown(true);
185 }
186 }, [isCalendarShown, preventNextFocusOpeningPicker, setIsCalendarShown]);
187 React.useImperativeHandle(props.componentRef, function () { return ({
188 focus: focus,
189 reset: function () {
190 setIsCalendarShown(false);
191 setSelectedDate(undefined);
192 setErrorMessage(undefined);
193 setStatusMessage(undefined);
194 },
195 showDatePickerPopup: showDatePickerPopup,
196 }); }, [focus, setErrorMessage, setIsCalendarShown, setSelectedDate, setStatusMessage, showDatePickerPopup]);
197 var onTextFieldFocus = function () {
198 if (disableAutoFocus) {
199 return;
200 }
201 if (!allowTextInput) {
202 if (!preventFocusOpeningPicker.current) {
203 showDatePickerPopup();
204 }
205 preventFocusOpeningPicker.current = false;
206 }
207 };
208 var onSelectDate = function (date) {
209 if (props.calendarProps && props.calendarProps.onSelectDate) {
210 props.calendarProps.onSelectDate(date);
211 }
212 calendarDismissed(date);
213 };
214 var onCalloutPositioned = function () {
215 var shouldFocus = true;
216 // If the user has specified that the callout shouldn't use initial focus, then respect
217 // that and don't attempt to set focus. That will default to true within the callout
218 // so we need to check if it's undefined here.
219 if (props.calloutProps && props.calloutProps.setInitialFocus !== undefined) {
220 shouldFocus = props.calloutProps.setInitialFocus;
221 }
222 if (calendar.current && shouldFocus) {
223 calendar.current.focus();
224 }
225 };
226 var onTextFieldBlur = function (ev) {
227 validateTextInput();
228 };
229 var onTextFieldChanged = function (ev, newValue) {
230 var _a;
231 var textField = props.textField;
232 if (allowTextInput) {
233 if (isCalendarShown) {
234 dismissDatePickerPopup();
235 }
236 setFormattedDate(newValue);
237 }
238 (_a = textField === null || textField === void 0 ? void 0 : textField.onChange) === null || _a === void 0 ? void 0 : _a.call(textField, ev, newValue);
239 };
240 var onTextFieldKeyDown = function (ev) {
241 // eslint-disable-next-line deprecation/deprecation
242 switch (ev.which) {
243 case utilities_1.KeyCodes.enter:
244 ev.preventDefault();
245 ev.stopPropagation();
246 if (!isCalendarShown) {
247 validateTextInput();
248 showDatePickerPopup();
249 }
250 else {
251 // When DatePicker allows input date string directly,
252 // it is expected to hit another enter to close the popup
253 if (props.allowTextInput) {
254 dismissDatePickerPopup();
255 }
256 }
257 break;
258 case utilities_1.KeyCodes.escape:
259 handleEscKey(ev);
260 break;
261 case utilities_1.KeyCodes.down:
262 if (ev.altKey && !isCalendarShown) {
263 showDatePickerPopup();
264 }
265 break;
266 default:
267 break;
268 }
269 };
270 var onTextFieldClick = function (ev) {
271 // default openOnClick to !props.disableAutoFocus for legacy support of disableAutoFocus behavior
272 var openOnClick = props.openOnClick || !props.disableAutoFocus;
273 if (openOnClick && !isCalendarShown && !props.disabled) {
274 showDatePickerPopup();
275 return;
276 }
277 if (props.allowTextInput) {
278 dismissDatePickerPopup();
279 }
280 };
281 var onIconClick = function (ev) {
282 ev.stopPropagation();
283 if (!isCalendarShown && !props.disabled) {
284 showDatePickerPopup();
285 }
286 else if (props.allowTextInput) {
287 dismissDatePickerPopup();
288 }
289 };
290 var dismissDatePickerPopup = function (newlySelectedDate) {
291 if (isCalendarShown) {
292 setIsCalendarShown(false);
293 validateTextInput(newlySelectedDate);
294 if (!allowTextInput && newlySelectedDate) {
295 setSelectedDate(newlySelectedDate);
296 }
297 }
298 };
299 var renderTextfieldDescription = function (inputProps, defaultRender) {
300 return (React.createElement(React.Fragment, null,
301 inputProps.description ? defaultRender(inputProps) : null,
302 React.createElement("div", { "aria-live": "assertive", className: classNames.statusMessage }, statusMessage)));
303 };
304 var renderReadOnlyInput = function (inputProps) {
305 var divProps = utilities_1.getNativeProps(inputProps, utilities_1.divProperties);
306 // Talkback on Android treats readonly inputs as disabled, so swipe gestures to open the Calendar
307 // don't register. Workaround is rendering a div with role="combobox" (passed in via TextField props).
308 return (React.createElement("div", tslib_1.__assign({}, divProps, { className: utilities_1.css(divProps.className, classNames.readOnlyTextField), tabIndex: tabIndex || 0 }), formattedDate || (
309 // Putting the placeholder in a separate span fixes specificity issues for the text color
310 React.createElement("span", { className: classNames.readOnlyPlaceholder }, placeholder))));
311 };
312 /**
313 * Callback for closing the calendar callout
314 */
315 var calendarDismissed = function (newlySelectedDate) {
316 preventNextFocusOpeningPicker();
317 dismissDatePickerPopup(newlySelectedDate);
318 // don't need to focus the text box, if necessary the focusTrapZone will do it
319 };
320 var calloutDismissed = function (ev) {
321 calendarDismissed();
322 };
323 var handleEscKey = function (ev) {
324 if (isCalendarShown) {
325 ev.stopPropagation();
326 calendarDismissed();
327 }
328 };
329 var classNames = getClassNames(styles, {
330 theme: theme,
331 className: className,
332 disabled: disabled,
333 underlined: underlined,
334 label: !!label,
335 isDatePickerShown: isCalendarShown,
336 });
337 var nativeProps = utilities_1.getNativeProps(props, utilities_1.divProperties, ['value']);
338 var iconProps = textFieldProps && textFieldProps.iconProps;
339 var textFieldId = textFieldProps && textFieldProps.id && textFieldProps.id !== id ? textFieldProps.id : id + '-label';
340 var readOnly = !allowTextInput && !disabled;
341 return (React.createElement("div", tslib_1.__assign({}, nativeProps, { className: classNames.root, ref: forwardedRef }),
342 React.createElement("div", { ref: datePickerDiv, "aria-owns": isCalendarShown ? calloutId : undefined, className: classNames.wrapper },
343 React.createElement(TextField_1.TextField, tslib_1.__assign({ role: "combobox", label: label, "aria-expanded": isCalendarShown, ariaLabel: ariaLabel, "aria-haspopup": "dialog", "aria-controls": isCalendarShown ? calloutId : undefined, required: isRequired, disabled: disabled, errorMessage: errorMessage, placeholder: placeholder, borderless: borderless, value: formattedDate, componentRef: textFieldRef, underlined: underlined, tabIndex: tabIndex, readOnly: !allowTextInput }, textFieldProps, { id: textFieldId, className: utilities_1.css(classNames.textField, textFieldProps && textFieldProps.className), iconProps: tslib_1.__assign(tslib_1.__assign({ iconName: 'Calendar' }, iconProps), { className: utilities_1.css(classNames.icon, iconProps && iconProps.className), onClick: onIconClick }),
344 // eslint-disable-next-line react/jsx-no-bind
345 onRenderDescription: renderTextfieldDescription,
346 // eslint-disable-next-line react/jsx-no-bind
347 onKeyDown: onTextFieldKeyDown,
348 // eslint-disable-next-line react/jsx-no-bind
349 onFocus: onTextFieldFocus,
350 // eslint-disable-next-line react/jsx-no-bind
351 onBlur: onTextFieldBlur,
352 // eslint-disable-next-line react/jsx-no-bind
353 onClick: onTextFieldClick,
354 // eslint-disable-next-line react/jsx-no-bind
355 onChange: onTextFieldChanged, onRenderInput: readOnly ? renderReadOnlyInput : undefined }))),
356 isCalendarShown && (React.createElement(Callout_1.Callout, tslib_1.__assign({ id: calloutId, role: "dialog", ariaLabel: pickerAriaLabel, isBeakVisible: false, gapSpace: 0, doNotLayer: false, target: datePickerDiv.current, directionalHint: Callout_1.DirectionalHint.bottomLeftEdge }, calloutProps, { className: utilities_1.css(classNames.callout, calloutProps && calloutProps.className),
357 // eslint-disable-next-line react/jsx-no-bind
358 onDismiss: calloutDismissed,
359 // eslint-disable-next-line react/jsx-no-bind
360 onPositioned: onCalloutPositioned }),
361 React.createElement(FocusTrapZone_1.FocusTrapZone, { isClickableOutsideFocusTrap: true, disableFirstFocus: disableAutoFocus },
362 React.createElement(CalendarType, tslib_1.__assign({}, calendarProps, {
363 // eslint-disable-next-line react/jsx-no-bind
364 onSelectDate: onSelectDate,
365 // eslint-disable-next-line react/jsx-no-bind
366 onDismiss: calendarDismissed, isMonthPickerVisible: props.isMonthPickerVisible, showMonthPickerAsOverlay: props.showMonthPickerAsOverlay, today: props.today, value: selectedDate || initialPickerDate, firstDayOfWeek: firstDayOfWeek, strings: strings, highlightCurrentMonth: props.highlightCurrentMonth, highlightSelectedMonth: props.highlightSelectedMonth, showWeekNumbers: props.showWeekNumbers, firstWeekOfYear: props.firstWeekOfYear, showGoToToday: props.showGoToToday, dateTimeFormatter: props.dateTimeFormatter, minDate: minDate, maxDate: maxDate, componentRef: calendar, showCloseButton: showCloseButton, allFocusable: allFocusable })))))));
367});
368exports.DatePickerBase.displayName = 'DatePickerBase';
369function isDateOutOfBounds(date, minDate, maxDate) {
370 return (!!minDate && date_time_utilities_1.compareDatePart(minDate, date) > 0) || (!!maxDate && date_time_utilities_1.compareDatePart(maxDate, date) < 0);
371}
372//# sourceMappingURL=DatePicker.base.js.map
\No newline at end of file