update
This commit is contained in:
@@ -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}
|
type={nativeType}
|
||||||
sx={{ width: '100%' }}
|
value={nativeValue}
|
||||||
value={displayed}
|
onChange={(e) => onChange?.(e.target.value)}
|
||||||
onChange={(e) => handleTextChange(e.target.value)}
|
InputLabelProps={{ shrink: true, ...rest.InputLabelProps }}
|
||||||
onBlur={handleBlur}
|
sx={{
|
||||||
placeholder={mode === 'date' ? 'TT.MM.JJJJ' : 'TT.MM.JJJJ HH:MM'}
|
'& input': {
|
||||||
InputLabelProps={{ shrink: true, ...rest.InputLabelProps }}
|
colorScheme: isDark ? 'dark' : 'light',
|
||||||
InputProps={{
|
},
|
||||||
endAdornment: (
|
...sx,
|
||||||
<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}
|
|
||||||
onChange={(e) => handlePickerChange(e.target.value)}
|
|
||||||
tabIndex={-1}
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
bottom: 0,
|
|
||||||
left: 0,
|
|
||||||
opacity: 0,
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
pointerEvents: 'none',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user