From 413fbc6da3d6d570b4d459ccc0fe347bba5ddb32 Mon Sep 17 00:00:00 2001 From: Garri Figueroa Date: Sat, 15 Aug 2026 19:14:27 -0400 Subject: [PATCH] Keep the sidebar within the viewport The sidebar is a grid item spanning all four rows, so `block-size: 100%` resolves against a grid area as tall as the whole document. Its `overflow` never kicks in, and the absolutely positioned table of contents inside it stretches to the bottom of the page. In a long book that leaves the page scrollbar as the only way to reach the end of the table of contents, and scrolling the text drags the navigation out of view. Stick the sidebar to the viewport and bound it to `100dvh` so the table of contents scrolls its own content instead. --- app/assets/stylesheets/layout.css | 10 +++++--- test/system/sidebar_scroll_test.rb | 41 ++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 test/system/sidebar_scroll_test.rb diff --git a/app/assets/stylesheets/layout.css b/app/assets/stylesheets/layout.css index 8450fab4..351ff9c3 100644 --- a/app/assets/stylesheets/layout.css +++ b/app/assets/stylesheets/layout.css @@ -93,15 +93,19 @@ body { } } +/* The sidebar spans all four grid rows, so sizing it against its grid area makes + it as tall as the whole document and its overflow never kicks in. Stick it to + the viewport instead, so the table of contents scrolls its own content. */ :where(#sidebar) { background-color: var(--color-subtle-light); - block-size: 100%; + block-size: 100dvh; font-size: var(--font-medium-responsive); grid-area: sidebar; inline-size: 25vw; - max-block-size: 100%; + inset-block-start: 0; + max-block-size: 100dvh; overflow: auto; - position: relative; + position: sticky; transition: margin-inline-start 0.2s ease-out; :has(#sidebar-toggle:checked) & { diff --git a/test/system/sidebar_scroll_test.rb b/test/system/sidebar_scroll_test.rb new file mode 100644 index 00000000..ab7b9c66 --- /dev/null +++ b/test/system/sidebar_scroll_test.rb @@ -0,0 +1,41 @@ +require "application_system_test_case" + +class SidebarScrollTest < ApplicationSystemTestCase + setup do + sign_in "kevin@example.com" + + leaves(:welcome_page).leafable.update! body: ([ "A paragraph of the book." ] * 400).join("\n\n") + + visit leafable_slug_path(leaves(:welcome_page)) + find("label.sidebar__toggle").click + + assert_operator document_height, :>, viewport_height * 2, + "the test page should be considerably taller than the viewport" + end + + test "the table of contents fits the viewport instead of the page" do + assert_operator sidebar_height, :<=, viewport_height, + "the sidebar is #{sidebar_height}px tall in a #{viewport_height}px viewport: " \ + "it grows with the length of the page instead of staying within the screen" + end + + test "the table of contents stays put while the page scrolls" do + execute_script "window.scrollTo(0, document.documentElement.scrollHeight)" + + assert_equal 0, sidebar_top.round, + "the sidebar scrolled away with the page instead of staying in view" + end + + private + def viewport_height = evaluate_script("window.innerHeight") + + def document_height = evaluate_script("document.documentElement.scrollHeight") + + def sidebar_height = sidebar_rect["height"] + + def sidebar_top = sidebar_rect["top"] + + def sidebar_rect + evaluate_script("document.querySelector('#sidebar').getBoundingClientRect().toJSON()") + end +end