pcp-tgu-ops-ui: маска ввода времени — невозможные значения недопустимы
Time24Field принимал любой текст (99:99 и т.п.), валидным он был только при коммите/блюре. Добавлена маска maskTime: на каждый ввод оставляет цифры, расставляет двоеточия и зажимает сегменты (часы 0-23, мин/сек 0-59) с авто- дополнением первого разряда, поэтому набрать невозможное время нельзя. Покрыто юнит-тестами (timeFields.test.ts).
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { maskTime } from "./timeFields";
|
||||
|
||||
describe("maskTime", () => {
|
||||
it("не пропускает невозможные значения", () => {
|
||||
expect(maskTime("99:99", false)).toBe("09:09"); // 9>2 → 09; 9>5 → 09
|
||||
expect(maskTime("3070", false)).toBe("03:07"); // 3>2 → 03; 7>5 → 07
|
||||
expect(maskTime("2599", false)).toBe("23:09"); // 25→23 (зажим), 9>5 → 09
|
||||
});
|
||||
|
||||
it("расставляет двоеточия и ограничивает длину", () => {
|
||||
expect(maskTime("1234", false)).toBe("12:34");
|
||||
expect(maskTime("12", false)).toBe("12");
|
||||
expect(maskTime("123456789", false)).toBe("12:34");
|
||||
});
|
||||
|
||||
it("поддерживает секунды", () => {
|
||||
expect(maskTime("123456", true)).toBe("12:34:56");
|
||||
expect(maskTime("235959", true)).toBe("23:59:59");
|
||||
expect(maskTime("990000", true)).toBe("09:09:00");
|
||||
});
|
||||
|
||||
it("игнорирует мусор и лишние двоеточия", () => {
|
||||
expect(maskTime("1a2:b3", false)).toBe("12:3");
|
||||
expect(maskTime("", false)).toBe("");
|
||||
});
|
||||
});
|
||||
@@ -23,6 +23,46 @@ type Time24FieldProps = {
|
||||
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$/;
|
||||
|
||||
/**
|
||||
* Забирает из потока цифр один сегмент времени `00..max` (часы → 23, мин/сек → 59).
|
||||
* Если первая цифра заведомо больше старшего разряда (напр. `9` для часов), сразу
|
||||
* дополняет нулём (`09`) и переходит к следующему сегменту; двузначное значение
|
||||
* сверх максимума зажимается. Возвращает [сегмент, остаток цифр].
|
||||
*/
|
||||
function takeSegment(digits: string, max: number): [string, string] {
|
||||
if (digits.length === 0) return ["", ""];
|
||||
const firstMax = Math.floor(max / 10);
|
||||
if (Number(digits[0]) > firstMax) {
|
||||
return [`0${digits[0]}`, digits.slice(1)];
|
||||
}
|
||||
let segment = digits.slice(0, 2);
|
||||
if (segment.length === 2 && Number(segment) > max) {
|
||||
segment = String(max);
|
||||
}
|
||||
return [segment, digits.slice(2)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Маскирует произвольный ввод в `ЧЧ:ММ[:СС]`: оставляет только цифры, расставляет
|
||||
* двоеточия и зажимает каждый сегмент в допустимый диапазон, поэтому невозможные
|
||||
* значения (`99:99`, `30:70`) набрать нельзя — они приводятся к корректным.
|
||||
*/
|
||||
export function maskTime(input: string, withSeconds: boolean): string {
|
||||
const digits = input.replace(/\D/g, "").slice(0, withSeconds ? 6 : 4);
|
||||
const segments: string[] = [];
|
||||
let [segment, rest] = takeSegment(digits, 23);
|
||||
segments.push(segment);
|
||||
if (rest.length > 0) {
|
||||
[segment, rest] = takeSegment(rest, 59);
|
||||
segments.push(segment);
|
||||
}
|
||||
if (withSeconds && rest.length > 0) {
|
||||
[segment, rest] = takeSegment(rest, 59);
|
||||
segments.push(segment);
|
||||
}
|
||||
return segments.join(":");
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -38,13 +78,14 @@ export function Time24Field({ value, withSeconds = false, onChange, className, s
|
||||
{...rest}
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
maxLength={withSeconds ? 8 : 5}
|
||||
className={className}
|
||||
style={style}
|
||||
placeholder={withSeconds ? "чч:мм:сс" : "чч:мм"}
|
||||
value={draft}
|
||||
onFocus={() => setEditing(true)}
|
||||
onChange={(event) => {
|
||||
const next = event.target.value;
|
||||
const next = maskTime(event.target.value, withSeconds);
|
||||
setDraft(next);
|
||||
if (re.test(next)) onChange(next);
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user