406 lines
22 KiB
JavaScript
406 lines
22 KiB
JavaScript
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.DatePickerBase = void 0;
|
|
var tslib_1 = require("tslib");
|
|
var React = require("react");
|
|
var utilities_1 = require("@fluentui/utilities");
|
|
var Calendar_1 = require("../../Calendar");
|
|
var date_time_utilities_1 = require("@fluentui/date-time-utilities");
|
|
var Callout_1 = require("../../Callout");
|
|
var Styling_1 = require("../../Styling");
|
|
var TextField_1 = require("../../TextField");
|
|
var FocusTrapZone_1 = require("../../FocusTrapZone");
|
|
var react_hooks_1 = require("@fluentui/react-hooks");
|
|
var defaults_1 = require("./defaults");
|
|
var getClassNames = (0, utilities_1.classNamesFunction)();
|
|
var DEFAULT_PROPS = {
|
|
allowTextInput: false,
|
|
formatDate: function (date) { return (date ? date.toDateString() : ''); },
|
|
parseDateFromString: function (dateStr) {
|
|
//if dateStr is DATE ONLY ISO 8601 -> add time so Date.parse() won't convert it to UTC
|
|
//See here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/parse#date_time_string_format
|
|
if (dateStr.match(/^\d{4}(-\d{2}){2}$/)) {
|
|
dateStr += 'T12:00';
|
|
}
|
|
var date = Date.parse(dateStr);
|
|
return date ? new Date(date) : null;
|
|
},
|
|
firstDayOfWeek: date_time_utilities_1.DayOfWeek.Sunday,
|
|
initialPickerDate: new Date(),
|
|
isRequired: false,
|
|
isMonthPickerVisible: true,
|
|
showMonthPickerAsOverlay: false,
|
|
strings: defaults_1.defaultDatePickerStrings,
|
|
highlightCurrentMonth: false,
|
|
highlightSelectedMonth: false,
|
|
borderless: false,
|
|
pickerAriaLabel: 'Calendar',
|
|
showWeekNumbers: false,
|
|
firstWeekOfYear: date_time_utilities_1.FirstWeekOfYear.FirstDay,
|
|
showGoToToday: true,
|
|
showCloseButton: false,
|
|
underlined: false,
|
|
allFocusable: false,
|
|
};
|
|
function useFocusLogic() {
|
|
var textFieldRef = React.useRef(null);
|
|
var preventFocusOpeningPicker = React.useRef(false);
|
|
var focus = function () {
|
|
var _a, _b;
|
|
(_b = (_a = textFieldRef.current) === null || _a === void 0 ? void 0 : _a.focus) === null || _b === void 0 ? void 0 : _b.call(_a);
|
|
};
|
|
var preventNextFocusOpeningPicker = function () {
|
|
preventFocusOpeningPicker.current = true;
|
|
};
|
|
return [textFieldRef, focus, preventFocusOpeningPicker, preventNextFocusOpeningPicker];
|
|
}
|
|
function useCalendarVisibility(_a, focus) {
|
|
var allowTextInput = _a.allowTextInput, onAfterMenuDismiss = _a.onAfterMenuDismiss;
|
|
var _b = React.useState(false), isCalendarShown = _b[0], setIsCalendarShown = _b[1];
|
|
var isMounted = React.useRef(false);
|
|
var async = (0, react_hooks_1.useAsync)();
|
|
React.useEffect(function () {
|
|
if (isMounted.current && !isCalendarShown) {
|
|
// In browsers like IE, textfield gets unfocused when datepicker is collapsed
|
|
if (allowTextInput) {
|
|
async.requestAnimationFrame(focus);
|
|
}
|
|
// If DatePicker's menu (Calendar) is closed, run onAfterMenuDismiss
|
|
onAfterMenuDismiss === null || onAfterMenuDismiss === void 0 ? void 0 : onAfterMenuDismiss();
|
|
}
|
|
isMounted.current = true;
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [isCalendarShown]);
|
|
return [isCalendarShown, setIsCalendarShown];
|
|
}
|
|
function useSelectedDate(_a) {
|
|
var formatDate = _a.formatDate, value = _a.value, onSelectDate = _a.onSelectDate;
|
|
var _b = (0, react_hooks_1.useControllableValue)(value, undefined, function (ev, newValue) {
|
|
return onSelectDate === null || onSelectDate === void 0 ? void 0 : onSelectDate(newValue);
|
|
}), selectedDate = _b[0], setSelectedDateState = _b[1];
|
|
var _c = React.useState(function () { return (value && formatDate ? formatDate(value) : ''); }), formattedDate = _c[0], setFormattedDate = _c[1];
|
|
var setSelectedDate = function (newDate) {
|
|
setSelectedDateState(newDate);
|
|
setFormattedDate(newDate && formatDate ? formatDate(newDate) : '');
|
|
};
|
|
React.useEffect(function () {
|
|
setFormattedDate(value && formatDate ? formatDate(value) : '');
|
|
}, [formatDate, value]);
|
|
return [selectedDate, formattedDate, setSelectedDate, setFormattedDate];
|
|
}
|
|
function useErrorMessage(_a, selectedDate, setSelectedDate, inputValue, isCalendarShown) {
|
|
var _b;
|
|
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, textField = _a.textField;
|
|
var _c = React.useState(), errorMessage = _c[0], setErrorMessage = _c[1];
|
|
var _d = React.useState(), statusMessage = _d[0], setStatusMessage = _d[1];
|
|
var isFirstLoadRef = React.useRef(true);
|
|
var validateOnLoad = (_b = textField === null || textField === void 0 ? void 0 : textField.validateOnLoad) !== null && _b !== void 0 ? _b : true;
|
|
var validateTextInput = function (date) {
|
|
if (date === void 0) { date = null; }
|
|
if (allowTextInput) {
|
|
if (inputValue || date) {
|
|
// Don't parse if the selected date has the same formatted string as what we're about to parse.
|
|
// The formatted string might be ambiguous (ex: "1/2/3" or "New Year Eve") and the parser might
|
|
// not be able to come up with the exact same date.
|
|
if (selectedDate && !errorMessage && formatDate && formatDate(date !== null && date !== void 0 ? date : selectedDate) === inputValue) {
|
|
return;
|
|
}
|
|
date = date || parseDateFromString(inputValue);
|
|
// Check if date is null, or date is Invalid Date
|
|
if (!date || isNaN(date.getTime())) {
|
|
// Reset invalid input field, if formatting is available
|
|
setSelectedDate(selectedDate);
|
|
// default the newer isResetStatusMessage string to invalidInputErrorMessage for legacy support
|
|
var selectedText = formatDate ? formatDate(selectedDate) : '';
|
|
var statusText = strings.isResetStatusMessage
|
|
? (0, utilities_1.format)(strings.isResetStatusMessage, inputValue, selectedText)
|
|
: strings.invalidInputErrorMessage || '';
|
|
setStatusMessage(statusText);
|
|
}
|
|
else {
|
|
// Check against optional date boundaries
|
|
if (isDateOutOfBounds(date, minDate, maxDate)) {
|
|
setErrorMessage(strings.isOutOfBoundsErrorMessage || ' ');
|
|
}
|
|
else {
|
|
setSelectedDate(date);
|
|
setErrorMessage(undefined);
|
|
setStatusMessage(undefined);
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
// Only show error for empty inputValue if it is a required field
|
|
setErrorMessage(isRequired ? strings.isRequiredErrorMessage || ' ' : undefined);
|
|
// If no input date string or input date string is invalid
|
|
// date variable will be null, callback should expect null value for this case
|
|
onSelectDate === null || onSelectDate === void 0 ? void 0 : onSelectDate(date);
|
|
}
|
|
}
|
|
else if (isRequired && !inputValue) {
|
|
// Check when DatePicker is a required field but has NO input value
|
|
setErrorMessage(strings.isRequiredErrorMessage || ' ');
|
|
}
|
|
else {
|
|
// Cleanup the error message and status message
|
|
setErrorMessage(undefined);
|
|
setStatusMessage(undefined);
|
|
}
|
|
};
|
|
React.useEffect(function () {
|
|
if (isFirstLoadRef.current) {
|
|
isFirstLoadRef.current = false;
|
|
if (!validateOnLoad) {
|
|
return;
|
|
}
|
|
}
|
|
if (isRequired && !selectedDate) {
|
|
setErrorMessage(strings.isRequiredErrorMessage || ' ');
|
|
}
|
|
else if (selectedDate && isDateOutOfBounds(selectedDate, minDate, maxDate)) {
|
|
setErrorMessage(strings.isOutOfBoundsErrorMessage || ' ');
|
|
}
|
|
else {
|
|
setErrorMessage(undefined);
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [
|
|
// We don't want to compare the date itself, since two instances of date at the same time are not equal
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
minDate && (0, date_time_utilities_1.getDatePartHashValue)(minDate),
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
maxDate && (0, date_time_utilities_1.getDatePartHashValue)(maxDate),
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
selectedDate && (0, date_time_utilities_1.getDatePartHashValue)(selectedDate),
|
|
isRequired,
|
|
validateOnLoad,
|
|
]);
|
|
return [
|
|
isCalendarShown ? undefined : errorMessage,
|
|
validateTextInput,
|
|
setErrorMessage,
|
|
isCalendarShown ? undefined : statusMessage,
|
|
setStatusMessage,
|
|
];
|
|
}
|
|
exports.DatePickerBase = React.forwardRef(function (propsWithoutDefaults, forwardedRef) {
|
|
var _a, _b;
|
|
var props = (0, utilities_1.getPropsWithDefaults)(DEFAULT_PROPS, propsWithoutDefaults);
|
|
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, _c = props.calendarAs, CalendarType = _c === void 0 ? Calendar_1.Calendar : _c, tabIndex = props.tabIndex, _d = props.disableAutoFocus, disableAutoFocus = _d === void 0 ? true : _d;
|
|
var id = (0, react_hooks_1.useId)('DatePicker', props.id);
|
|
var calloutId = (0, react_hooks_1.useId)('DatePicker-Callout');
|
|
var calendar = React.useRef(null);
|
|
var datePickerDiv = React.useRef(null);
|
|
var _e = useFocusLogic(), textFieldRef = _e[0], focus = _e[1], preventFocusOpeningPicker = _e[2], preventNextFocusOpeningPicker = _e[3];
|
|
var _f = useCalendarVisibility(props, focus), isCalendarShown = _f[0], setIsCalendarShown = _f[1];
|
|
var _g = useSelectedDate(props), selectedDate = _g[0], formattedDate = _g[1], setSelectedDate = _g[2], setFormattedDate = _g[3];
|
|
var _h = useErrorMessage(props, selectedDate, setSelectedDate, formattedDate, isCalendarShown), errorMessage = _h[0], validateTextInput = _h[1], setErrorMessage = _h[2], statusMessage = _h[3], setStatusMessage = _h[4];
|
|
var showDatePickerPopup = React.useCallback(function () {
|
|
if (!isCalendarShown) {
|
|
preventNextFocusOpeningPicker();
|
|
setIsCalendarShown(true);
|
|
}
|
|
}, [isCalendarShown, preventNextFocusOpeningPicker, setIsCalendarShown]);
|
|
React.useImperativeHandle(props.componentRef, function () { return ({
|
|
focus: focus,
|
|
reset: function () {
|
|
setIsCalendarShown(false);
|
|
setSelectedDate(undefined);
|
|
setErrorMessage(undefined);
|
|
setStatusMessage(undefined);
|
|
},
|
|
showDatePickerPopup: showDatePickerPopup,
|
|
}); }, [focus, setErrorMessage, setIsCalendarShown, setSelectedDate, setStatusMessage, showDatePickerPopup]);
|
|
var onTextFieldFocus = function () {
|
|
if (disableAutoFocus) {
|
|
return;
|
|
}
|
|
if (!allowTextInput) {
|
|
if (!preventFocusOpeningPicker.current) {
|
|
showDatePickerPopup();
|
|
}
|
|
preventFocusOpeningPicker.current = false;
|
|
}
|
|
};
|
|
var onSelectDate = function (date) {
|
|
if (props.calendarProps && props.calendarProps.onSelectDate) {
|
|
props.calendarProps.onSelectDate(date);
|
|
}
|
|
calendarDismissed(date);
|
|
};
|
|
var onCalloutPositioned = function () {
|
|
var shouldFocus = true;
|
|
// If the user has specified that the callout shouldn't use initial focus, then respect
|
|
// that and don't attempt to set focus. That will default to true within the callout
|
|
// so we need to check if it's undefined here.
|
|
if (props.calloutProps && props.calloutProps.setInitialFocus !== undefined) {
|
|
shouldFocus = props.calloutProps.setInitialFocus;
|
|
}
|
|
if (calendar.current && shouldFocus) {
|
|
calendar.current.focus();
|
|
}
|
|
};
|
|
var onTextFieldBlur = function (ev) {
|
|
validateTextInput();
|
|
};
|
|
var onTextFieldChanged = function (ev, newValue) {
|
|
var _a;
|
|
var textField = props.textField;
|
|
if (allowTextInput) {
|
|
if (isCalendarShown) {
|
|
dismissDatePickerPopup();
|
|
}
|
|
setFormattedDate(newValue);
|
|
}
|
|
(_a = textField === null || textField === void 0 ? void 0 : textField.onChange) === null || _a === void 0 ? void 0 : _a.call(textField, ev, newValue);
|
|
};
|
|
var onTextFieldKeyDown = function (ev) {
|
|
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
switch (ev.which) {
|
|
case utilities_1.KeyCodes.enter:
|
|
ev.preventDefault();
|
|
ev.stopPropagation();
|
|
if (!isCalendarShown) {
|
|
validateTextInput();
|
|
showDatePickerPopup();
|
|
}
|
|
else {
|
|
// When DatePicker allows input date string directly,
|
|
// it is expected to hit another enter to close the popup
|
|
if (props.allowTextInput) {
|
|
dismissDatePickerPopup();
|
|
}
|
|
}
|
|
break;
|
|
case utilities_1.KeyCodes.escape:
|
|
handleEscKey(ev);
|
|
break;
|
|
case utilities_1.KeyCodes.down:
|
|
if (ev.altKey && !isCalendarShown) {
|
|
showDatePickerPopup();
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
};
|
|
var onTextFieldClick = function (ev) {
|
|
// default openOnClick to !props.disableAutoFocus for legacy support of disableAutoFocus behavior
|
|
var openOnClick = props.openOnClick || !props.disableAutoFocus;
|
|
if (openOnClick && !isCalendarShown && !props.disabled) {
|
|
showDatePickerPopup();
|
|
return;
|
|
}
|
|
if (props.allowTextInput) {
|
|
dismissDatePickerPopup();
|
|
}
|
|
};
|
|
var onIconClick = function (ev) {
|
|
ev.stopPropagation();
|
|
if (!isCalendarShown && !props.disabled) {
|
|
showDatePickerPopup();
|
|
}
|
|
else if (props.allowTextInput) {
|
|
dismissDatePickerPopup();
|
|
}
|
|
};
|
|
var dismissDatePickerPopup = function (newlySelectedDate) {
|
|
if (isCalendarShown) {
|
|
setIsCalendarShown(false);
|
|
validateTextInput(newlySelectedDate);
|
|
if (!allowTextInput && newlySelectedDate) {
|
|
setSelectedDate(newlySelectedDate);
|
|
}
|
|
}
|
|
};
|
|
var renderTextfieldDescription = function (inputProps, defaultRender) {
|
|
return (React.createElement(React.Fragment, null,
|
|
inputProps.description || inputProps.onRenderDescription ? defaultRender(inputProps) : null,
|
|
React.createElement("div", { "aria-live": "assertive", className: classNames.statusMessage }, statusMessage)));
|
|
};
|
|
var renderReadOnlyInput = function (inputProps) {
|
|
var divProps = (0, utilities_1.getNativeProps)(inputProps, utilities_1.divProperties);
|
|
// Need to merge styles so the provided styles win over the default ones. This is due to the classnames having the
|
|
// same specificity.
|
|
var readOnlyTextFieldClassName = (0, Styling_1.mergeStyles)(divProps.className, classNames.readOnlyTextField);
|
|
// Talkback on Android treats readonly inputs as disabled, so swipe gestures to open the Calendar
|
|
// don't register. Workaround is rendering a div with role="combobox" (passed in via TextField props).
|
|
return (React.createElement("div", tslib_1.__assign({}, divProps, { className: readOnlyTextFieldClassName, tabIndex: tabIndex || 0 }), formattedDate || (
|
|
// Putting the placeholder in a separate span fixes specificity issues for the text color
|
|
React.createElement("span", { className: classNames.readOnlyPlaceholder }, placeholder))));
|
|
};
|
|
/**
|
|
* Callback for closing the calendar callout
|
|
*/
|
|
var calendarDismissed = function (newlySelectedDate) {
|
|
preventNextFocusOpeningPicker();
|
|
dismissDatePickerPopup(newlySelectedDate);
|
|
// don't need to focus the text box, if necessary the focusTrapZone will do it
|
|
};
|
|
var calloutDismissed = function (ev) {
|
|
calendarDismissed();
|
|
};
|
|
var handleEscKey = function (ev) {
|
|
if (isCalendarShown) {
|
|
ev.stopPropagation();
|
|
calendarDismissed();
|
|
}
|
|
};
|
|
var onCalendarDismissed = function (ev) {
|
|
calendarDismissed();
|
|
};
|
|
var classNames = getClassNames(styles, {
|
|
theme: theme,
|
|
className: className,
|
|
disabled: disabled,
|
|
underlined: underlined,
|
|
label: !!label,
|
|
isDatePickerShown: isCalendarShown,
|
|
});
|
|
var nativeProps = (0, utilities_1.getNativeProps)(props, utilities_1.divProperties, ['value']);
|
|
var iconProps = textFieldProps && textFieldProps.iconProps;
|
|
var textFieldId = textFieldProps && textFieldProps.id && textFieldProps.id !== id ? textFieldProps.id : id + '-label';
|
|
var readOnly = !allowTextInput && !disabled;
|
|
var dataIsFocusable = (_b = (_a = textFieldProps === null || textFieldProps === void 0 ? void 0 : textFieldProps['data-is-focusable']) !== null && _a !== void 0 ? _a : props['data-is-focusable']) !== null && _b !== void 0 ? _b : true;
|
|
// Props to create a semantic but non-focusable button when the datepicker has a text input
|
|
// Used for voice control and touch screen reader accessibility
|
|
var iconA11yProps = allowTextInput
|
|
? {
|
|
role: 'button',
|
|
'aria-expanded': isCalendarShown,
|
|
'aria-label': ariaLabel !== null && ariaLabel !== void 0 ? ariaLabel : label,
|
|
'aria-labelledby': textFieldProps && textFieldProps['aria-labelledby'],
|
|
}
|
|
: {};
|
|
return (React.createElement("div", tslib_1.__assign({}, nativeProps, { className: classNames.root, ref: forwardedRef }),
|
|
React.createElement("div", { ref: datePickerDiv, "aria-owns": isCalendarShown ? calloutId : undefined, className: classNames.wrapper },
|
|
React.createElement(TextField_1.TextField, tslib_1.__assign({ role: "combobox", label: label, "aria-expanded": isCalendarShown, "aria-required": isRequired, 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, { "data-is-focusable": dataIsFocusable, id: textFieldId, className: (0, utilities_1.css)(classNames.textField, textFieldProps && textFieldProps.className), iconProps: tslib_1.__assign(tslib_1.__assign(tslib_1.__assign({ iconName: 'Calendar' }, iconA11yProps), iconProps), { className: (0, utilities_1.css)(classNames.icon, iconProps && iconProps.className), onClick: onIconClick }),
|
|
// eslint-disable-next-line react/jsx-no-bind
|
|
onRenderDescription: renderTextfieldDescription,
|
|
// eslint-disable-next-line react/jsx-no-bind
|
|
onKeyDown: onTextFieldKeyDown,
|
|
// eslint-disable-next-line react/jsx-no-bind
|
|
onFocus: onTextFieldFocus,
|
|
// eslint-disable-next-line react/jsx-no-bind
|
|
onBlur: onTextFieldBlur,
|
|
// eslint-disable-next-line react/jsx-no-bind
|
|
onClick: onTextFieldClick,
|
|
// eslint-disable-next-line react/jsx-no-bind
|
|
onChange: onTextFieldChanged, onRenderInput: readOnly ? renderReadOnlyInput : undefined }))),
|
|
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: (0, utilities_1.css)(classNames.callout, calloutProps && calloutProps.className),
|
|
// eslint-disable-next-line react/jsx-no-bind
|
|
onDismiss: calloutDismissed,
|
|
// eslint-disable-next-line react/jsx-no-bind
|
|
onPositioned: onCalloutPositioned }),
|
|
React.createElement(FocusTrapZone_1.FocusTrapZone, { isClickableOutsideFocusTrap: true, disableFirstFocus: disableAutoFocus },
|
|
React.createElement(CalendarType, tslib_1.__assign({}, calendarProps, {
|
|
// eslint-disable-next-line react/jsx-no-bind
|
|
onSelectDate: onSelectDate,
|
|
// eslint-disable-next-line react/jsx-no-bind
|
|
onDismiss: onCalendarDismissed, 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 })))))));
|
|
});
|
|
exports.DatePickerBase.displayName = 'DatePickerBase';
|
|
function isDateOutOfBounds(date, minDate, maxDate) {
|
|
return (!!minDate && (0, date_time_utilities_1.compareDatePart)(minDate, date) > 0) || (!!maxDate && (0, date_time_utilities_1.compareDatePart)(maxDate, date) < 0);
|
|
}
|
|
//# sourceMappingURL=DatePicker.base.js.map
|