SelectField and the refresh-interval picker are built on a new Listbox, a select-only combobox: a button that keeps focus and a listbox in the top layer, so it works the same in every current browser, is not clipped by a scrolling ancestor and shows above a modal dialog. Keys, type-ahead and placement are pure functions with tests. required becomes aria-required, as a button has no native form validation.
113 lines
3.5 KiB
JavaScript
113 lines
3.5 KiB
JavaScript
// What the styled dropdown's keys and placement do. Run by `npm test`.
|
|
import assert from "node:assert/strict";
|
|
import { test } from "node:test";
|
|
import {
|
|
indexOfValue,
|
|
moveIndex,
|
|
nextQuery,
|
|
placePopup,
|
|
TYPE_AHEAD_MS,
|
|
typeAhead,
|
|
} from "../src/ui/select-field/select-nav.ts";
|
|
|
|
const opts = ["alpha", "beta", "bravo", "charlie", "Delta"].map((label) => ({
|
|
value: label.toLowerCase(),
|
|
label,
|
|
}));
|
|
|
|
test("the option holding a value is found, or -1", () => {
|
|
assert.equal(indexOfValue(opts, "bravo"), 2);
|
|
assert.equal(indexOfValue(opts, "nope"), -1);
|
|
});
|
|
|
|
test("arrow keys step and stop at the ends; Home and End jump", () => {
|
|
assert.equal(moveIndex(1, 5, "ArrowDown"), 2);
|
|
assert.equal(moveIndex(4, 5, "ArrowDown"), 4, "stops at the last");
|
|
assert.equal(moveIndex(1, 5, "ArrowUp"), 0);
|
|
assert.equal(moveIndex(0, 5, "ArrowUp"), 0, "stops at the first");
|
|
assert.equal(moveIndex(2, 5, "Home"), 0);
|
|
assert.equal(moveIndex(2, 5, "End"), 4);
|
|
assert.equal(moveIndex(0, 30, "PageDown"), 10);
|
|
assert.equal(moveIndex(25, 30, "PageDown"), 29);
|
|
assert.equal(moveIndex(3, 30, "PageUp"), 0);
|
|
});
|
|
|
|
test("a key that moves nothing, or no options, leave the active one alone", () => {
|
|
assert.equal(moveIndex(2, 5, "a"), 2);
|
|
assert.equal(moveIndex(0, 0, "ArrowDown"), -1);
|
|
assert.equal(
|
|
moveIndex(-1, 3, "ArrowDown"),
|
|
0,
|
|
"from nothing active, the first",
|
|
);
|
|
});
|
|
|
|
test("typing a letter steps through the options starting with it, wrapping", () => {
|
|
assert.equal(typeAhead(opts, 0, "b"), 1);
|
|
assert.equal(typeAhead(opts, 1, "b"), 2);
|
|
assert.equal(typeAhead(opts, 2, "b"), 1, "wraps round to the first b");
|
|
assert.equal(typeAhead(opts, 0, "d"), 4, "case does not matter");
|
|
assert.equal(typeAhead(opts, 0, "z"), -1);
|
|
});
|
|
|
|
test("the same letter typed again steps on; a longer word narrows in place", () => {
|
|
assert.equal(typeAhead(opts, 1, "bb"), 2, "bb is b again");
|
|
assert.equal(typeAhead(opts, 1, "br"), 2);
|
|
assert.equal(typeAhead(opts, 2, "br"), 2, "stays while it still matches");
|
|
assert.equal(typeAhead(opts, 0, ""), -1);
|
|
assert.equal(typeAhead([], 0, "a"), -1);
|
|
});
|
|
|
|
test("a short pause continues the word, a long one starts a new one", () => {
|
|
assert.equal(nextQuery("b", "r", 100), "br");
|
|
assert.equal(nextQuery("br", "c", TYPE_AHEAD_MS + 1), "c");
|
|
});
|
|
|
|
const viewport = { width: 1000, height: 800 };
|
|
|
|
test("the list opens under the control when it fits", () => {
|
|
const p = placePopup(
|
|
{ top: 100, bottom: 140, left: 50, width: 200 },
|
|
viewport,
|
|
120,
|
|
);
|
|
assert.deepEqual(p, { top: 144, left: 50, width: 200, maxHeight: 120 });
|
|
});
|
|
|
|
test("it opens above when under is too short and above has more room", () => {
|
|
const p = placePopup(
|
|
{ top: 700, bottom: 740, left: 50, width: 200 },
|
|
viewport,
|
|
300,
|
|
);
|
|
assert.equal(p.maxHeight, 300);
|
|
assert.equal(p.top, 700 - 4 - 300);
|
|
});
|
|
|
|
test("it scrolls under the control when neither side fits it", () => {
|
|
const p = placePopup(
|
|
{ top: 100, bottom: 140, left: 50, width: 200 },
|
|
{ width: 1000, height: 300 },
|
|
900,
|
|
);
|
|
// 300 - 140 - 8 - 4 under; 100 - 12 above: under has more here.
|
|
assert.equal(p.top, 144);
|
|
assert.equal(p.maxHeight, 148);
|
|
});
|
|
|
|
test("it is at least the minimum width and stays inside the viewport", () => {
|
|
const p = placePopup(
|
|
{ top: 100, bottom: 140, left: 960, width: 30 },
|
|
viewport,
|
|
100,
|
|
120,
|
|
);
|
|
assert.equal(p.width, 120);
|
|
assert.equal(p.left, 1000 - 8 - 120);
|
|
const q = placePopup(
|
|
{ top: 0, bottom: 40, left: -20, width: 100 },
|
|
viewport,
|
|
50,
|
|
);
|
|
assert.equal(q.left, 8);
|
|
});
|