Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/pages/material-ui/api/input-base.json
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,12 @@
"description": "Styles applied to the input element.",
"isGlobal": false
},
{
"key": "inputEmptyDateLike",
"className": "MuiInputBase-inputEmptyDateLike",
"description": "State class applied to the input element if it is an empty date/time input (`type=\"date\"`, `\"datetime-local\"`, `\"month\"`, `\"time\"`, or `\"week\"`).",
"isGlobal": false
},
{
"key": "inputTypeSearch",
"className": "MuiInputBase-inputTypeSearch",
Expand Down
6 changes: 6 additions & 0 deletions docs/pages/material-ui/api/input.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,12 @@
"description": "Styles applied to the input element.",
"isGlobal": false
},
{
"key": "inputEmptyDateLike",
"className": "MuiInput-inputEmptyDateLike",
"description": "State class applied to the input element if it is an empty date/time input (`type=\"date\"`, `\"datetime-local\"`, `\"month\"`, `\"time\"`, or `\"week\"`).",
"isGlobal": false
},
{
"key": "inputTypeSearch",
"className": "MuiInput-inputTypeSearch",
Expand Down
6 changes: 6 additions & 0 deletions docs/pages/material-ui/api/outlined-input.json
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,12 @@
"description": "Styles applied to the input element.",
"isGlobal": false
},
{
"key": "inputEmptyDateLike",
"className": "MuiOutlinedInput-inputEmptyDateLike",
"description": "State class applied to the input element if it is an empty date/time input (`type=\"date\"`, `\"datetime-local\"`, `\"month\"`, `\"time\"`, or `\"week\"`).",
"isGlobal": false
},
{
"key": "inputTypeSearch",
"className": "MuiOutlinedInput-inputTypeSearch",
Expand Down
5 changes: 5 additions & 0 deletions docs/translations/api-docs/input-base/input-base.json
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,11 @@
"conditions": "<code>hiddenLabel={true}</code>"
},
"input": { "description": "Styles applied to {{nodeName}}.", "nodeName": "the input element" },
"inputEmptyDateLike": {
"description": "State class applied to {{nodeName}} if {{conditions}}.",
"nodeName": "the input element",
"conditions": "it is an empty date/time input (<code>type=\"date\"</code>, <code>\"datetime-local\"</code>, <code>\"month\"</code>, <code>\"time\"</code>, or <code>\"week\"</code>)"
},
"inputTypeSearch": { "description": "" },
"multiline": {
"description": "Styles applied to {{nodeName}} if {{conditions}}.",
Expand Down
5 changes: 5 additions & 0 deletions docs/translations/api-docs/input/input.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,11 @@
"conditions": "<code>fullWidth={true}</code>"
},
"input": { "description": "Styles applied to {{nodeName}}.", "nodeName": "the input element" },
"inputEmptyDateLike": {
"description": "State class applied to {{nodeName}} if {{conditions}}.",
"nodeName": "the input element",
"conditions": "it is an empty date/time input (<code>type=\"date\"</code>, <code>\"datetime-local\"</code>, <code>\"month\"</code>, <code>\"time\"</code>, or <code>\"week\"</code>)"
},
"inputTypeSearch": {
"description": "Styles applied to {{nodeName}} if {{conditions}}.",
"nodeName": "the input element",
Expand Down
5 changes: 5 additions & 0 deletions docs/translations/api-docs/outlined-input/outlined-input.json
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,11 @@
"conditions": "the component is focused"
},
"input": { "description": "Styles applied to {{nodeName}}.", "nodeName": "the input element" },
"inputEmptyDateLike": {
"description": "State class applied to {{nodeName}} if {{conditions}}.",
"nodeName": "the input element",
"conditions": "it is an empty date/time input (<code>type=\"date\"</code>, <code>\"datetime-local\"</code>, <code>\"month\"</code>, <code>\"time\"</code>, or <code>\"week\"</code>)"
},
"inputTypeSearch": {
"description": "Styles applied to {{nodeName}} if {{conditions}}.",
"nodeName": "the input element",
Expand Down
2 changes: 2 additions & 0 deletions packages/mui-material/src/Input/inputClasses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export interface InputClasses {
input: string;
/** Styles applied to the input element if `type="search"`. */
inputTypeSearch: string;
/** State class applied to the input element if it is an empty date/time input (`type="date"`, `"datetime-local"`, `"month"`, `"time"`, or `"week"`). */
inputEmptyDateLike: string;
}

export type InputClassKey = keyof InputClasses;
Expand Down
35 changes: 33 additions & 2 deletions packages/mui-material/src/InputBase/InputBase.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ import { getTransitionStyles } from '../transitions/utils';
const MUI_AUTO_FILL = 'mui-auto-fill';
const MUI_AUTO_FILL_CANCEL = 'mui-auto-fill-cancel';

// Native date/time inputs render their value (and, when empty, a placeholder)
// through the `::-webkit-datetime-edit` pseudo-elements using the input's color.
// As a result an empty field looks identical to a filled one. We track the empty
// state to grey out the placeholder, matching the regular text placeholder.
const DATE_LIKE_TYPES = ['date', 'datetime-local', 'month', 'time', 'week'];

export const rootOverridesResolver = (props, styles) => {
const { ownerState } = props;

Expand Down Expand Up @@ -58,6 +64,7 @@ const useUtilityClasses = (ownerState) => {
formControl,
fullWidth,
hiddenLabel,
isEmptyDateLikeInput,
multiline,
readOnly,
size,
Expand All @@ -84,6 +91,7 @@ const useUtilityClasses = (ownerState) => {
'input',
disabled && 'disabled',
type === 'search' && 'inputTypeSearch',
isEmptyDateLikeInput && 'inputEmptyDateLike',
readOnly && 'readOnly',
],
};
Expand Down Expand Up @@ -162,6 +170,14 @@ export const InputBaseInput = styled('input', {
: {
opacity: light ? 0.42 : 0.5,
};
// Applied to the `::-webkit-datetime-edit` of an empty date/time input so its
// placeholder is dimmed like a regular text placeholder instead of looking filled.
const datetimePlaceholder = {
...placeholderVisible,
...getTransitionStyles(theme, 'opacity', {
duration: theme.transitions.duration.shorter,
}),
};

return {
font: 'inherit',
Expand Down Expand Up @@ -242,6 +258,12 @@ export const InputBaseInput = styled('input', {
MozAppearance: 'textfield', // Improve type search style.
},
},
{
props: ({ ownerState }) => ownerState.isEmptyDateLikeInput,
style: {
'&::-webkit-datetime-edit': datetimePlaceholder,
},
},
],
};
}),
Expand Down Expand Up @@ -305,6 +327,10 @@ const InputBase = React.forwardRef(function InputBase(inProps, ref) {
const value = inputPropsProp.value != null ? inputPropsProp.value : valueProp;
const { current: isControlled } = React.useRef(value != null);

const isDateLikeType = DATE_LIKE_TYPES.includes(type);
// Only meaningful for date/time inputs, where it dims the native placeholder.
const [empty, setEmpty] = React.useState(() => !isFilled({ value, defaultValue }, true));

const inputRef = React.useRef();
const handleInputRefWarning = React.useCallback((instance) => {
if (process.env.NODE_ENV !== 'production') {
Expand Down Expand Up @@ -364,15 +390,19 @@ const InputBase = React.forwardRef(function InputBase(inProps, ref) {

const checkDirty = React.useCallback(
(obj) => {
if (isFilled(obj)) {
const filled = isFilled(obj);
if (filled) {
if (onFilled) {
onFilled();
}
} else if (onEmpty) {
onEmpty();
}
if (isDateLikeType) {
setEmpty(!filled);
}
},
[onFilled, onEmpty],
[onFilled, onEmpty, isDateLikeType],
);

useEnhancedEffect(() => {
Expand Down Expand Up @@ -535,6 +565,7 @@ const InputBase = React.forwardRef(function InputBase(inProps, ref) {
formControl: muiFormControl,
fullWidth,
hiddenLabel: fcs.hiddenLabel,
isEmptyDateLikeInput: isDateLikeType && empty,
multiline,
size: fcs.size,
startAdornment,
Expand Down
43 changes: 43 additions & 0 deletions packages/mui-material/src/InputBase/InputBase.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,49 @@ describe('<InputBase />', () => {
});
});

describe('empty date/time inputs', () => {
['date', 'datetime-local', 'month', 'time', 'week'].forEach((type) => {
it(`applies the inputEmptyDateLike class to an empty type="${type}" input`, () => {
const { container } = render(<InputBase type={type} value="" onChange={() => {}} />);
expect(container.querySelector('input')).to.have.class(classes.inputEmptyDateLike);
});
});

it('does not apply the class when the date input has a value', () => {
const { container } = render(
<InputBase type="date" value="2020-01-01" onChange={() => {}} />,
);
expect(container.querySelector('input')).not.to.have.class(classes.inputEmptyDateLike);
});

it('does not apply the class to a non-date input even when empty', () => {
const { container } = render(<InputBase type="text" value="" onChange={() => {}} />);
expect(container.querySelector('input')).not.to.have.class(classes.inputEmptyDateLike);
});

it('toggles the class as the controlled value changes', () => {
const { container, setProps } = render(
<InputBase type="date" value="" onChange={() => {}} />,
);
expect(container.querySelector('input')).to.have.class(classes.inputEmptyDateLike);

setProps({ value: '2020-01-01' });
expect(container.querySelector('input')).not.to.have.class(classes.inputEmptyDateLike);

setProps({ value: '' });
expect(container.querySelector('input')).to.have.class(classes.inputEmptyDateLike);
});

it('toggles the class for uncontrolled inputs on change', () => {
const { container } = render(<InputBase type="date" />);
const input = container.querySelector('input');
expect(input).to.have.class(classes.inputEmptyDateLike);

fireEvent.change(input, { target: { value: '2020-01-01' } });
expect(input).not.to.have.class(classes.inputEmptyDateLike);
});
});

it('should fire event callbacks', () => {
const handleChange = spy();
const handleFocus = spy();
Expand Down
3 changes: 3 additions & 0 deletions packages/mui-material/src/InputBase/inputBaseClasses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ export interface InputBaseClasses {
/** Styles applied to the input element. */
input: string;
inputTypeSearch: string;
/** State class applied to the input element if it is an empty date/time input (`type="date"`, `"datetime-local"`, `"month"`, `"time"`, or `"week"`). */
inputEmptyDateLike: string;
}

export type InputBaseClassKey = keyof InputBaseClasses;
Expand All @@ -55,6 +57,7 @@ const inputBaseClasses: InputBaseClasses = generateUtilityClasses('MuiInputBase'
'readOnly',
'input',
'inputTypeSearch',
'inputEmptyDateLike',
]);

export default inputBaseClasses;
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export interface OutlinedInputClasses {
input: string;
/** Styles applied to the input element if `type="search"`. */
inputTypeSearch: string;
/** State class applied to the input element if it is an empty date/time input (`type="date"`, `"datetime-local"`, `"month"`, `"time"`, or `"week"`). */
inputEmptyDateLike: string;
}

export type OutlinedInputClassKey = keyof OutlinedInputClasses;
Expand Down
44 changes: 44 additions & 0 deletions test/regressions/fixtures/TextField/EmptyDateTextField.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import * as React from 'react';
import TextField from '@mui/material/TextField';

const types = ['date', 'datetime-local', 'month', 'time', 'week'];

// The empty inputs should render a dimmed placeholder, while the filled ones
// render at full color, so the two rows are visually distinguishable.
export default function EmptyDateTextField() {
return (
<div>
{['standard', 'outlined', 'filled'].map((variant) => (
<div key={variant}>
{types.map((type) => (
<TextField
key={`${variant}-empty-${type}`}
type={type}
label={`empty ${type}`}
variant={variant}
slotProps={{ inputLabel: { shrink: true } }}
/>
))}
{types.map((type) => (
<TextField
key={`${variant}-filled-${type}`}
type={type}
defaultValue={
{
date: '2020-01-01',
'datetime-local': '2020-01-01T10:30',
month: '2020-01',
time: '10:30',
week: '2020-W01',
}[type]
}
label={`filled ${type}`}
variant={variant}
slotProps={{ inputLabel: { shrink: true } }}
/>
))}
</div>
))}
</div>
);
}
Loading