feat(ribbon): basic implementation for scroll pinning

This commit is contained in:
Elian Doran 2025-12-09 08:18:27 +02:00
parent 6b059a9a75
commit 0805e077a1
No known key found for this signature in database
3 changed files with 65 additions and 4 deletions

View File

@ -0,0 +1,18 @@
.content-header-widget {
position: relative;
transition: position 0.3s ease, box-shadow 0.3s ease, z-index 0.3s ease;
z-index: 8;
}
.content-header-widget.floating {
position: sticky;
top: 0;
z-index: 11;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
background-color: var(--main-background-color, #fff);
}
/* Ensure content inside doesn't get affected by the floating state */
.content-header-widget > * {
transition: inherit;
}

View File

@ -2,15 +2,19 @@ import { EventData } from "../../components/app_context";
import BasicWidget from "../basic_widget";
import Container from "./container";
import NoteContext from "../../components/note_context";
import "./content_header.css";
export default class ContentHeader extends Container<BasicWidget> {
noteContext?: NoteContext;
thisElement?: HTMLElement;
parentElement?: HTMLElement;
resizeObserver: ResizeObserver;
currentHeight: number = 0;
currentSafeMargin: number = NaN;
previousScrollTop: number = 0;
isFloating: boolean = false;
scrollThreshold: number = 10; // pixels before triggering float
constructor() {
super();
@ -35,7 +39,36 @@ export default class ContentHeader extends Container<BasicWidget> {
this.thisElement = this.$widget.get(0)!;
this.resizeObserver.observe(this.thisElement);
this.parentElement.addEventListener("scroll", this.updateSafeMargin.bind(this));
this.parentElement.addEventListener("scroll", this.updateScrollState.bind(this), { passive: true });
}
updateScrollState() {
const currentScrollTop = this.parentElement!.scrollTop;
const isScrollingUp = currentScrollTop < this.previousScrollTop;
const hasMovedEnough = Math.abs(currentScrollTop - this.previousScrollTop) > this.scrollThreshold;
if (hasMovedEnough) {
this.setFloating(isScrollingUp);
this.previousScrollTop = currentScrollTop;
}
this.updateSafeMargin();
}
setFloating(shouldFloat: boolean) {
if (shouldFloat !== this.isFloating) {
this.isFloating = shouldFloat;
if (shouldFloat) {
this.$widget.addClass("floating");
// Set CSS variable so ribbon can position itself below the floating header
this.parentElement!.style.setProperty("--content-header-height", `${this.currentHeight}px`);
} else {
this.$widget.removeClass("floating");
// Reset CSS variable when header is not floating
this.parentElement!.style.removeProperty("--content-header-height");
}
}
}
updateSafeMargin() {
@ -60,4 +93,4 @@ export default class ContentHeader extends Container<BasicWidget> {
}
}
}
}

View File

@ -1,5 +1,15 @@
.ribbon-container {
margin-bottom: 5px;
position: relative;
transition: position 0.3s ease, z-index 0.3s ease, top 0.3s ease;
z-index: 8;
}
/* When content header is floating, ribbon sticks below it */
.scrolling-container:has(.content-header-widget.floating) .ribbon-container {
position: sticky;
top: var(--content-header-height, 100px);
z-index: 10;
}
.ribbon-top-row {
@ -404,4 +414,4 @@ body[dir=rtl] .attribute-list-editor {
background-color: transparent !important;
pointer-events: none; /* makes it unclickable */
}
/* #endregion */
/* #endregion */