Skip to content

Commit

Permalink
hover state and separate hook for endAdornment
Browse files Browse the repository at this point in the history
  • Loading branch information
noraleonte committed May 29, 2023
1 parent a04e420 commit 557a597
Show file tree
Hide file tree
Showing 6 changed files with 131 additions and 41 deletions.
39 changes: 13 additions & 26 deletions packages/x-date-pickers/src/DateField/DateField.tsx
Original file line number Diff line number Diff line change
@@ -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 = <ClearIcon fontSize="small" />,
}) => {
return {
endAdornment: clearable ? (
<React.Fragment>
{InputProps?.endAdornment}
<IconButton className="deleteIcon" onClick={onClear} tabIndex={-1}>
{clearIcon}
</IconButton>
</React.Fragment>
) : (
InputProps?.endAdornment
),
};
};
import { useClearEndAdornment } from '../internals/hooks/useClearEndAdornment/useClearEndAdornment';

type DateFieldComponent = (<TDate>(
props: DateFieldProps<TDate> & React.RefAttributes<HTMLDivElement>,
Expand Down Expand Up @@ -71,12 +54,16 @@ const DateField = React.forwardRef(function DateField<TDate>(
inputRef: externalInputRef,
});

console.log(fieldProps);

const ProcessedInputProps = useClearEndAdornment({
const ProcessedInputProps = useClearEndAdornment<
typeof fieldProps.InputProps,
DateFieldSlotsComponent,
DateFieldSlotsComponentsProps<TDate>
>({
onClear,
clearable,
InputProps: fieldProps.InputProps,
slots,
slotProps,
});

return (
Expand Down
8 changes: 6 additions & 2 deletions packages/x-date-pickers/src/DateField/DateField.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<TDate, TChildProps extends {}> {
props: UseDateFieldComponentProps<TDate, TChildProps>;
Expand Down Expand Up @@ -45,7 +49,7 @@ export interface DateFieldProps<TDate>

export type DateFieldOwnerState<TDate> = DateFieldProps<TDate>;

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`.
Expand All @@ -54,6 +58,6 @@ export interface DateFieldSlotsComponent {
TextField?: React.ElementType;
}

export interface DateFieldSlotsComponentsProps<TDate> {
export interface DateFieldSlotsComponentsProps<TDate> extends FieldSlotsComponentsProps {
textField?: SlotComponentProps<typeof TextField, {}, DateFieldOwnerState<TDate>>;
}
Original file line number Diff line number Diff line change
@@ -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<HTMLButtonElement>;
} & SlotsAndSlotProps<TFieldSlotsComponents, TFieldSlotsComponentsProps>;

export const useClearEndAdornment = <
TInputProps extends { endAdornment?: React.ReactNode } | undefined,
TFieldSlotsComponents extends FieldSlotsComponents,
TFieldSlotsComponentsProps extends FieldSlotsComponentsProps,
>({
clearable,
InputProps: ForwardedInputProps,
onClear,
slots,
slotProps,
}: UseClearEndAdornmentProps<TInputProps, TFieldSlotsComponents, TFieldSlotsComponentsProps>) => {
const EndClearIcon = slots?.clearIcon ?? ClearIcon;
const endClearIconProps = useSlotProps({
elementType: ClearIcon,
externalSlotProps: slotProps?.clearIcon,
externalForwardedProps: {},
ownerState: {},
});

const InputProps = {
...ForwardedInputProps,
endAdornment: clearable ? (
<React.Fragment>
{ForwardedInputProps?.endAdornment}
<IconButton className="clearButton" onClick={onClear} tabIndex={-1}>
<EndClearIcon {...endClearIconProps} />
</IconButton>
</React.Fragment>
) : (
ForwardedInputProps?.endAdornment
),
};

return InputProps;
};
43 changes: 31 additions & 12 deletions packages/x-date-pickers/src/internals/hooks/useField/useField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export const useField = <
setTempAndroidValueStr,
sectionsValueBoundaries,
placeholder,
setIsHovered,
} = useFieldState(params);

const {
Expand All @@ -56,6 +57,8 @@ export const useField = <
error,
clearable,
onClear,
onMouseEnter,
onMouseLeave,
...otherForwardedProps
},
fieldValueManager,
Expand Down Expand Up @@ -143,13 +146,17 @@ export const useField = <
});

const handleInputBlur = useEventCallback((event: React.FocusEvent<HTMLInputElement>, ...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<HTMLInputElement>) => {
Expand Down Expand Up @@ -459,6 +466,7 @@ export const useField = <
valueManager.emptyValue,
);
const shouldShowPlaceholder = !inputHasFocus && areAllSectionsEmpty;
const isInputHovered = state.isHovered;

React.useImperativeHandle(unstableFieldRef, () => ({
getSections: () => state.sections,
Expand All @@ -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,
Expand All @@ -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,
};
};
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -136,8 +138,10 @@ export interface UseFieldForwardedProps {
onFocus?: () => void;
onBlur?: React.FocusEventHandler<HTMLInputElement>;
error?: boolean;
onClear?: () => void;
onClear?: React.MouseEventHandler;
clearable?: boolean;
onMouseEnter?: React.MouseEventHandler;
onMouseLeave?: React.MouseEventHandler;
}

export type UseFieldResponse<TForwardedProps extends UseFieldForwardedProps> = Omit<
Expand Down Expand Up @@ -319,6 +323,7 @@ export interface UseFieldState<TValue, TSection extends FieldSection> {
* 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<
Expand Down Expand Up @@ -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<typeof ClearIcon, {}, {}>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ export const useFieldState = <
valueManager.getTodayValue(utils, valueType),
),
tempValueStrAndroid: null,
isHovered: false,
};
});

Expand All @@ -133,6 +134,13 @@ export const useFieldState = <
}));
};

const setIsHovered = (isHovered: boolean) => {
setState((prevState) => ({
...prevState,
isHovered,
}));
};

const selectedSectionIndexes = React.useMemo<FieldSelectedSectionsIndexes | null>(() => {
if (selectedSections == null) {
return null;
Expand Down Expand Up @@ -412,5 +420,6 @@ export const useFieldState = <
setTempAndroidValueStr,
sectionsValueBoundaries,
placeholder,
setIsHovered,
};
};

0 comments on commit 557a597

Please sign in to comment.