╭ changed ──────────╮ │src/embed/script.js│ ╰───────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 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 } │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─────────────╮ │src/ui/Repo/Issues.zig│ ╰──────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1717 .arrow_left => if (cur > button_in_row_index) root_focus.setFocus(row.children.keys()[cur - 1]) else try self.focusList(index, root_focus),│ │1718 .arrow_right => if (cur + 1 < row.children.count()) root_focus.setFocus(row.children.keys()[cur + 1]), │ │1719 .arrow_up => self.focusHeader(root_focus), │ │1720 - .arrow_down => self.focusTitle(index, root_focus), │ │1720 + .arrow_down => { │ │1721 + self.focusDetailEdge(index, root_focus, false); │ │1722 + self.detailScroll(index).y = 0; │ │1723 + }, │ │1724 .enter => if (on_status) try self.toggleIssueStatus(allocator, index), │ │1725 .mouse => |mouse| if (self.statusButton(index)) |button| { │ │1726 if (inp.leftClickOn(root_focus, button.getFocus().id, mouse)) try self.toggleIssueStatus(allocator, index); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1733 switch (key) { │ │1734 .arrow_left => try self.focusList(index, root_focus), │ │1735 .arrow_up => self.focusToolRow(index, root_focus), │ │1733 - .arrow_down => if (self.authorPresent(index)) self.focusAuthor(index, root_focus) else try self.focusDescription(index, root_focus),│ │1736 + .arrow_down => _ = self.moveDetailVertical(index, root_focus, true), │ │1737 else => {}, │ │1738 } │ │1739 } │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1743 fn authorInput(self: *View, index: usize, key: Key, root_focus: *Focus) !void { │ │1744 switch (key) { │ │1745 .arrow_left => try self.focusList(index, root_focus), │ │1743 - .arrow_up => self.focusTitle(index, root_focus), │ │1744 - .arrow_down => if (self.tagFlow(index) != null) try self.focusTags(index, root_focus) else try self.focusDescription(index, root_focus),│ │1746 + .arrow_up => _ = self.moveDetailVertical(index, root_focus, false), │ │1747 + .arrow_down => _ = self.moveDetailVertical(index, root_focus, true), │ │1748 else => {}, │ │1749 } │ │1750 } │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────╮ │1753 const sc = self.detailScroll(index); │ │1754 switch (key) { │ │1755 .arrow_left => return self.focusList(index, root_focus), │ │1753 - // once the scroll can't move further, cross into the tags (or the │ │1754 - // open/close button when the issue has none). │ │1756 + // the terminal moves through the description one line at a time; │ │1757 + // the web lets focus changes drive its native scroll. │ │1758 .arrow_up => { │ │1759 + if (!self.session.is_terminal) { │ │1760 + _ = self.moveDetailVertical(index, root_focus, false); │ │1761 + return; │ │1762 + } │ │1763 + if (self.focusAdjacentVisible(index, root_focus, false)) return;│ │1764 const before = sc.y; │ │1765 sc.y -= 1; │ │1766 sc.clampToContent(); │ ╰──────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────╮ │1759 - if (sc.y == before) { │ │1760 - if (self.tagFlow(index) != null) │ │1761 - try self.focusTags(index, root_focus) │ │1762 - else if (self.authorPresent(index)) │ │1763 - self.focusAuthor(index, root_focus) │ │1764 - else │ │1765 - self.focusTitle(index, root_focus); │ │1766 - } │ │1767 + if (sc.y == before) _ = self.moveDetailVertical(index, root_focus, false);│ │1768 return; │ │1769 }, │ │1770 .arrow_down => { │ ╰────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────╮ │1770 - if (self.moveCommentVertical(index, root_focus, true)) return; │ │1771 + if (!self.session.is_terminal) { │ │1772 + _ = self.moveDetailVertical(index, root_focus, true); │ │1773 + return; │ │1774 + } │ │1775 + if (self.focusAdjacentVisible(index, root_focus, true)) return;│ │1776 const before = sc.y; │ │1777 sc.y += 1; │ │1778 sc.clampToContent(); │ ╰─────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯