diff --git a/packages/x-date-pickers/src/DateField/DateField.tsx b/packages/x-date-pickers/src/DateField/DateField.tsx index 3b265ea6392ee..c16b3e3fadfb3 100644 --- a/packages/x-date-pickers/src/DateField/DateField.tsx +++ b/packages/x-date-pickers/src/DateField/DateField.tsx @@ -1,32 +1,15 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import MuiTextField from '@mui/material/TextField'; -import IconButton from '@mui/material/IconButton'; import { useThemeProps } from '@mui/material/styles'; -import ClearIcon from '@mui/icons-material/Clear'; import { useSlotProps } from '@mui/base/utils'; -import { DateFieldProps } from './DateField.types'; +import { + DateFieldProps, + DateFieldSlotsComponent, + DateFieldSlotsComponentsProps, +} from './DateField.types'; import { useDateField } from './useDateField'; - -const useClearEndAdornment = ({ - clearable, - InputProps, - onClear, - clearIcon = , -}) => { - return { - endAdornment: clearable ? ( - - {InputProps?.endAdornment} - - {clearIcon} - - - ) : ( - InputProps?.endAdornment - ), - }; -}; +import { useClearEndAdornment } from '../internals/hooks/useClearEndAdornment/useClearEndAdornment'; type DateFieldComponent = (( props: DateFieldProps & React.RefAttributes, @@ -71,12 +54,16 @@ const DateField = React.forwardRef(function DateField( inputRef: externalInputRef, }); - console.log(fieldProps); - - const ProcessedInputProps = useClearEndAdornment({ + const ProcessedInputProps = useClearEndAdornment< + typeof fieldProps.InputProps, + DateFieldSlotsComponent, + DateFieldSlotsComponentsProps + >({ onClear, clearable, InputProps: fieldProps.InputProps, + slots, + slotProps, }); return ( diff --git a/packages/x-date-pickers/src/DateField/DateField.types.ts b/packages/x-date-pickers/src/DateField/DateField.types.ts index acf3abdea8a18..a1436c845b942 100644 --- a/packages/x-date-pickers/src/DateField/DateField.types.ts +++ b/packages/x-date-pickers/src/DateField/DateField.types.ts @@ -12,6 +12,10 @@ import { } from '../internals/models/validation'; import { FieldsTextFieldProps } from '../internals/models/fields'; import { SlotsAndSlotProps } from '../internals/utils/slots-migration'; +import { + FieldSlotsComponents, + FieldSlotsComponentsProps, +} from '../internals/hooks/useField/useField.types'; export interface UseDateFieldParams { props: UseDateFieldComponentProps; @@ -45,7 +49,7 @@ export interface DateFieldProps export type DateFieldOwnerState = DateFieldProps; -export interface DateFieldSlotsComponent { +export interface DateFieldSlotsComponent extends FieldSlotsComponents { /** * Form control with an input to render the value. * Receives the same props as `@mui/material/TextField`. @@ -54,6 +58,6 @@ export interface DateFieldSlotsComponent { TextField?: React.ElementType; } -export interface DateFieldSlotsComponentsProps { +export interface DateFieldSlotsComponentsProps extends FieldSlotsComponentsProps { textField?: SlotComponentProps>; } diff --git a/packages/x-date-pickers/src/internals/hooks/useClearEndAdornment/useClearEndAdornment.tsx b/packages/x-date-pickers/src/internals/hooks/useClearEndAdornment/useClearEndAdornment.tsx new file mode 100644 index 0000000000000..518c0e3659748 --- /dev/null +++ b/packages/x-date-pickers/src/internals/hooks/useClearEndAdornment/useClearEndAdornment.tsx @@ -0,0 +1,53 @@ +import * as React from 'react'; + +import { useSlotProps } from '@mui/base'; +import ClearIcon from '@mui/icons-material/Clear'; +import IconButton from '@mui/material/IconButton'; +import { SlotsAndSlotProps } from '../../utils/slots-migration'; +import { FieldSlotsComponents, FieldSlotsComponentsProps } from '../useField/useField.types'; + +type UseClearEndAdornmentProps< + TInputProps extends { endAdornment?: React.ReactNode } | undefined, + TFieldSlotsComponents extends FieldSlotsComponents, + TFieldSlotsComponentsProps extends FieldSlotsComponentsProps, +> = { + clearable: boolean; + InputProps: TInputProps; + onClear: React.MouseEventHandler; +} & SlotsAndSlotProps; + +export const useClearEndAdornment = < + TInputProps extends { endAdornment?: React.ReactNode } | undefined, + TFieldSlotsComponents extends FieldSlotsComponents, + TFieldSlotsComponentsProps extends FieldSlotsComponentsProps, +>({ + clearable, + InputProps: ForwardedInputProps, + onClear, + slots, + slotProps, +}: UseClearEndAdornmentProps) => { + const EndClearIcon = slots?.clearIcon ?? ClearIcon; + const endClearIconProps = useSlotProps({ + elementType: ClearIcon, + externalSlotProps: slotProps?.clearIcon, + externalForwardedProps: {}, + ownerState: {}, + }); + + const InputProps = { + ...ForwardedInputProps, + endAdornment: clearable ? ( + + {ForwardedInputProps?.endAdornment} + + + + + ) : ( + ForwardedInputProps?.endAdornment + ), + }; + + return InputProps; +}; diff --git a/packages/x-date-pickers/src/internals/hooks/useField/useField.ts b/packages/x-date-pickers/src/internals/hooks/useField/useField.ts index 3135012a194f9..8c685094e87bf 100644 --- a/packages/x-date-pickers/src/internals/hooks/useField/useField.ts +++ b/packages/x-date-pickers/src/internals/hooks/useField/useField.ts @@ -40,6 +40,7 @@ export const useField = < setTempAndroidValueStr, sectionsValueBoundaries, placeholder, + setIsHovered, } = useFieldState(params); const { @@ -56,6 +57,8 @@ export const useField = < error, clearable, onClear, + onMouseEnter, + onMouseLeave, ...otherForwardedProps }, fieldValueManager, @@ -143,13 +146,17 @@ export const useField = < }); const handleInputBlur = useEventCallback((event: React.FocusEvent, ...args) => { - const isClearButton = Boolean( - event.relatedTarget?.className?.split(' ')?.includes('deleteIcon'), - ); - // if (!isClearButton) { - onBlur?.(event, ...(args as [])); - setSelectedSections(null); - // } + const { relatedTarget } = event; + + const shouldBlur = + !relatedTarget && + relatedTarget !== inputRef.current && + !inputRef.current.contains(relatedTarget); + + if (shouldBlur) { + onBlur?.(event, ...(args as [])); + setSelectedSections(null); + } }); const handleInputPaste = useEventCallback((event: React.ClipboardEvent) => { @@ -459,6 +466,7 @@ export const useField = < valueManager.emptyValue, ); const shouldShowPlaceholder = !inputHasFocus && areAllSectionsEmpty; + const isInputHovered = state.isHovered; React.useImperativeHandle(unstableFieldRef, () => ({ getSections: () => state.sections, @@ -480,16 +488,25 @@ export const useField = < setSelectedSections: (activeSectionIndex) => setSelectedSections(activeSectionIndex), })); - const handleClearValue = (event, ...args) => { + const handleClearValue = useEventCallback((event: React.MouseEvent, ...args) => { + // the click event of the endAdornmnet propagates to the input and triggers the `handleInputClick` handler. event.stopPropagation(); event.preventDefault(); - onClear?.(...(args as [])); + onClear?.(event, ...(args as [])); clearValue(); setSelectedSections(0); inputRef?.current?.focus(); - }; + }); + + const handleMouseEnter = useEventCallback((event: React.MouseEvent, ...args) => { + onMouseEnter?.(event, ...(args as [])); + setIsHovered(true); + }); - console.log(selectedSectionIndexes); + const handleMouseLeave = useEventCallback((event: React.MouseEvent, ...args) => { + onMouseLeave?.(event, ...(args as [])); + setIsHovered(false); + }); return { placeholder, @@ -508,6 +525,8 @@ export const useField = < onClear: handleClearValue, error: inputError, ref: handleRef, - clearable: Boolean(clearable && inputHasFocus && !areAllSectionsEmpty), + clearable: Boolean(clearable && !areAllSectionsEmpty && (inputHasFocus || isInputHovered)), + onMouseEnter: handleMouseEnter, + onMouseLeave: handleMouseLeave, }; }; diff --git a/packages/x-date-pickers/src/internals/hooks/useField/useField.types.ts b/packages/x-date-pickers/src/internals/hooks/useField/useField.types.ts index e0a2b4143073b..b5ffe29586058 100644 --- a/packages/x-date-pickers/src/internals/hooks/useField/useField.types.ts +++ b/packages/x-date-pickers/src/internals/hooks/useField/useField.types.ts @@ -1,4 +1,6 @@ import * as React from 'react'; +import { SlotComponentProps } from '@mui/base/utils'; +import ClearIcon from '@mui/icons-material/Clear'; import { FieldSectionType, FieldSection, @@ -136,8 +138,10 @@ export interface UseFieldForwardedProps { onFocus?: () => void; onBlur?: React.FocusEventHandler; error?: boolean; - onClear?: () => void; + onClear?: React.MouseEventHandler; clearable?: boolean; + onMouseEnter?: React.MouseEventHandler; + onMouseLeave?: React.MouseEventHandler; } export type UseFieldResponse = Omit< @@ -319,6 +323,7 @@ export interface UseFieldState { * The property below allows us to set the first `onChange` value into state waiting for the second one. */ tempValueStrAndroid: string | null; + isHovered: boolean; } export type UseFieldValidationProps< @@ -361,3 +366,16 @@ export type SectionOrdering = { */ endIndex: number; }; + +export interface FieldSlotsComponents { + /** + * Icon to display inside the clear button. + * Receives the same props as `@mui/icons-material/Clear`. + * @default ClearIcon from '@mui/icons-material/Clear' + */ + ClearIcon?: React.ElementType; +} + +export interface FieldSlotsComponentsProps { + clearIcon?: SlotComponentProps; +} diff --git a/packages/x-date-pickers/src/internals/hooks/useField/useFieldState.ts b/packages/x-date-pickers/src/internals/hooks/useField/useFieldState.ts index 14343b895869b..c7b80c04dafda 100644 --- a/packages/x-date-pickers/src/internals/hooks/useField/useFieldState.ts +++ b/packages/x-date-pickers/src/internals/hooks/useField/useFieldState.ts @@ -113,6 +113,7 @@ export const useFieldState = < valueManager.getTodayValue(utils, valueType), ), tempValueStrAndroid: null, + isHovered: false, }; }); @@ -133,6 +134,13 @@ export const useFieldState = < })); }; + const setIsHovered = (isHovered: boolean) => { + setState((prevState) => ({ + ...prevState, + isHovered, + })); + }; + const selectedSectionIndexes = React.useMemo(() => { if (selectedSections == null) { return null; @@ -412,5 +420,6 @@ export const useFieldState = < setTempAndroidValueStr, sectionsValueBoundaries, placeholder, + setIsHovered, }; };