fix(forms): round temporal input steps to millisecond precision

This commit is contained in:
ldm0
2026-10-04 05:27:43 +08:00
parent 91bfb9f374
commit 5e047950e2
3 changed files with 110 additions and 1 deletions
+21 -1
View File
@@ -209,11 +209,31 @@ pub fn input_step(input_type: InputType, step: Option<&str>) -> Option<f64> {
Some(
parse_finite_number(step)
.filter(|value| *value > 0.0)
.map(|value| value * input_step_scale(input_type))
.map(|value| {
let scaled = value * input_step_scale(input_type);
if matches!(input_type, InputType::Time | InputType::DatetimeLocal) {
temporal_step_milliseconds(step).unwrap_or_else(|| scaled.round().max(1.0))
} else {
scaled
}
})
.unwrap_or_else(|| default_input_step(input_type)),
)
}
fn temporal_step_milliseconds(step: &str) -> Option<f64> {
let value = parse_decimal_number(step)?;
let milliseconds = if value.scale <= 3 {
value
.coefficient
.checked_mul(checked_pow10(3 - value.scale)?)?
} else {
let divisor = checked_pow10(value.scale - 3)?;
value.coefficient / divisor + i128::from(value.coefficient % divisor >= divisor / 2)
};
Some(milliseconds.max(1) as f64)
}
fn default_input_step(input_type: InputType) -> f64 {
match input_type {
InputType::Date => MS_PER_DAY,
+50
View File
@@ -439,6 +439,56 @@ fn meter_values_share_html_numeric_clamping_and_gauge_region_rules() {
);
}
#[test]
fn temporal_steps_round_to_millisecond_precision_before_stepping() {
for kind in [InputType::Time, InputType::DatetimeLocal] {
for (step, expected) in [
("0.9999", 1000.0),
("1.0025", 1003.0),
("2.0004", 2000.0),
("2.0005", 2001.0),
("0.0001", 1.0),
("5e-4", 1.0),
("59.9999", 60000.0),
] {
assert_eq!(
input_step(kind, Some(step)),
Some(expected),
"{kind:?} step={step}"
);
}
assert_eq!(input_step(kind, Some("any")), None);
assert_eq!(input_step(kind, Some("0")), Some(60000.0));
}
assert_eq!(input_step(InputType::Number, Some("0.0001")), Some(0.0001));
assert_eq!(input_step(InputType::Range, Some("0.9999")), Some(0.9999));
for (kind, initial, expected) in [
(InputType::Time, "00:00:01.003", "00:00:02.006"),
(
InputType::DatetimeLocal,
"1970-01-01T00:00:01.003",
"1970-01-01T00:00:02.006",
),
] {
assert_eq!(
step_input_value(
InputStepState {
input_type: kind,
value: initial,
min: None,
max: None,
step: Some("1.0025"),
value_attribute: None,
},
InputStepDirection::Up,
1.0
)
.unwrap(),
InputStepOutcome::Set(expected.into())
);
}
}
#[test]
fn input_step_up_down_rules_are_shared() {
let stepped = step_input_value(
@@ -6008,3 +6008,42 @@ fn disconnected_radio_groups_follow_tree_roots_and_form_owners() {
"true|true:false|true|true:false|true|true|true:true|false:true"
);
}
#[test]
fn temporal_input_validation_and_stepping_use_rounded_millisecond_steps() {
for (kind, step, initial, after_up) in [
("time", "0.9999", "12:30:02", "12:30:03"),
("time", "1.0025", "00:00:01.003", "00:00:02.006"),
("time", "0.0001", "00:00:00.001", "00:00:00.002"),
(
"datetime-local",
"59.9999",
"2024-02-29T12:30",
"2024-02-29T12:31",
),
(
"datetime-local",
"1.0025",
"1970-01-01T00:00:01.003",
"1970-01-01T00:00:02.006",
),
] {
let mut vm = new_storage_test_vm("https://temporal-step.test/");
let result=vm.eval(&format!(r#"
(() => {{
const input=document.createElement('input');
input.type={kind:?};input.step={step:?};input.value={initial:?};
const initialMismatch=input.validity.stepMismatch;
input.stepUp();
const afterUp=input.value, afterUpMismatch=input.validity.stepMismatch;
input.stepDown();
return JSON.stringify([initialMismatch,afterUp,afterUpMismatch,input.value,input.validity.stepMismatch]);
}})()
"#)).expect("ordinary temporal input probe");
assert_eq!(
result,
format!("[false,{after_up:?},false,{initial:?},false]"),
"{kind} step={step}"
);
}
}