51fd8a446b
Нативные input type=time/datetime-local рендерят время по локали браузера/ОС (AM/PM) и игнорируют атрибут lang в Chrome — поэтому lang=ru-RU не помогал. Заменено на собственные контролы (components/timeFields.tsx): - Time24Field — текстовый ввод ЧЧ:ММ[:СС] с валидацией, детерминированный 24ч; - DateTime24Field — нативный календарь для даты + Time24Field для времени. Подключено в окне маршрута редактора и во всех datetime-local фильтрах (Планы, заявки на карте, создание/фильтр заявок).
95 lines
3.7 KiB
TypeScript
95 lines
3.7 KiB
TypeScript
import { useEffect, useState, type CSSProperties } from "react";
|
||
|
||
/**
|
||
* Ввод времени суток в строгом 24-часовом формате `ЧЧ:ММ[:СС]` (UTC).
|
||
*
|
||
* Нативные `<input type="time">`/`type="datetime-local"` рендерят время по локали
|
||
* браузера/ОС (часто AM/PM) и не подчиняются атрибуту `lang`, поэтому время вводим
|
||
* обычным текстовым полем с собственной валидацией. Локальный черновик позволяет
|
||
* печатать промежуточные значения; наружу отдаём только корректное `ЧЧ:ММ[:СС]`.
|
||
*/
|
||
type Time24FieldProps = {
|
||
/** Текущее значение в формате `ЧЧ:ММ` или `ЧЧ:ММ:СС`. */
|
||
value: string;
|
||
/** Показывать и принимать секунды. */
|
||
withSeconds?: boolean;
|
||
/** Вызывается при вводе корректного значения. */
|
||
onChange: (value: string) => void;
|
||
className?: string;
|
||
style?: CSSProperties;
|
||
"aria-label"?: string;
|
||
};
|
||
|
||
const TIME_RE_HM = /^([01]\d|2[0-3]):[0-5]\d$/;
|
||
const TIME_RE_HMS = /^([01]\d|2[0-3]):[0-5]\d:[0-5]\d$/;
|
||
|
||
export function Time24Field({ value, withSeconds = false, onChange, className, style, ...rest }: Time24FieldProps) {
|
||
const re = withSeconds ? TIME_RE_HMS : TIME_RE_HM;
|
||
const [draft, setDraft] = useState(value);
|
||
const [editing, setEditing] = useState(false);
|
||
|
||
// Пока поле не редактируется — синхронизируем отображение с внешним значением.
|
||
useEffect(() => {
|
||
if (!editing) setDraft(value);
|
||
}, [value, editing]);
|
||
|
||
return (
|
||
<input
|
||
{...rest}
|
||
type="text"
|
||
inputMode="numeric"
|
||
className={className}
|
||
style={style}
|
||
placeholder={withSeconds ? "чч:мм:сс" : "чч:мм"}
|
||
value={draft}
|
||
onFocus={() => setEditing(true)}
|
||
onChange={(event) => {
|
||
const next = event.target.value;
|
||
setDraft(next);
|
||
if (re.test(next)) onChange(next);
|
||
}}
|
||
onBlur={() => {
|
||
setEditing(false);
|
||
if (re.test(draft)) onChange(draft);
|
||
else setDraft(value); // невалидный ввод — откат к последнему корректному
|
||
}}
|
||
/>
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Замена нативного `<input type="datetime-local">`: нативный календарь для даты
|
||
* (у даты нет проблемы AM/PM) + [Time24Field] для времени в 24-часовом формате.
|
||
* Контракт значения тот же — строка `YYYY-MM-DDTHH:mm` (UTC), как у datetime-local.
|
||
*/
|
||
type DateTime24FieldProps = {
|
||
value: string;
|
||
onChange: (value: string) => void;
|
||
className?: string;
|
||
};
|
||
|
||
export function DateTime24Field({ value, onChange, className }: DateTime24FieldProps) {
|
||
const sep = value.indexOf("T");
|
||
const datePart = sep >= 0 ? value.slice(0, sep) : value;
|
||
const timePart = sep >= 0 ? value.slice(sep + 1, sep + 6) : "";
|
||
|
||
const combine = (date: string, time: string) => `${date}T${time || "00:00"}`;
|
||
|
||
return (
|
||
<span className={`tgu-dt24${className ? ` ${className}` : ""}`} style={{ display: "flex", alignItems: "center", gap: "0.3rem" }}>
|
||
<input
|
||
type="date"
|
||
lang="ru-RU"
|
||
style={{ width: "8rem" }}
|
||
value={datePart}
|
||
onChange={(event) => onChange(combine(event.target.value, timePart))}
|
||
/>
|
||
<Time24Field
|
||
style={{ width: "4.6rem", textAlign: "center" }}
|
||
value={timePart}
|
||
onChange={(time) => onChange(combine(datePart, time))}
|
||
/>
|
||
</span>
|
||
);
|
||
}
|