diff --git a/change/@fluentui-react-datepicker-compat-59a888f8-093f-449c-b49e-4f580894da5e.json b/change/@fluentui-react-datepicker-compat-59a888f8-093f-449c-b49e-4f580894da5e.json new file mode 100644 index 0000000000000..162abc60d4804 --- /dev/null +++ b/change/@fluentui-react-datepicker-compat-59a888f8-093f-449c-b49e-4f580894da5e.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "feat: Add error handling to DatePicker.", + "packageName": "@fluentui/react-datepicker-compat", + "email": "esteban.230@hotmail.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-components/react-datepicker-compat/etc/react-datepicker-compat.api.md b/packages/react-components/react-datepicker-compat/etc/react-datepicker-compat.api.md index 1fb0c3c435cc9..d6ad592e1bb4d 100644 --- a/packages/react-components/react-datepicker-compat/etc/react-datepicker-compat.api.md +++ b/packages/react-components/react-datepicker-compat/etc/react-datepicker-compat.api.md @@ -152,6 +152,11 @@ export const DatePicker: ForwardRefComponent; // @public (undocumented) export const datePickerClassNames: SlotClassNames; +// @public +export type DatePickerErrorData = { + error: 'invalid-input' | 'out-of-bounds' | 'required-input'; +}; + // @public (undocumented) export type DatePickerProps = Omit>, 'defaultValue' | 'value'> & { componentRef?: React_2.RefObject; @@ -167,6 +172,7 @@ export type DatePickerProps = Omit>, 'de defaultOpen?: boolean; open?: boolean; onOpenChange?: (open: boolean) => void; + onValidationError?: (data: DatePickerErrorData) => void; inlinePopup?: boolean; positioning?: PositioningProps; placeholder?: string; @@ -224,7 +230,10 @@ export enum DayOfWeek { export const DAYS_IN_WEEK = 7; // @public (undocumented) -export const defaultCalendarStrings: CalendarStrings; +export const defaultDatePickerErrorStrings: Record; + +// @public (undocumented) +export const defaultDatePickerStrings: CalendarStrings; // @public export enum FirstWeekOfYear { diff --git a/packages/react-components/react-datepicker-compat/package.json b/packages/react-components/react-datepicker-compat/package.json index 50edc08388701..8ff9db76cec40 100644 --- a/packages/react-components/react-datepicker-compat/package.json +++ b/packages/react-components/react-datepicker-compat/package.json @@ -35,6 +35,7 @@ }, "dependencies": { "@fluentui/keyboard-keys": "^9.0.2", + "@fluentui/react-field": "^9.1.0", "@fluentui/react-icons": "^2.0.196", "@fluentui/react-input": "^9.4.10", "@fluentui/react-jsx-runtime": "9.0.0-alpha.1", diff --git a/packages/react-components/react-datepicker-compat/src/components/DatePicker/DatePicker.types.ts b/packages/react-components/react-datepicker-compat/src/components/DatePicker/DatePicker.types.ts index d4aea2095fdf2..950ec49fa96b1 100644 --- a/packages/react-components/react-datepicker-compat/src/components/DatePicker/DatePicker.types.ts +++ b/packages/react-components/react-datepicker-compat/src/components/DatePicker/DatePicker.types.ts @@ -104,6 +104,11 @@ export type DatePickerProps = Omit>, 'de */ onOpenChange?: (open: boolean) => void; + /** + * Callback to run when the DatePicker encounters an error when validating the input + */ + onValidationError?: (data: DatePickerErrorData) => void; + /** * Whether the DatePicker should render the popup as inline or in a portal * @@ -224,32 +229,18 @@ export type DatePickerProps = Omit>, 'de showCloseButton?: boolean; }; +/** + * State used in rendering DatePicker. + */ export type DatePickerState = ComponentState & { disabled: boolean; inlinePopup: boolean; }; -// TODO: remove this once we add error handling hook -export interface DatePickerStrings extends CalendarStrings { - /** - * Error message to render for Input if isRequired validation fails. - */ - isRequiredErrorMessage?: string; - - /** - * Error message to render for Input if input date string parsing fails. - */ - invalidInputErrorMessage?: string; - - /** - * Error message to render for Input if date boundary (minDate, maxDate) validation fails. - */ - isOutOfBoundsErrorMessage?: string; - - /** - * Status message to render for Input the input date parsing fails, - * and the typed value is cleared and reset to the previous value. - * e.g. "Invalid entry `{0}`, date reset to `{1}`" - */ - isResetStatusMessage?: string; -} +/** + * Data passed to the `onValidationError` callback. + */ +export type DatePickerErrorData = { + /** The error found when validating the input. */ + error: 'invalid-input' | 'out-of-bounds' | 'required-input'; +}; diff --git a/packages/react-components/react-datepicker-compat/src/components/DatePicker/defaults.ts b/packages/react-components/react-datepicker-compat/src/components/DatePicker/defaults.ts index 73c0431657052..6906ad13d407c 100644 --- a/packages/react-components/react-datepicker-compat/src/components/DatePicker/defaults.ts +++ b/packages/react-components/react-datepicker-compat/src/components/DatePicker/defaults.ts @@ -1,15 +1,18 @@ import { defaultCalendarStrings } from '../Calendar/defaults'; -import type { DatePickerStrings } from './DatePicker.types'; +import type { CalendarStrings } from '../../utils/index'; +import type { DatePickerErrorData } from './DatePicker.types'; -// TODO: Once we have error handling hook, this needs to be either renamed or removed. -export const defaultDatePickerStrings: DatePickerStrings = { +export const defaultDatePickerStrings: CalendarStrings = { ...defaultCalendarStrings, prevMonthAriaLabel: 'Go to previous month', nextMonthAriaLabel: 'Go to next month', prevYearAriaLabel: 'Go to previous year', nextYearAriaLabel: 'Go to next year', closeButtonAriaLabel: 'Close date picker', - isRequiredErrorMessage: 'Field is required', - invalidInputErrorMessage: 'Invalid date format', - isResetStatusMessage: 'Invalid entry "{0}", date reset to "{1}"', +}; + +export const defaultDatePickerErrorStrings: Record = { + 'invalid-input': 'Invalid date format', + 'out-of-bounds': 'Date is out of bounds', + 'required-input': 'Field is required', }; diff --git a/packages/react-components/react-datepicker-compat/src/components/DatePicker/useDatePicker.tsx b/packages/react-components/react-datepicker-compat/src/components/DatePicker/useDatePicker.tsx index 3cfb1323308d2..09d3846e29fa0 100644 --- a/packages/react-components/react-datepicker-compat/src/components/DatePicker/useDatePicker.tsx +++ b/packages/react-components/react-datepicker-compat/src/components/DatePicker/useDatePicker.tsx @@ -1,8 +1,10 @@ import * as React from 'react'; import { ArrowDown, Enter, Escape } from '@fluentui/keyboard-keys'; +import { Calendar } from '../Calendar/Calendar'; import { CalendarMonthRegular } from '@fluentui/react-icons'; +import { compareDatePart, DayOfWeek, FirstWeekOfYear } from '../../utils'; +import { defaultDatePickerStrings } from './defaults'; import { Input } from '@fluentui/react-input'; -import { useFocusFinders, useModalAttributes } from '@fluentui/react-tabster'; import { mergeCallbacks, resolveShorthand, @@ -13,14 +15,13 @@ import { useOnClickOutside, useOnScrollOutside, } from '@fluentui/react-utilities'; -import { compareDatePart, DayOfWeek, FirstWeekOfYear } from '../../utils'; -import { Calendar } from '../Calendar/Calendar'; -import { usePopupPositioning } from '../../utils/usePopupPositioning'; +import { useFieldContext_unstable as useFieldContext } from '@fluentui/react-field'; import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts'; -import type { InputProps, InputOnChangeData } from '@fluentui/react-input'; +import { useFocusFinders, useModalAttributes } from '@fluentui/react-tabster'; +import { usePopupPositioning } from '../../utils/usePopupPositioning'; import type { CalendarProps, ICalendar } from '../Calendar/Calendar.types'; import type { DatePickerProps, DatePickerState } from './DatePicker.types'; -import { defaultCalendarStrings } from '../Calendar/defaults'; +import type { InputProps, InputOnChangeData } from '@fluentui/react-input'; function isDateOutOfBounds(date: Date, minDate?: Date, maxDate?: Date): boolean { return (!!minDate && compareDatePart(minDate!, date) > 0) || (!!maxDate && compareDatePart(maxDate!, date) < 0); @@ -128,12 +129,13 @@ export const useDatePicker_unstable = (props: DatePickerProps, ref: React.Ref { + const styles = useStyles(); + const [error, setError] = React.useState(undefined); + + return ( + + setError(data.error)} + className={styles.control} + /> + + ); +}; diff --git a/packages/react-components/react-datepicker-compat/stories/DatePicker/index.stories.tsx b/packages/react-components/react-datepicker-compat/stories/DatePicker/index.stories.tsx index 3215f6149101d..d19305e309b52 100644 --- a/packages/react-components/react-datepicker-compat/stories/DatePicker/index.stories.tsx +++ b/packages/react-components/react-datepicker-compat/stories/DatePicker/index.stories.tsx @@ -9,6 +9,7 @@ export { FirstDayOfTheWeek } from './DatePickerFirstDayOfTheWeek.stories'; export { WeekNumbers } from './DatePickerWeekNumbers.stories'; export { DateBoundaries } from './DatePickerDateBoundaries.stories'; export { CustomDateFormatting } from './DatePickerCustomDateFormatting.stories'; +export { ErrorHandling } from './DatePickerErrorHandling.stories'; export { Controlled } from './DatePickerControlled.stories'; export { Required } from './DatePickerRequired.stories'; export { Disabled } from './DatePickerDisabled.stories';