From 8260b02eb77b8c0478a32c9c1a0abcf34251064e Mon Sep 17 00:00:00 2001 From: Johan Lindh Date: Wed, 22 Jul 2026 20:54:11 +0200 Subject: [PATCH] docs(ui): document Range browser-default bounds (#201) An HTML range input carries an implicit browser domain of min="0", max="100", step="1". Range emits none of those attributes, so a bound value outside that domain or off the step grid is silently clamped and rounded by the browser and the adjusted value is echoed back through the setter, overwriting server state. Document this on NewRange and RequestWriter.Range, stating that callers must supply explicit min/max/step attributes as params when the defaults do not cover the bound value's domain, and add a test guarding that those params render verbatim. --- lib/ui/input_widgets_test.go | 10 ++++++++++ lib/ui/range.go | 15 ++++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/lib/ui/input_widgets_test.go b/lib/ui/input_widgets_test.go index bc458fbc..6f30a87d 100644 --- a/lib/ui/input_widgets_test.go +++ b/lib/ui/input_widgets_test.go @@ -179,6 +179,16 @@ func TestInputFloatWidgets(t *testing.T) { mustMatch(t, `^$`, got) } +// TestRangeRendersExplicitBounds guards the documented workaround for the browser +// range defaults: min, max and step passed as params render verbatim after the +// value, letting a caller widen a domain the default 0..100 would otherwise clamp. +func TestRangeRendersExplicitBounds(t *testing.T) { + _, rq := newCoreRequest(t) + sf := newTestSetter(150.0) + _, got := renderUI(t, rq, NewRange(sf), `min="0"`, `max="200"`, `step="0.5"`) + mustMatch(t, `^$`, got) +} + // TestInputFloat_ReconcilesConvertedValues verifies that number and range inputs // receive the canonical server value when a numeric adapter truncates or rounds // an input to the value that was already stored. diff --git a/lib/ui/range.go b/lib/ui/range.go index ab728014..753163f8 100644 --- a/lib/ui/range.go +++ b/lib/ui/range.go @@ -19,6 +19,13 @@ type Range struct{ InputFloat } // rendering or wire representation, so rendering, updating, or receiving one from // the browser cancels the [jaws.Request] with a cause wrapping // [jaws.ErrValueNotFinite]. +// +// The widget emits no min, max, or step attribute of its own, so the browser +// applies the HTML range defaults min="0", max="100", step="1". A bound value +// outside that domain, or off the step grid, is silently clamped and rounded by +// the browser, and the adjusted value is echoed back through the setter on the +// next input event. Supply explicit min, max, and step attributes as render +// params when the defaults do not cover the bound value's domain. func NewRange(g bind.Setter[float64]) *Range { return &Range{InputFloat{Setter: g}} } // JawsRender renders ui as an HTML range input. @@ -28,7 +35,13 @@ func (u *Range) JawsRender(elem *jaws.Element, w io.Writer, params []any) error // Range renders an HTML range input. // -// See [NewRange] for how a non-finite bound value is handled. +// Supply min, max, and step attributes in params when the browser defaults +// (min="0", max="100", step="1") do not cover the bound value's domain; a value +// outside it is silently clamped and rounded by the browser. For example: +// +// rw.Range(&v, `min="0"`, `max="200"`, `step="0.5"`) +// +// See [NewRange] for details and for how a non-finite bound value is handled. func (rw RequestWriter) Range(value any, params ...any) error { return rw.NewUI(NewRange(bind.MakeSetterFloat64(value)), params...) }