Skip to content

Commit

Permalink
first iteration of clearable behavior on DateField
Browse files Browse the repository at this point in the history
 hook and event propagation

hover state and separate hook for endAdornment
  • Loading branch information
noraleonte committed May 29, 2023
1 parent f9041a8 commit 64020ec
Show file tree
Hide file tree
Showing 6 changed files with 154 additions and 34 deletions.
34 changes: 20 additions & 14 deletions packages/x-date-pickers/src/DateField/DateField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ import * as React from 'react';
import PropTypes from 'prop-types';
import MuiTextField from '@mui/material/TextField';
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';
import { IconButton } from '@mui/material';
import { useClearEndAdornment } from '../internals/hooks/useClearEndAdornment/useClearEndAdornment';

type DateFieldComponent = (<TDate>(
props: DateFieldProps<TDate> & React.RefAttributes<HTMLDivElement>,
Expand Down Expand Up @@ -44,29 +47,32 @@ const DateField = React.forwardRef(function DateField<TDate>(
inputMode,
readOnly,
clearable,
onClear,
...fieldProps
} = useDateField<TDate, typeof textFieldProps>({
props: textFieldProps,
inputRef: externalInputRef,
});

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

return (
<TextField
ref={ref}
{...fieldProps}
InputProps={{
...fieldProps.InputProps,
...ProcessedInputProps,
readOnly,
endAdornment: clearable && (
<IconButton
className="deleteIcon"
onClick={() => {
console.log('wtf');
}}
>
<ClearIcon />
</IconButton>
),
}}
inputProps={{ ...fieldProps.inputProps, inputMode, onPaste, ref: inputRef }}
/>
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;
};
61 changes: 45 additions & 16 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 @@ -54,7 +55,10 @@ export const useField = <
onMouseUp,
onPaste,
error,
clearable: forwardedClearable,
clearable,
onClear,
onMouseEnter,
onMouseLeave,
...otherForwardedProps
},
fieldValueManager,
Expand All @@ -74,8 +78,6 @@ export const useField = <
const theme = useTheme();
const isRTL = theme.direction === 'rtl';

let clearable = forwardedClearable;

const sectionOrder = React.useMemo(
() => getSectionOrder(state.sections, isRTL),
[state.sections, isRTL],
Expand All @@ -100,7 +102,6 @@ export const useField = <
);
}
const sectionIndex = nextSectionIndex === -1 ? state.sections.length - 1 : nextSectionIndex - 1;

setSelectedSections(sectionIndex);
};

Expand Down Expand Up @@ -144,9 +145,18 @@ export const useField = <
});
});

const handleInputBlur = useEventCallback((...args) => {
onBlur?.(...(args as []));
setSelectedSections(null);
const handleInputBlur = useEventCallback((event: React.FocusEvent<HTMLInputElement>, ...args) => {
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 @@ -450,8 +460,13 @@ export const useField = <
}, [selectedSectionIndexes, state.sections]);

const inputHasFocus = inputRef.current && inputRef.current === getActiveElement(document);
const areSectionsEmpty = valueManager.areValuesEqual(utils, state.value, valueManager.emptyValue);
const shouldShowPlaceholder = !inputHasFocus && areSectionsEmpty;
const areAllSectionsEmpty = valueManager.areValuesEqual(
utils,
state.value,
valueManager.emptyValue,
);
const shouldShowPlaceholder = !inputHasFocus && areAllSectionsEmpty;
const isInputHovered = state.isHovered;

React.useImperativeHandle(unstableFieldRef, () => ({
getSections: () => state.sections,
Expand All @@ -473,13 +488,25 @@ export const useField = <
setSelectedSections: (activeSectionIndex) => setSelectedSections(activeSectionIndex),
}));

const handleClearValue = React.useCallback(() => {
console.log('hei there clearing');
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?.(event, ...(args as []));
clearValue();
setSelectedSections(0);
inputRef?.current?.focus();
});

React.useEffect(() => {
console.log(areSectionsEmpty, state.value);
}, [areSectionsEmpty]);
const handleMouseEnter = useEventCallback((event: React.MouseEvent, ...args) => {
onMouseEnter?.(event, ...(args as []));
setIsHovered(true);
});

const handleMouseLeave = useEventCallback((event: React.MouseEvent, ...args) => {
onMouseLeave?.(event, ...(args as []));
setIsHovered(false);
});

return {
placeholder,
Expand All @@ -495,9 +522,11 @@ export const useField = <
onChange: handleInputChange,
onKeyDown: handleInputKeyDown,
onMouseUp: handleInputMouseUp,
onClear: handleClearValue,
error: inputError,
ref: handleRef,
handleClearValue,
clearable: Boolean(forwardedClearable && inputHasFocus && !areSectionsEmpty),
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 @@ -134,9 +136,12 @@ export interface UseFieldForwardedProps {
onPaste?: React.ClipboardEventHandler<HTMLInputElement>;
onClick?: () => void;
onFocus?: () => void;
onBlur?: () => void;
onBlur?: React.FocusEventHandler<HTMLInputElement>;
error?: boolean;
clearable: boolean;
onClear?: React.MouseEventHandler;
clearable?: boolean;
onMouseEnter?: React.MouseEventHandler;
onMouseLeave?: React.MouseEventHandler;
}

export type UseFieldResponse<TForwardedProps extends UseFieldForwardedProps> = Omit<
Expand Down Expand Up @@ -318,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 @@ -360,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 64020ec

Please sign in to comment.