╭ changed ──────────╮ │src/embed/script.js│ ╰───────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 51 function readWasmString(ptr, len) { │ │ 52 const memory = wasmInstance.exports.memory; │ │ 53 return decoder.decode(new Uint8Array(memory.buffer, ptr, len)); │ │ 54 +} │ │ 55 + │ │ 56 +function focusedNativeScroll() { │ │ 57 + const id = grid.dataset.focusId; │ │ 58 + if (!id) return null; │ │ 59 + const focused = grid.querySelectorAll(`[data-focus-id="${id}"]`); │ │ 60 + if (focused.length === 0) return null; │ │ 61 + const container = focused[0].closest(".scroll"); │ │ 62 + return container ? { focused, container } : null; │ │ 63 +} │ │ 64 + │ │ 65 +// scroll one row while the focused widget continues past the viewport edge. │ │ 66 +function scrollFocusedWidget(down) { │ │ 67 + const native = focusedNativeScroll(); │ │ 68 + if (!native) return false; │ │ 69 + const viewport = native.container.getBoundingClientRect(); │ │ 70 + const edge = (down ? native.focused[native.focused.length - 1] : native.focused[0]).getBoundingClientRect();│ │ 71 + const overflow = down ? edge.bottom - viewport.bottom : viewport.top - edge.top; │ │ 72 + if (overflow <= 0.5) return false; │ │ 73 + if (!cellHeight) measureCell(); │ │ 74 + native.container.scrollTop += (down ? 1 : -1) * Math.min(cellHeight, overflow); │ │ 75 + return true; │ │ 76 } │ │ 77 │ │ 78 +// page the focused native scroll, then focus its leading visible widget. │ │ 79 +function pageFocusedScroll(down) { │ │ 80 + const native = focusedNativeScroll(); │ │ 81 + if (!native) return false; │ │ 82 + const container = native.container; │ │ 83 + │ │ 84 + container.scrollTop += (down ? 1 : -1) * container.clientHeight; │ │ 85 + const viewport = container.getBoundingClientRect(); │ │ 86 + let chosen = null; │ │ 87 + for (const el of container.querySelectorAll(".clickable[data-focus-id]")) { │ │ 88 + const rect = el.getBoundingClientRect(); │ │ 89 + if (rect.bottom <= viewport.top || rect.top >= viewport.bottom) continue; │ │ 90 + chosen = el; │ │ 91 + if (!down) break; │ │ 92 + } │ │ 93 + if (chosen) wasmInstance.exports._setFocus(Number(chosen.dataset.focusId)); │ │ 94 + wasmInstance.exports._tick(minRows(), maxCols()); │ │ 95 + return true; │ │ 96 +} │ │ 97 + │ │ 98 const importObject = { │ │ 99 env: { │ │100 _consoleLog: function (ptr, len) { │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────╮ │165 window.location.assign(readWasmString(ptr, len)); │ │166 }, │ │167 _scrollToFocus: function (id) { │ │168 + grid.dataset.focusId = id; │ │169 // bring the focused widget into view within its own .scroll div. it's │ │170 // several spans (one per row) sharing this id, in top-to-bottom order,│ │171 // so the first gives its top/left edge and the last its bottom. │ ╰────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │181 const bottom = last.bottom - c.top + container.scrollTop; │ │182 const left = first.left - c.left + container.scrollLeft; │ │183 │ │141 - // vertical is the priority axis: fit the widget, preferring its top │ │142 - // when it's taller than the viewport. │ │143 - if (top < container.scrollTop || bottom - top > container.clientHeight) { │ │184 + // vertical is the priority axis. keep the viewport in place when it │ │185 + // already overlaps a tall widget; otherwise reveal its nearest edge. │ │186 + if (bottom - top > container.clientHeight) { │ │187 + container.scrollTop = Math.min(Math.max(container.scrollTop, top), bottom - container.clientHeight);│ │188 + } else if (top < container.scrollTop) { │ │189 container.scrollTop = top; │ │190 } else if (bottom > container.scrollTop + container.clientHeight) { │ │191 container.scrollTop = bottom - container.clientHeight; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │260 // arrows — handled natively by the browser │ │261 return; │ │262 } │ │263 + } │ │264 + if ((event.key === "ArrowUp" || event.key === "ArrowDown") && scrollFocusedWidget(event.key === "ArrowDown")) { │ │265 + event.preventDefault(); │ │266 + return; │ │267 } │ │268 + if ((event.key === "PageUp" || event.key === "PageDown") && pageFocusedScroll(event.key === "PageDown")) { │ │269 + event.preventDefault(); │ │270 + return; │ │271 + } │ │272 if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "PageUp", "PageDown", "Home", "End"].includes(event.key)) {│ │273 event.preventDefault(); │ │274 } │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯