diff --git a/packages/x-date-pickers/src/internals/components/FakeTextField/FakeTextField.tsx b/packages/x-date-pickers/src/internals/components/FakeTextField/FakeTextField.tsx deleted file mode 100644 index 8cf10f6f25fdf..0000000000000 --- a/packages/x-date-pickers/src/internals/components/FakeTextField/FakeTextField.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import * as React from 'react'; -import Box from '@mui/material/Box'; - -export interface FakeTextFieldElement { - container: React.HTMLAttributes; - content: React.HTMLAttributes; - before: React.HTMLAttributes; - after: React.HTMLAttributes; -} - -interface FakeTextFieldProps { - elements: FakeTextFieldElement[]; - valueStr: string; - onValueStrChange: React.ChangeEventHandler; - error: boolean; - id?: string; - InputProps: any; - inputProps: any; - disabled?: boolean; - autoFocus?: boolean; - ownerState?: any; - valueType: 'value' | 'placeholder'; -} - -export const FakeTextField = React.forwardRef(function FakeTextField( - props: FakeTextFieldProps, - ref: React.Ref, -) { - const { - elements, - valueStr, - onValueStrChange, - id, - error, - InputProps, - inputProps, - autoFocus, - disabled, - valueType, - ownerState, - ...other - } = props; - - return ( - - - {elements.map(({ container, content, before, after }, elementIndex) => ( - - - - - - ))} - - - - ); -}); diff --git a/packages/x-date-pickers/src/internals/components/FakeTextField/index.ts b/packages/x-date-pickers/src/internals/components/FakeTextField/index.ts deleted file mode 100644 index 17b2be0c0a9e6..0000000000000 --- a/packages/x-date-pickers/src/internals/components/FakeTextField/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { FakeTextField } from './FakeTextField'; diff --git a/packages/x-date-pickers/src/internals/components/PickersTextField/Outline.tsx b/packages/x-date-pickers/src/internals/components/PickersTextField/Outline.tsx new file mode 100644 index 0000000000000..10a90c1fe05f4 --- /dev/null +++ b/packages/x-date-pickers/src/internals/components/PickersTextField/Outline.tsx @@ -0,0 +1,84 @@ +import * as React from 'react'; +import { styled } from '@mui/material/styles'; + +const OutlineRoot = styled('fieldset')({ + textAlign: 'left', + position: 'absolute', + bottom: 0, + right: 0, + top: -5, + left: 0, + margin: 0, + padding: '0 8px', + pointerEvents: 'none', + borderRadius: 'inherit', + borderStyle: 'solid', + borderWidth: 1, + overflow: 'hidden', + minWidth: '0%', +}); + +const OutlineLegend = styled('legend')<{ ownerState: any }>(({ ownerState, theme }) => ({ + float: 'unset', // Fix conflict with bootstrap + width: 'auto', // Fix conflict with bootstrap + overflow: 'hidden', // Fix Horizontal scroll when label too long + ...(!ownerState.withLabel && { + padding: 0, + lineHeight: '11px', // sync with `height` in `legend` styles + transition: theme.transitions.create('width', { + duration: 150, + easing: theme.transitions.easing.easeOut, + }), + }), + ...(ownerState.withLabel && { + display: 'block', // Fix conflict with normalize.css and sanitize.css + padding: 0, + height: 11, // sync with `lineHeight` in `legend` styles + fontSize: '0.75em', + visibility: 'hidden', + maxWidth: 0.01, + transition: theme.transitions.create('max-width', { + duration: 50, + easing: theme.transitions.easing.easeOut, + }), + whiteSpace: 'nowrap', + '& > span': { + paddingLeft: 5, + paddingRight: 5, + display: 'inline-block', + opacity: 0, + visibility: 'visible', + }, + ...(ownerState.notched && { + maxWidth: '100%', + transition: theme.transitions.create('max-width', { + duration: 100, + easing: theme.transitions.easing.easeOut, + delay: 50, + }), + }), + }), +})); + +export default function Outline(props) { + const { children, classes, className, label, notched, ...other } = props; + const withLabel = label != null && label !== ''; + const ownerState = { + ...props, + notched, + withLabel, + }; + return ( + + + {/* Use the nominal use case of the legend, avoid rendering artefacts. */} + {withLabel ? ( + {label} + ) : ( + // notranslate needed while Google Translate will not fix zero-width space issue + + )} + + + ); +} diff --git a/packages/x-date-pickers/src/internals/components/PickersTextField/PickersInput.tsx b/packages/x-date-pickers/src/internals/components/PickersTextField/PickersInput.tsx new file mode 100644 index 0000000000000..eec6c98022a37 --- /dev/null +++ b/packages/x-date-pickers/src/internals/components/PickersTextField/PickersInput.tsx @@ -0,0 +1,337 @@ +import * as React from 'react'; +import clsx from 'clsx'; +import Box from '@mui/material/Box'; +import { useFormControl } from '@mui/material/FormControl'; +import { styled } from '@mui/material/styles'; +import useForkRef from '@mui/utils/useForkRef'; +import { + unstable_composeClasses as composeClasses, + unstable_capitalize as capitalize, + visuallyHidden, +} from '@mui/utils'; +import { pickersInputClasses, getPickersInputUtilityClass } from './pickersTextFieldClasses'; +import Outline from './Outline'; +import { PickersInputElement, PickersInputProps } from './PickersInput.types'; + +const SectionsWrapper = styled(Box, { + name: 'MuiPickersInput', + slot: 'Root', + overridesResolver: (props, styles) => styles.root, +})<{ ownerState: OwnerStateType }>(({ theme, ownerState }) => { + const borderColor = + theme.palette.mode === 'light' ? 'rgba(0, 0, 0, 0.23)' : 'rgba(255, 255, 255, 0.23)'; + return { + cursor: 'text', + padding: '16.5px 14px', + display: 'flex', + justifyContent: 'flex-start', + alignItems: 'center', + width: ownerState.fullWidth ? '100%' : '25ch', + position: 'relative', + borderRadius: (theme.vars || theme).shape.borderRadius, + [`&:hover .${pickersInputClasses.notchedOutline}`]: { + borderColor: (theme.vars || theme).palette.text.primary, + }, + + // Reset on touch devices, it doesn't add specificity + '@media (hover: none)': { + [`&:hover .${pickersInputClasses.notchedOutline}`]: { + borderColor: theme.vars + ? `rgba(${theme.vars.palette.common.onBackgroundChannel} / 0.23)` + : borderColor, + }, + }, + [`&.${pickersInputClasses.focused} .${pickersInputClasses.notchedOutline}`]: { + borderStyle: 'solid', + borderColor: (theme.vars || theme).palette[ownerState.color].main, + borderWidth: 2, + }, + [`&.${pickersInputClasses.disabled}`]: { + [`& .${pickersInputClasses.notchedOutline}`]: { + borderColor: (theme.vars || theme).palette.action.disabled, + }, + + '*': { + color: (theme.vars || theme).palette.action.disabled, + }, + }, + + [`&.${pickersInputClasses.error} .${pickersInputClasses.notchedOutline}`]: { + borderColor: (theme.vars || theme).palette.error.main, + }, + + ...(ownerState.size === 'small' && { + padding: '8.5px 14px', + }), + }; +}); + +const SectionsContainer = styled('div', { + name: 'MuiPickersInput', + slot: 'Input', + overridesResolver: (props, styles) => styles.input, +})<{ ownerState: OwnerStateType }>(({ theme, ownerState }) => ({ + fontFamily: theme.typography.fontFamily, + fontSize: 'inherit', + lineHeight: '1.4375em', // 23px + flexGrow: 1, + visibility: ownerState.adornedStart || ownerState.focused ? 'visible' : 'hidden', +})); + +const SectionContainer = styled('span', { + name: 'MuiPickersInput', + slot: 'Section', + overridesResolver: (props, styles) => styles.section, +})(({ theme }) => ({ + fontFamily: theme.typography.fontFamily, + fontSize: 'inherit', + lineHeight: '1.4375em', // 23px + flexGrow: 1, +})); + +const SectionInput = styled('span', { + name: 'MuiPickersInput', + slot: 'Content', + overridesResolver: (props, styles) => styles.content, +})(({ theme }) => ({ + fontFamily: theme.typography.fontFamily, + lineHeight: '1.4375em', // 23px + letterSpacing: 'inherit', + width: 'fit-content', +})); + +const SectionSeparator = styled('span', { + name: 'MuiPickersInput', + slot: 'Separator', + overridesResolver: (props, styles) => styles.separator, +})(() => ({ + whiteSpace: 'pre', +})); + +const FakeHiddenInput = styled('input', { + name: 'MuiPickersInput', + slot: 'HiddenInput', + overridesResolver: (props, styles) => styles.hiddenInput, +})({ + ...visuallyHidden, +}); + +const NotchedOutlineRoot = styled(Outline, { + name: 'MuiPickersInput', + slot: 'NotchedOutline', + overridesResolver: (props, styles) => styles.notchedOutline, +})(({ theme }) => { + const borderColor = + theme.palette.mode === 'light' ? 'rgba(0, 0, 0, 0.23)' : 'rgba(255, 255, 255, 0.23)'; + return { + borderColor: theme.vars + ? `rgba(${theme.vars.palette.common.onBackgroundChannel} / 0.23)` + : borderColor, + }; +}); + +function InputContent({ + elements, + contentEditable, + ownerState, +}: { + elements: PickersInputElement[]; + contentEditable?: string | boolean; + ownerState: OwnerStateType; +}) { + if (contentEditable) { + return elements + .map(({ content, before, after }) => `${before.children}${content.children}${after.children}`) + .join(''); + } + + return ( + + {elements.map(({ container, content, before, after }, elementIndex) => ( + + + + + + ))} + + ); +} + +const useUtilityClasses = (ownerState: OwnerStateType) => { + const { + focused, + disabled, + error, + classes, + fullWidth, + color, + size, + endAdornment, + startAdornment, + } = ownerState; + + const slots = { + root: [ + 'root', + focused && !disabled && 'focused', + disabled && 'disabled', + error && 'error', + fullWidth && 'fullWidth', + `color${capitalize(color)}`, + size === 'small' && 'inputSizeSmall', + Boolean(startAdornment) && 'adornedStart', + Boolean(endAdornment) && 'adornedEnd', + ], + notchedOutline: ['notchedOutline'], + before: ['before'], + after: ['after'], + content: ['content'], + input: ['input'], + }; + + return composeClasses(slots, getPickersInputUtilityClass, classes); +}; + +// TODO: move to utils +// Separates the state props from the form control +function formControlState({ props, states, muiFormControl }) { + return states.reduce((acc, state) => { + acc[state] = props[state]; + + if (muiFormControl) { + if (typeof props[state] === 'undefined') { + acc[state] = muiFormControl[state]; + } + } + + return acc; + }, {}); +} + +interface OwnerStateType extends PickersInputProps { + color: 'primary' | 'secondary' | 'error' | 'info' | 'success' | 'warning'; + disabled?: boolean; + error?: boolean; + fullWidth?: boolean; + variant?: 'filled' | 'outlined' | 'standard'; + size?: 'small' | 'medium'; + adornedStart?: boolean; +} + +const PickersInput = React.forwardRef(function PickersInput( + props: PickersInputProps, + ref: React.Ref, +) { + const { + elements, + defaultValue, + label, + onFocus, + onWrapperClick, + onBlur, + valueStr, + onValueStrChange, + id, + InputProps, + inputProps, + autoFocus, + ownerState: ownerStateProp, + endAdornment, + startAdornment, + ...other + } = props; + + const inputRef = React.useRef(null); + const handleInputRef = useForkRef(ref, inputRef); + + const muiFormControl = useFormControl(); + const fcs = formControlState({ + props, + muiFormControl, + states: [ + 'color', + 'disabled', + 'error', + 'focused', + 'size', + 'required', + 'fullWidth', + 'adornedStart', + ], + }); + + React.useEffect(() => { + if (muiFormControl) { + muiFormControl.setAdornedStart(Boolean(startAdornment)); + } + }, [muiFormControl, startAdornment]); + + const ownerState = { + ...props, + ...ownerStateProp, + color: fcs.color || 'primary', + disabled: fcs.disabled, + error: fcs.error, + focused: fcs.focused, + fullWidth: fcs.fullWidth, + size: fcs.size, + }; + const classes = useUtilityClasses(ownerState); + + return ( + + {startAdornment} + + + + {endAdornment} + + {label} +  {'*'} + + ) : ( + label + ) + } + ownerState={ownerState} + /> + + ); +}); + +export default PickersInput; diff --git a/packages/x-date-pickers/src/internals/components/PickersTextField/PickersInput.types.ts b/packages/x-date-pickers/src/internals/components/PickersTextField/PickersInput.types.ts new file mode 100644 index 0000000000000..2492b9a86d1b9 --- /dev/null +++ b/packages/x-date-pickers/src/internals/components/PickersTextField/PickersInput.types.ts @@ -0,0 +1,31 @@ +import { OutlinedInputProps } from '@mui/material/OutlinedInput'; +import { FieldsTextFieldProps } from '../../models'; + +export interface PickersInputElement { + container: React.HTMLAttributes; + content: React.HTMLAttributes; + before: React.HTMLAttributes; + after: React.HTMLAttributes; +} + +export interface PickersInputProps extends FieldsTextFieldProps { + elements: PickersInputElement[]; + areAllSectionsEmpty?: boolean; + valueStr: string; + onValueStrChange: React.ChangeEventHandler; + id?: string; + InputProps?: Partial; + inputProps: any; + autoFocus?: boolean; + ownerState?: any; + onWrapperClick: () => void; + defaultValue: string; + label?: string; + endAdornment?: React.ReactNode; + startAdornment?: React.ReactNode; + onBlur?: React.FocusEventHandler; + onChange?: React.FormEventHandler; + onFocus?: React.FocusEventHandler; + onKeyDown?: React.KeyboardEventHandler; + onKeyUp?: React.KeyboardEventHandler; +} diff --git a/packages/x-date-pickers/src/internals/components/PickersTextField/PickersTextField.tsx b/packages/x-date-pickers/src/internals/components/PickersTextField/PickersTextField.tsx new file mode 100644 index 0000000000000..7b1d9cd34b9d5 --- /dev/null +++ b/packages/x-date-pickers/src/internals/components/PickersTextField/PickersTextField.tsx @@ -0,0 +1,133 @@ +import * as React from 'react'; +import clsx from 'clsx'; +import { styled } from '@mui/material/styles'; +import useForkRef from '@mui/utils/useForkRef'; +import { unstable_composeClasses as composeClasses, unstable_useId as useId } from '@mui/utils'; +import InputLabel from '@mui/material/InputLabel'; +import FormHelperText from '@mui/material/FormHelperText'; +import FormControl from '@mui/material/FormControl'; +import { getPickersTextFieldUtilityClass } from './pickersTextFieldClasses'; +import PickersInput from './PickersInput'; +import { PickersTextFieldProps } from './PickersTextField.types'; + +const PickersTextFieldRoot = styled(FormControl, { + name: 'MuiPickersTextField', + slot: 'Root', + overridesResolver: (props, styles) => styles.root, +})<{ ownerState: OwnerStateType }>({}); + +const useUtilityClasses = (ownerState: PickersTextFieldProps) => { + const { focused, disabled, classes, required } = ownerState; + + const slots = { + root: [ + 'root', + focused && !disabled && 'focused', + disabled && 'disabled', + required && 'required', + ], + }; + + return composeClasses(slots, getPickersTextFieldUtilityClass, classes); +}; + +type OwnerStateType = Partial; + +export const PickersTextField = React.forwardRef(function PickersTextField( + props: PickersTextFieldProps, + ref: React.Ref, +) { + const { + elements, + className, + color = 'primary', + disabled = false, + error = false, + label, + variant = 'outlined', + fullWidth = false, + valueStr, + helperText, + valueType, + id: idOverride, + FormHelperTextProps, + InputLabelProps, + inputProps, + InputProps, + required = false, + focused: focusedProp, + ownerState: ownerStateProp, + ...other + } = props; + + const [focused, setFocused] = React.useState(focusedProp); + + const rootRef = React.useRef(null); + const handleRootRef = useForkRef(ref, rootRef); + + const inputRef = React.useRef(null); + const handleInputRef = useForkRef(inputRef, InputProps?.ref); + + const id = useId(idOverride); + const helperTextId = helperText && id ? `${id}-helper-text` : undefined; + const inputLabelId = label && id ? `${id}-label` : undefined; + + const ownerState = { + ...props, + color, + disabled, + error, + focused, + variant, + }; + + const classes = useUtilityClasses(ownerState); + + // TODO: delete after behavior implementation + const onWrapperClick = () => { + if (!focused) { + setFocused(true); + const container = rootRef.current; + + // Find the first input element within the container + const firstInput = container?.querySelector('.content'); + + // Check if the input element exists before focusing it + if (firstInput) { + firstInput.focus(); + } + + inputRef.current?.focus(); + } + }; + + return ( + + + {label} + + + {helperText && ( + + {helperText} + + )} + + ); +}); diff --git a/packages/x-date-pickers/src/internals/components/PickersTextField/PickersTextField.types.ts b/packages/x-date-pickers/src/internals/components/PickersTextField/PickersTextField.types.ts new file mode 100644 index 0000000000000..ddbf6e4673fd9 --- /dev/null +++ b/packages/x-date-pickers/src/internals/components/PickersTextField/PickersTextField.types.ts @@ -0,0 +1,18 @@ +import { TextFieldClasses } from '@mui/material/TextField'; +import { PickersInputProps } from './PickersInput.types'; + +export interface PickersTextFieldProps extends PickersInputProps { + classes?: Partial; + className?: string; + color?: 'primary' | 'secondary' | 'error' | 'info' | 'success' | 'warning'; + disabled?: boolean; + error?: boolean; + fullWidth?: boolean; + helperText?: React.ReactNode; + size?: 'small' | 'medium'; + variant?: 'filled' | 'outlined' | 'standard'; + valueStr: string; + InputProps: any; + valueType: 'value' | 'placeholder'; + required?: boolean; +} diff --git a/packages/x-date-pickers/src/internals/components/PickersTextField/index.ts b/packages/x-date-pickers/src/internals/components/PickersTextField/index.ts new file mode 100644 index 0000000000000..ccce70e8f8ef8 --- /dev/null +++ b/packages/x-date-pickers/src/internals/components/PickersTextField/index.ts @@ -0,0 +1 @@ +export { PickersTextField } from './PickersTextField'; diff --git a/packages/x-date-pickers/src/internals/components/PickersTextField/pickersTextFieldClasses.ts b/packages/x-date-pickers/src/internals/components/PickersTextField/pickersTextFieldClasses.ts new file mode 100644 index 0000000000000..308eb8d259064 --- /dev/null +++ b/packages/x-date-pickers/src/internals/components/PickersTextField/pickersTextFieldClasses.ts @@ -0,0 +1,71 @@ +import { + unstable_generateUtilityClass as generateUtilityClass, + unstable_generateUtilityClasses as generateUtilityClasses, +} from '@mui/utils'; + +export interface PickersTextFieldClasses { + /** Styles applied to the root element. */ + root: string; + /** Styles applied to the root element if focused. */ + focused: string; + /** State class applied to the root element if `disabled=true`. */ + disabled: string; + /** State class applied to the root element if `error=true`. */ + error: string; + /** State class applied to the root element id `required=true` */ + required: string; +} +export type PickersTextFieldClassKey = keyof PickersTextFieldClasses; + +export function getPickersTextFieldUtilityClass(slot: string) { + return generateUtilityClass('MuiPickersTextField', slot); +} + +export const pickersTextFieldClasses = generateUtilityClasses( + 'MuiPickersTextField', + ['root', 'focused', 'disabled', 'error', 'required'], +); +export interface PickersInputClasses { + /** Styles applied to the root element. */ + root: string; + /** Styles applied to the root element if focused. */ + focused: string; + /** State class applied to the root element if `disabled=true`. */ + disabled: string; + /** State class applied to the root element if `error=true`. */ + error: string; + /** Styles applied to the NotchedOutline element. */ + notchedOutline: string; + /** Styles applied to the fake input element. */ + input: string; + /** Styles applied to the section of the picker. */ + content: string; + /** Styles applied to the startSeparator */ + before: string; + /** Styles applied to the endSeparator */ + after: string; + /** Styles applied to the root if there is a startAdornment present */ + adornedStart: string; + /** Styles applied to the root if there is an endAdornment present */ + adornedEnd: string; +} + +export type PickersInputClassKey = keyof PickersInputClasses; + +export function getPickersInputUtilityClass(slot: string) { + return generateUtilityClass('MuiPickersInput', slot); +} + +export const pickersInputClasses = generateUtilityClasses('MuiPickersInput', [ + 'root', + 'focused', + 'disabled', + 'error', + 'notchedOutline', + 'content', + 'before', + 'after', + 'adornedStart', + 'adornedEnd', + 'input', +]);