Skip to content

Commit 64e15a0

Browse files
committed
Final fixes to ToC scrolling
1 parent bc4c812 commit 64e15a0

4 files changed

Lines changed: 63 additions & 42 deletions

File tree

‎_TODO.md‎

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -145,16 +145,6 @@ https://mermaid.js.org/config/directives.html
145145

146146
See comp images in Breadcrumbs component directory
147147

148-
## Keyboard Navigation
149-
150-
- Should header links be in the tab order?
151-
- Does the total page tab order make sense?
152-
153-
154-
## Non-featured tags
155-
156-
Generate cover images for the skills and technologies tags pages
157-
158148
## Code Block Problems
159149

160150
- Inline code blocks are not wrapping. They're breaking to a new line. An example is in the "Scenario: CRD Sync Order Problem" section of `argocd-sync-failures-gitops-debugging-troubleshooting#specific-failure-scenarios`.

‎src/components/Content/ProgressBar/index.astro‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,15 @@
1010
---
1111

1212
<reading-progress-bar
13-
class="fixed left-0 right-0 block h-1 sm:h-2"
13+
class="fixed left-0 right-0 block h-1.5 sm:h-2"
1414
style="top: var(--layout-top-offset, 0); z-index: var(--z-nav);"
1515
data-progress-bar
1616
aria-hidden="true"
1717
>
1818
<progress
1919
max="100"
2020
value="0"
21-
class="block h-full w-full appearance-none [&::-webkit-progress-bar]:bg-transparent [&::-webkit-progress-value]:bg-success [&::-moz-progress-bar]:bg-success"
21+
class="block h-full w-full appearance-none [&::-webkit-progress-bar]:bg-transparent [&::-webkit-progress-value]:bg-success [&::-moz-progress-bar]:bg-success text-[8px]"
2222
></progress>
2323
</reading-progress-bar>
2424

‎src/components/Icon/icons/location.astro‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,6 @@ const { color, classes, size, accessible, focusable, isListMarker, id } = Astro.
2525
{accessible && <title>location icon</title>}
2626
<path
2727
style="stroke-width:0.308"
28-
d="m 11.999997,2.1538483 c -4.0781539,0 -7.3846159,3.3064621 -7.3846159,7.3846163 0,1.6058464 0.5264616,3.0793844 1.4015387,4.2861534 0.015692,0.02892 0.018153,0.06123 0.036,0.08892 l 4.9230772,7.384614 c 0.228308,0.342462 0.612923,0.548 1.024,0.548 0.411077,0 0.795693,-0.205538 1.024,-0.548 l 4.923079,-7.384614 c 0.01814,-0.02769 0.02031,-0.06 0.036,-0.08892 0.875081,-1.206769 1.401543,-2.680307 1.401543,-4.2861539 0,-4.0781543 -3.306462,-7.3846162 -7.384617,-7.3846162 z m 0,9.8461547 c -1.359384,0 -2.4615383,-1.102154 -2.4615383,-2.4615384 0,-1.3593849 1.1021543,-2.4615387 2.4615383,-2.4615387 1.359385,0 2.461539,1.1021538 2.461539,2.4615387 0,1.3593844 -1.102154,2.4615384 -2.461539,2.4615384 z"
28+
d="m 9.4322381,2.705524 c -4.0781543,0 -7.3846163,3.3064621 -7.3846163,7.384616 0,1.605847 0.5264616,3.079385 1.4015387,4.286154 0.015692,0.02892 0.018153,0.06123 0.036,0.08892 l 4.9230772,7.384614 c 0.228308,0.342462 0.612923,0.548 1.0240004,0.548 0.411077,0 0.7956929,-0.205538 1.0239999,-0.548 l 4.923079,-7.384614 c 0.01814,-0.02769 0.02031,-0.06 0.036,-0.08892 0.875081,-1.206769 1.401543,-2.680307 1.401543,-4.286154 0,-4.0781545 -3.306462,-7.3846164 -7.3846169,-7.3846164 z m 0,9.846155 c -1.3593844,0 -2.4615387,-1.102154 -2.4615387,-2.461539 0,-1.3593846 1.1021543,-2.4615384 2.4615387,-2.4615384 1.3593849,0 2.4615389,1.1021538 2.4615389,2.4615384 0,1.359385 -1.102154,2.461539 -2.4615389,2.461539 z"
2929
/>
3030
</svg>

‎src/components/scripts/stickySidebar.ts‎

Lines changed: 60 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,47 @@
55
* Handles both short sidebars (smaller than viewport) and tall sidebars
66
* (taller than viewport) with different sticking strategies:
77
*
8-
* SHORT SIDEBAR: Pins the sidebar's top edge below the scroll viewport's
9-
* top edge (with padding) as the user scrolls down. Returns to natural
10-
* position when the container scrolls back into view.
8+
* SHORT SIDEBAR: Pins the sidebar's top edge below the fixed header
9+
* (with padding) as the user scrolls down. Returns to natural position
10+
* when the container scrolls back into view.
1111
*
1212
* TALL SIDEBAR: When scrolling down, allows the sidebar to scroll naturally
1313
* until its bottom edge reaches the viewport bottom (with padding), then
1414
* pins it there. When scrolling up, releases and scrolls naturally until
15-
* the top edge reaches the viewport top (with padding), then pins at top.
15+
* the top edge reaches the header bottom (with padding), then pins at top.
1616
*
17-
* The scroll container is always `#scroll-viewport`, which sits below the
18-
* header/progress bar in the flex layout. Its BCR top naturally accounts
19-
* for all fixed chrome above content.
17+
* Measures the actual `.header-fixed` and `[data-progress-bar]` elements
18+
* each frame so the reference point always matches the visual chrome,
19+
* regardless of header collapse animations or transition timing.
2020
*/
2121
import { handleScriptError } from '@components/scripts/errors/handler'
22+
import {
23+
getHeaderFixedElement,
24+
getProgressBarElement,
25+
} from '@components/scripts/store/selectors'
2226

2327
export interface StickySidebarOptions {
24-
/** Extra space below the scroll viewport's visible top edge (px). Default: 16 */
28+
/** Extra space below the fixed chrome (header + progress bar) in px. Default: 16 */
2529
topPadding?: number
26-
/** Extra space above the scroll viewport's visible bottom edge (px). Default: 16 */
30+
/** Extra space above the viewport bottom edge in px. Default: 16 */
2731
bottomPadding?: number
2832
/** Minimum window width to enable sticky behavior (px). Default: 1024 (lg breakpoint) */
2933
minWidth?: number
3034
}
3135

36+
/**
37+
* Compute the visual bottom of all fixed chrome above content.
38+
* Returns the maximum of header-fixed and progress-bar bottoms.
39+
*/
40+
function measureChromeBottom(): number {
41+
const headerEl = getHeaderFixedElement()
42+
const progressEl = getProgressBarElement()
43+
return Math.max(
44+
headerEl ? headerEl.getBoundingClientRect().bottom : 0,
45+
progressEl ? progressEl.getBoundingClientRect().bottom : 0,
46+
)
47+
}
48+
3249
/**
3350
* Initialize sticky sidebar behavior on a sidebar element within a container.
3451
*
@@ -54,8 +71,8 @@ export function initStickySidebar(
5471
}
5572

5673
let currentTranslateY = 0
74+
let prevScrollTop = scrollContainer.scrollTop
5775
let rafId: number | null = null
58-
// Capture scrollContainer in a const to satisfy strict null checks inside closures
5976
const scroller = scrollContainer
6077

6178
/**
@@ -70,47 +87,61 @@ export function initStickySidebar(
7087
sidebar.style.transform = ''
7188
currentTranslateY = 0
7289
}
90+
prevScrollTop = scroller.scrollTop
7391
return
7492
}
7593

94+
const scrollTop = scroller.scrollTop
95+
const scrollDelta = scrollTop - prevScrollTop
96+
prevScrollTop = scrollTop
97+
7698
const sidebarRect = sidebar.getBoundingClientRect()
7799
const sidebarHeight = sidebarRect.height
78100
if (sidebarHeight === 0) return
79101

80102
const containerRect = container.getBoundingClientRect()
81-
const viewportRect = scroller.getBoundingClientRect()
82103

83-
// Sidebar's natural top position (without current transform applied)
104+
// Visible bounds: measured from actual fixed chrome, not scroll viewport BCR
105+
const visibleTop = measureChromeBottom() + topPadding
106+
const visibleBottom = window.innerHeight - bottomPadding
107+
const availableHeight = visibleBottom - visibleTop
108+
109+
// Sidebar's natural top position (where it would be with translateY = 0)
84110
const naturalTop = sidebarRect.top - currentTranslateY
85111

86112
// Maximum translateY: sidebar bottom must not exceed container bottom
87113
const maxTranslateY = Math.max(0, containerRect.bottom - (naturalTop + sidebarHeight))
88114

89-
// Visible bounds within the scroll viewport
90-
const visibleTop = viewportRect.top + topPadding
91-
const visibleBottom = viewportRect.bottom - bottomPadding
92-
const availableHeight = visibleBottom - visibleTop
93-
94115
let newTranslateY: number
95116

96117
if (sidebarHeight <= availableHeight) {
97118
// SHORT SIDEBAR: pin top edge at the visible top boundary
98119
newTranslateY = Math.max(0, visibleTop - naturalTop)
99120
} else {
100-
// TALL SIDEBAR: direction-aware pinning.
101-
// Top pin: sidebar top aligns with visibleTop
102-
const topPin = visibleTop - naturalTop
103-
// Bottom pin: sidebar bottom aligns with visibleBottom
104-
const bottomPin = visibleBottom - sidebarHeight - naturalTop
105-
106-
// When the sidebar is taller than available space, topPin > bottomPin.
107-
// Clamping currentTranslateY between them gives us direction-aware
108-
// behavior: scrolling down hits the bottom pin, scrolling up hits the
109-
// top pin, and in between the sidebar scrolls naturally.
110-
newTranslateY = Math.max(bottomPin, Math.min(currentTranslateY, topPin))
121+
// TALL SIDEBAR: direction-aware pinning
122+
const actualTop = naturalTop + currentTranslateY
123+
const actualBottom = actualTop + sidebarHeight
124+
125+
if (scrollDelta > 0) {
126+
// Scrolling DOWN: pin bottom at visibleBottom when it would go above
127+
if (actualBottom < visibleBottom) {
128+
newTranslateY = visibleBottom - sidebarHeight - naturalTop
129+
} else {
130+
newTranslateY = currentTranslateY
131+
}
132+
} else if (scrollDelta < 0) {
133+
// Scrolling UP: pin top at visibleTop when it would go below
134+
if (actualTop > visibleTop) {
135+
newTranslateY = visibleTop - naturalTop
136+
} else {
137+
newTranslateY = currentTranslateY
138+
}
139+
} else {
140+
newTranslateY = currentTranslateY
141+
}
111142
}
112143

113-
// Global clamp: never go above natural position or below container bottom
144+
// Clamp: never go above natural position or below container bottom
114145
newTranslateY = Math.max(0, Math.min(newTranslateY, maxTranslateY))
115146

116147
// Apply only when value changes meaningfully (avoid sub-pixel jitter)

0 commit comments

Comments
 (0)