This commit is contained in:
Matthias Hochmeister
2026-03-26 13:01:59 +01:00
parent 507111e8e8
commit 3c95b7506b

View File

@@ -1,32 +1,21 @@
import { useState, useRef } from 'react';
import TextField, { type TextFieldProps } from '@mui/material/TextField'; import TextField, { type TextFieldProps } from '@mui/material/TextField';
import InputAdornment from '@mui/material/InputAdornment'; import { useTheme } from '@mui/material/styles';
import IconButton from '@mui/material/IconButton';
import Box from '@mui/material/Box';
import CalendarMonth from '@mui/icons-material/CalendarMonth';
import {
toGermanDate,
toGermanDateTime,
fromGermanDate,
fromGermanDateTime,
} from '../../utils/dateInput';
export type GermanDateMode = 'date' | 'datetime'; export type GermanDateMode = 'date' | 'datetime';
export interface GermanDateFieldProps extends Omit<TextFieldProps, 'value' | 'onChange' | 'type'> { export interface GermanDateFieldProps extends Omit<TextFieldProps, 'value' | 'onChange' | 'type'> {
/** ISO date (YYYY-MM-DD) or datetime (YYYY-MM-DDTHH:MM) string */ /** ISO date (YYYY-MM-DD) or datetime (YYYY-MM-DDTHH:MM) string */
value?: string | null; value?: string | null;
/** Called with ISO date/datetime string whenever value changes to a valid date */ /** Called with ISO date/datetime string on change */
onChange?: (isoValue: string) => void; onChange?: (isoValue: string) => void;
mode?: GermanDateMode; mode?: GermanDateMode;
} }
/** /**
* A MUI TextField that: * Themed date / datetime input that:
* - Displays dates in German format (TT.MM.JJJJ / TT.MM.JJJJ HH:MM) * - Uses the native browser date picker (locale-aware → shows DD.MM.YYYY in German browsers)
* - Accepts free-text entry in that format * - Properly supports dark / light mode via CSS colorScheme
* - Has a calendar icon that opens the native browser date picker as a popup * - Thin wrapper: all MUI TextField props work as expected
* - onChange fires with an ISO string whenever the entered value is valid
*/ */
export default function GermanDateField({ export default function GermanDateField({
value, value,
@@ -35,84 +24,26 @@ export default function GermanDateField({
sx, sx,
...rest ...rest
}: GermanDateFieldProps) { }: GermanDateFieldProps) {
// localValue overrides the displayed text while the user is typing const theme = useTheme();
const [localValue, setLocalValue] = useState<string | null>(null); const isDark = theme.palette.mode === 'dark';
const hiddenRef = useRef<HTMLInputElement>(null);
const displayed =
localValue ?? (mode === 'date' ? toGermanDate(value) : toGermanDateTime(value));
function handleTextChange(raw: string) {
setLocalValue(raw);
const iso = mode === 'date' ? fromGermanDate(raw) : fromGermanDateTime(raw);
if (iso) {
onChange?.(iso);
setLocalValue(null);
}
}
function handleBlur() {
// Revert to the last valid formatted value if the text is incomplete
setLocalValue(null);
}
function handlePickerChange(nativeVal: string) {
if (!nativeVal) return;
if (mode === 'date') {
onChange?.(nativeVal);
} else {
// Preserve current time portion if available, default to 08:00
const time = value?.substring(11, 16) || '08:00';
onChange?.(`${nativeVal}T${time}`);
}
}
const nativeType = mode === 'date' ? 'date' : 'datetime-local';
const nativeValue = const nativeValue =
mode === 'date' ? (value?.substring(0, 10) || '') : (value?.substring(0, 16) || ''); mode === 'date' ? (value?.substring(0, 10) || '') : (value?.substring(0, 16) || '');
return ( return (
<Box sx={{ position: 'relative', display: 'inline-flex', width: rest.fullWidth ? '100%' : undefined, ...sx }}>
<TextField <TextField
{...rest} {...rest}
sx={{ width: '100%' }} type={nativeType}
value={displayed}
onChange={(e) => handleTextChange(e.target.value)}
onBlur={handleBlur}
placeholder={mode === 'date' ? 'TT.MM.JJJJ' : 'TT.MM.JJJJ HH:MM'}
InputLabelProps={{ shrink: true, ...rest.InputLabelProps }}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
size="small"
tabIndex={-1}
edge="end"
onClick={() => hiddenRef.current?.showPicker?.()}
>
<CalendarMonth fontSize="small" />
</IconButton>
</InputAdornment>
),
...rest.InputProps,
}}
/>
{/* Hidden native input used solely to open the browser's date picker popup */}
<input
ref={hiddenRef}
type={mode === 'date' ? 'date' : 'datetime-local'}
value={nativeValue} value={nativeValue}
onChange={(e) => handlePickerChange(e.target.value)} onChange={(e) => onChange?.(e.target.value)}
tabIndex={-1} InputLabelProps={{ shrink: true, ...rest.InputLabelProps }}
style={{ sx={{
position: 'absolute', '& input': {
bottom: 0, colorScheme: isDark ? 'dark' : 'light',
left: 0, },
opacity: 0, ...sx,
width: '100%',
height: '100%',
pointerEvents: 'none',
}} }}
/> />
</Box>
); );
} }