Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions src/tweaks/controls/interval.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// ── Interval / range slider — dual-handle [lo,hi]. Lazy.
import { el, txt, clamp, roundToStep, stepPrecision, gridEnds, normalizeRange, rangeStep, overlapsText, wireHoverClass, onReady, onLive, registerControl } from "../shared.js";
import { el, txt, clamp, roundToStep, stepPrecision, gridEnds, normalizeRange, rangeStep, defaultRange, overlapsText, wireHoverClass, onReady, onLive, registerControl } from "../shared.js";
import { dragGesture } from "../heavy.js";
import type { OnChange } from "../shared.js";
import type { Meta } from "../schema.js";
Expand All @@ -16,7 +16,13 @@ function createInterval(meta: Meta, onChange: OnChange): Control {
// swaps, and a degenerate step re-infers — so markup like data-min="abc"
// can't ride in as NaN ("NaN – NaN").
const t0 = +(meta.value && meta.value[0]), t1 = +(meta.value && meta.value[1]);
let { min, max, step } = normalizeRange(meta.min, meta.max, meta.step, Number.isFinite(t0) ? t0 : 0, Number.isFinite(t1) ? t1 : 1);
const lo0 = Number.isFinite(t0) ? t0 : 0, hi0 = Number.isFinite(t1) ? t1 : 1;
// An absent bound derives from the values the range must hold, as the shorthand and a verbose
// slider do: each end widens it, so [20, 80] spans 0–240, not just 20–80 (which pinned both
// handles at the ends).
const [a, b] = [defaultRange(lo0), defaultRange(hi0)];
const given = (n: unknown) => n != null && n !== "" && Number.isFinite(+n!);
let { min, max, step } = normalizeRange(given(meta.min) ? meta.min : Math.min(a[0], b[0]), given(meta.max) ? meta.max : Math.max(a[1], b[1]), meta.step, lo0, hi0);
[min, max] = gridEnds(min, max, step); // as the slider: the range is its reachable grid, so an off-grid bound never reports a value past it and every surface shares one lattice
const decimals = stepPrecision(step);
const q = (v: number) => roundToStep(v, min, step);
Expand Down
11 changes: 4 additions & 7 deletions src/tweaks/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,10 @@ const restoreDefault = (ctrl: Pick<Control, "set">, raw: unknown, def: unknown)
// "segmented" is kept as an alias: picking one of a list renders as the radio grid (the
// nicer-looking single-select). The inline pill is reserved for booleans.
const radiogridMeta = (v: any): MetaFields => Array.isArray(v.options) && { type: "radiogrid", options: v.options, value: v.value ?? optValue(v.options[0]), cols: v.cols };
// The colour a `{ type: "color" }` / [data-tw="color"] opens on when none is given — it
// lives on the meta (not only in the picker's own fallback) so reset() restores it rather
// than handing the control `undefined`, which parsed as black.
// The verbose range bounds (slider + interval): an absent min/max derives from the value(s)
// the range must contain, the way the bare-number shorthand does (defaultRange) — so
// `{ type: "slider", value: 50 }` spans 0–150 like `size: 50`, and an interval's ends each
// widen it. (Both used to default to 0–1 and clamp a 50 to 1.) Step defaults to the grain.
// A verbose slider's range bounds: an absent min/max derives from the value the range must
// contain, the way the bare-number shorthand does (defaultRange), so `{ type: "slider",
// value: 50 }` spans 0–150 like `size: 50`. (The interval does the same in its own
// constructor, since its verbose form passes through as given.) Step defaults to the grain.
const rangeOf = (v: any, ...seeds: unknown[]) => {
const ranges = (seeds.length ? seeds : [0]).map((s) => defaultRange(Number.isFinite(+s) ? +s : 0));
const min = v.min ?? Math.min(...ranges.map((r) => r[0])), max = v.max ?? Math.max(...ranges.map((r) => r[1]));
Expand Down
12 changes: 11 additions & 1 deletion test/schema-defaults.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,14 @@ test("a verbose slider / interval without min/max derives its range from the val
assert.deepEqual(p.params.i, [20, 80]);
assert.equal(p.params.neg, -5);
assert.equal(p.params.unit, 0.5);
const [s] = p.el.querySelectorAll("[role=slider]");
const [s, i0, i1] = p.el.querySelectorAll("[role=slider]");
assert.equal(s.getAttribute("aria-valuemin"), "0");
assert.equal(s.getAttribute("aria-valuemax"), "150"); // the shorthand's 0–3×value
// The interval's ends each widen its range (0–240 for [20, 80]), so both handles can move out;
// a range locked to the value tuple pinned them at the ends.
for (const h of [i0, i1]) { assert.equal(h.getAttribute("aria-valuemin"), "0"); assert.equal(h.getAttribute("aria-valuemax"), "240"); }
p.set("i", [0, 200]);
assert.deepEqual(p.params.i, [0, 200]);
// The markup path agrees: one meta derivation for every entry point.
const host = document.createElement("div");
host.innerHTML = '<div data-tw="slider" data-value="50"></div>';
Expand All @@ -38,6 +43,11 @@ test("a verbose slider / interval without min/max derives its range from the val
assert.equal(m.getAttribute("aria-valuenow"), "50");
assert.equal(m.getAttribute("aria-valuemax"), "150");
host.remove();
const ih = document.createElement("div");
ih.innerHTML = '<div data-tw="interval" data-value="20,80"></div>';
document.body.append(ih); enhance(ih);
for (const h of ih.querySelectorAll("[role=slider]")) assert.equal(h.getAttribute("aria-valuemax"), "240");
ih.remove();
});

test("an explicit min/max still wins over the derived range", () => {
Expand Down