mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-06-11 09:35:59 -04:00
Merge pull request #2267 from navidemad/fix-a17-input-range-clamp
forms: clamp <input type=range> value to min/max
This commit is contained in:
@@ -85,3 +85,77 @@
|
||||
testing.expectEqual('', i3.autocomplete);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="range-clamp">
|
||||
{
|
||||
// Per WHATWG HTML spec, value sanitization for `type=range` clamps to
|
||||
// [min, max] with defaults of 0 and 100.
|
||||
// https://html.spec.whatwg.org/multipage/input.html#range-state-(type=range)
|
||||
const r = document.createElement('input');
|
||||
r.type = 'range';
|
||||
r.min = '0';
|
||||
r.max = '100';
|
||||
|
||||
// Above max -> max
|
||||
r.value = '999';
|
||||
testing.expectEqual('100', r.value);
|
||||
|
||||
// Below min -> min
|
||||
r.value = '-10';
|
||||
testing.expectEqual('0', r.value);
|
||||
|
||||
// In range -> unchanged
|
||||
r.value = '50';
|
||||
testing.expectEqual('50', r.value);
|
||||
|
||||
// Negative range
|
||||
const r2 = document.createElement('input');
|
||||
r2.type = 'range';
|
||||
r2.min = '-50';
|
||||
r2.max = '50';
|
||||
r2.value = '0';
|
||||
testing.expectEqual('0', r2.value);
|
||||
r2.value = '100';
|
||||
testing.expectEqual('50', r2.value);
|
||||
r2.value = '-100';
|
||||
testing.expectEqual('-50', r2.value);
|
||||
|
||||
// Fractional bounds
|
||||
const r3 = document.createElement('input');
|
||||
r3.type = 'range';
|
||||
r3.min = '0.5';
|
||||
r3.max = '1.5';
|
||||
r3.value = '2';
|
||||
testing.expectEqual('1.5', r3.value);
|
||||
r3.value = '0';
|
||||
testing.expectEqual('0.5', r3.value);
|
||||
// Note: in-range pass-through under clamping alone is exercised by r1/r4.
|
||||
// An assertion like `r3.value = '1' -> '1'` would conflict with WHATWG step
|
||||
// matching (default step=1, base=min=0.5 -> nearest valid is '1.5'), which
|
||||
// is intentionally out of scope for this PR; tracking separately.
|
||||
|
||||
// Default min/max (0..100) when attributes absent
|
||||
const r4 = document.createElement('input');
|
||||
r4.type = 'range';
|
||||
r4.value = '999';
|
||||
testing.expectEqual('100', r4.value);
|
||||
r4.value = '-1';
|
||||
testing.expectEqual('0', r4.value);
|
||||
|
||||
// Non-numeric value falls back to spec default `min + (max - min) / 2`
|
||||
const r5 = document.createElement('input');
|
||||
r5.type = 'range';
|
||||
r5.min = '0';
|
||||
r5.max = '100';
|
||||
r5.value = 'garbage';
|
||||
testing.expectEqual('50', r5.value);
|
||||
|
||||
// Spec default is the midpoint, not a hardcoded "50"
|
||||
const r6 = document.createElement('input');
|
||||
r6.type = 'range';
|
||||
r6.min = '20';
|
||||
r6.max = '40';
|
||||
r6.value = 'garbage';
|
||||
testing.expectEqual('30', r6.value);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -564,7 +564,7 @@ fn sanitizeValue(self: *Input, comptime dupe: bool, value: []const u8, frame: *F
|
||||
.time => return if (isValidTime(value)) if (comptime dupe) try frame.dupeString(value) else value else "",
|
||||
.@"datetime-local" => return try sanitizeDatetimeLocal(dupe, value, frame.arena),
|
||||
.number => return if (isValidFloatingPoint(value)) if (comptime dupe) try frame.dupeString(value) else value else "",
|
||||
.range => return if (isValidFloatingPoint(value)) if (comptime dupe) try frame.dupeString(value) else value else "50",
|
||||
.range => return try sanitizeRange(dupe, value, self.getMin(), self.getMax(), frame),
|
||||
.color => {
|
||||
if (value.len == 7 and value[0] == '#') {
|
||||
var needs_lower = false;
|
||||
@@ -786,6 +786,46 @@ fn sanitizeDatetimeLocal(comptime dupe: bool, value: []const u8, arena: std.mem.
|
||||
return result[0..total_len];
|
||||
}
|
||||
|
||||
/// Sanitize value for `<input type=range>` per WHATWG HTML spec:
|
||||
/// https://html.spec.whatwg.org/multipage/input.html#range-state-(type=range)
|
||||
/// 1. If value is not a valid floating-point number, set it to
|
||||
/// `min + (max - min) / 2`.
|
||||
/// 2. If value < min, set it to min.
|
||||
/// 3. If value > max, set it to max.
|
||||
/// `min`/`max` default to 0 and 100 respectively when the attribute is missing
|
||||
/// or fails to parse as a valid floating-point number. Step matching is not
|
||||
/// applied here.
|
||||
fn sanitizeRange(
|
||||
comptime dupe: bool,
|
||||
value: []const u8,
|
||||
min_attr: []const u8,
|
||||
max_attr: []const u8,
|
||||
frame: *Frame,
|
||||
) ![]const u8 {
|
||||
const min: f64 = if (isValidFloatingPoint(min_attr))
|
||||
std.fmt.parseFloat(f64, min_attr) catch 0
|
||||
else
|
||||
0;
|
||||
const max: f64 = if (isValidFloatingPoint(max_attr))
|
||||
std.fmt.parseFloat(f64, max_attr) catch 100
|
||||
else
|
||||
100;
|
||||
|
||||
if (!isValidFloatingPoint(value)) {
|
||||
return try formatFloat(frame.arena, min + (max - min) / 2);
|
||||
}
|
||||
|
||||
const v = std.fmt.parseFloat(f64, value) catch unreachable; // grammar already validated
|
||||
if (v < min) return try formatFloat(frame.arena, min);
|
||||
if (v > max) return try formatFloat(frame.arena, max);
|
||||
return if (comptime dupe) try frame.dupeString(value) else value;
|
||||
}
|
||||
|
||||
/// Format an f64 to its shortest decimal representation, arena-allocated.
|
||||
fn formatFloat(arena: std.mem.Allocator, value: f64) ![]const u8 {
|
||||
return std.fmt.allocPrint(arena, "{d}", .{value});
|
||||
}
|
||||
|
||||
/// Parse a slice that must be ALL ASCII digits into a u32. Returns null if any non-digit or empty.
|
||||
fn parseAllDigits(s: []const u8) ?u32 {
|
||||
if (s.len == 0) return null;
|
||||
|
||||
Reference in New Issue
Block a user