import { Accessibility } from '@fluentui/accessibility'; import { DateRangeType, DayOfWeek, FirstWeekOfYear, isRestrictedDate, DEFAULT_CALENDAR_STRINGS, IDayGridOptions, ICalendarStrings, IDatepickerOptions, } from '@fluentui/date-time-utilities'; import { ComponentWithAs, getElementType, useAccessibility, useFluentContext, useStyles, useTelemetry, useUnhandledProps, useAutoControlled, } from '@fluentui/react-bindings'; import { Ref } from '@fluentui/react-component-ref'; import { CalendarIcon } from '@fluentui/react-icons-northstar'; import * as customPropTypes from '@fluentui/react-proptypes'; import * as _ from 'lodash'; import * as PropTypes from 'prop-types'; import * as React from 'react'; import { ComponentEventHandler, FluentComponentStaticProps, ShorthandValue } from '../../types'; import { commonPropTypes, createShorthand, createShorthandFactory, UIComponentProps } from '../../utils'; import { Button } from '../Button/Button'; import { Input, InputProps } from '../Input/Input'; import { Popup, PopupProps } from '../Popup/Popup'; import { DatepickerCalendar, DatepickerCalendarProps } from './DatepickerCalendar'; import { DatepickerCalendarCell } from './DatepickerCalendarCell'; import { DatepickerCalendarHeader } from './DatepickerCalendarHeader'; import { DatepickerCalendarHeaderAction } from './DatepickerCalendarHeaderAction'; import { DatepickerCalendarHeaderCell } from './DatepickerCalendarHeaderCell'; export interface DatepickerProps extends UIComponentProps, Partial, Partial { /** Accessibility behavior if overridden by the user. */ accessibility?: Accessibility; /** Shorthand for the datepicker calendar. */ calendar?: ShorthandValue; /** Shorthand for the datepicker popup. */ popup?: ShorthandValue; /** Shorthand for the date text input. */ input?: ShorthandValue; /** Datepicker shows it is currently unable to be interacted with. */ disabled?: boolean; /** Datepicker shows it is currently unable to be interacted with. */ required?: boolean; /** * Called on change of the date. * * @param event - React's original SyntheticEvent. * @param data - All props and proposed value. */ onDateChange?: ComponentEventHandler; /** Text placeholder for the input field. */ placeholder?: string; /** Target dates can be also entered through the input field. */ allowManualInput?: boolean; /** Should calendar be initially opened or closed. */ defaultCalendarOpenState?: boolean; } export type DatepickerStylesProps = never; export const datepickerClassName = 'ui-datepicker'; enum OpenState { Open, Closing, Closed, } /** * A Datepicker is used to display dates. * This component is currently UNSTABLE! */ export const Datepicker: ComponentWithAs<'div', DatepickerProps> & FluentComponentStaticProps & { Calendar: typeof DatepickerCalendar; CalendarHeader: typeof DatepickerCalendarHeader; CalendarHeaderAction: typeof DatepickerCalendarHeaderAction; CalendarHeaderCell: typeof DatepickerCalendarHeaderCell; CalendarCell: typeof DatepickerCalendarCell; Input: typeof Input; } = props => { const context = useFluentContext(); const { setStart, setEnd } = useTelemetry(Datepicker.displayName, context.telemetry); setStart(); const datepickerRef = React.useRef(); const [openState, setOpenState] = useAutoControlled({ defaultValue: props.defaultCalendarOpenState ? OpenState.Open : OpenState.Closed, value: undefined, initialValue: OpenState.Closed, }); const [selectedDate, setSelectedDate] = React.useState(); const [formattedDate, setFormattedDate] = React.useState(''); const [error, setError] = React.useState(() => props.required && !selectedDate ? props.isRequiredErrorMessage : '', ); const { calendar, popup, input, className, design, styles, variables, formatMonthDayYear } = props; const valueFormatter = date => (date ? formatMonthDayYear(date) : ''); const nonNullSelectedDate = selectedDate ?? props.today ?? new Date(); const calendarOptions: IDayGridOptions = { selectedDate: nonNullSelectedDate, navigatedDate: nonNullSelectedDate, firstDayOfWeek: props.firstDayOfWeek, firstWeekOfYear: props.firstWeekOfYear, dateRangeType: props.dateRangeType, daysToSelectInDayView: props.daysToSelectInDayView, today: props.today, showWeekNumbers: props.showWeekNumbers, workWeekDays: props.workWeekDays, minDate: props.minDate, maxDate: props.maxDate, restrictedDates: props.restrictedDates, }; const dateFormatting: ICalendarStrings = { formatDay: props.formatDay, formatYear: props.formatYear, formatMonthDayYear: props.formatMonthDayYear, formatMonthYear: props.formatMonthYear, parseDate: props.parseDate, months: props.months, shortMonths: props.shortMonths, days: props.days, shortDays: props.shortDays, isRequiredErrorMessage: props.isRequiredErrorMessage, invalidInputErrorMessage: props.invalidInputErrorMessage, isOutOfBoundsErrorMessage: props.isOutOfBoundsErrorMessage, goToToday: props.goToToday, prevMonthAriaLabel: props.prevMonthAriaLabel, nextMonthAriaLabel: props.nextMonthAriaLabel, prevYearAriaLabel: props.prevYearAriaLabel, nextYearAriaLabel: props.nextYearAriaLabel, prevYearRangeAriaLabel: props.prevYearRangeAriaLabel, nextYearRangeAriaLabel: props.nextYearRangeAriaLabel, monthPickerHeaderAriaLabel: props.monthPickerHeaderAriaLabel, yearPickerHeaderAriaLabel: props.yearPickerHeaderAriaLabel, closeButtonAriaLabel: props.closeButtonAriaLabel, weekNumberFormatString: props.weekNumberFormatString, selectedDateFormatString: props.selectedDateFormatString, todayDateFormatString: props.todayDateFormatString, }; const ElementType = getElementType(props); const unhandledProps = useUnhandledProps(Datepicker.handledProps, props); const getA11yProps = useAccessibility(props.accessibility, { debugName: Datepicker.displayName, actionHandlers: {}, rtl: context.rtl, }); const { classes } = useStyles(Datepicker.displayName, { className: datepickerClassName, mapPropsToInlineStyles: () => ({ className, design, styles, variables, }), rtl: context.rtl, }); const overrideDatepickerCalendarProps = (predefinedProps: DatepickerCalendarProps): DatepickerCalendarProps => ({ ...calendarOptions, ...dateFormatting, onDateChange: (e, itemProps) => { const targetDay = itemProps.value; setSelectedDate(targetDay.originalDate); setOpenState(OpenState.Closing); setError(''); setFormattedDate(valueFormatter(targetDay.originalDate)); _.invoke(props, 'onDateChange', e, { ...props, value: targetDay.originalDate }); }, }); const calendarElement = createShorthand(DatepickerCalendar, calendar, { defaultProps: () => getA11yProps('calendar', {}), overrideProps: overrideDatepickerCalendarProps, }); const openStateToBooleanKnob = (openState: OpenState): boolean => { return openState === OpenState.Open; }; const onInputClick = (): void => { if (openState === OpenState.Closed) { setOpenState(OpenState.Open); } else if (openState === OpenState.Open || openState === OpenState.Closing) { // Keep popup open in case we can only enter the date through calendar. if (props.allowManualInput) { setOpenState(OpenState.Closed); } else { setOpenState(OpenState.Open); } } }; const onInputChange = (e, target: { value: string }) => { const parsedDate = props.parseDate(target.value); setFormattedDate(target.value); if (parsedDate) { if (isRestrictedDate(parsedDate, calendarOptions)) { setError(props.isOutOfBoundsErrorMessage); } else { setError(''); setSelectedDate(parsedDate); _.invoke(props, 'onDateChange', e, { ...props, value: parsedDate }); } } else if (target.value) { setError(props.invalidInputErrorMessage); } else if (props.required && !selectedDate) { setError(props.isRequiredErrorMessage); } else { setError(''); } }; const element = ( {getA11yProps.unstable_wrapWithFocusZone( {createShorthand(Input, input, { defaultProps: () => ({ disabled: props.disabled || !props.allowManualInput, error: !!error, value: formattedDate, }), overrideProps: (predefinedProps: InputProps): InputProps => ({ onClick: onInputClick, onChange: onInputChange, }), })} {createShorthand(Popup, popup, { defaultProps: () => ({ open: openStateToBooleanKnob(openState) && !props.disabled, content: calendarElement, trapFocus: { disableFirstFocus: true, }, trigger: