Skip to content

Commit 0805e07

Browse files
committed
feat(ribbon): basic implementation for scroll pinning
1 parent 6b059a9 commit 0805e07

File tree

3 files changed

+65
-4
lines changed

3 files changed

+65
-4
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
.content-header-widget {
2+
position: relative;
3+
transition: position 0.3s ease, box-shadow 0.3s ease, z-index 0.3s ease;
4+
z-index: 8;
5+
}
6+
7+
.content-header-widget.floating {
8+
position: sticky;
9+
top: 0;
10+
z-index: 11;
11+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
12+
background-color: var(--main-background-color, #fff);
13+
}
14+
15+
/* Ensure content inside doesn't get affected by the floating state */
16+
.content-header-widget > * {
17+
transition: inherit;
18+
}

apps/client/src/widgets/containers/content_header.ts

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,19 @@ import { EventData } from "../../components/app_context";
22
import BasicWidget from "../basic_widget";
33
import Container from "./container";
44
import NoteContext from "../../components/note_context";
5+
import "./content_header.css";
56

67
export default class ContentHeader extends Container<BasicWidget> {
7-
8+
89
noteContext?: NoteContext;
910
thisElement?: HTMLElement;
1011
parentElement?: HTMLElement;
1112
resizeObserver: ResizeObserver;
1213
currentHeight: number = 0;
1314
currentSafeMargin: number = NaN;
15+
previousScrollTop: number = 0;
16+
isFloating: boolean = false;
17+
scrollThreshold: number = 10; // pixels before triggering float
1418

1519
constructor() {
1620
super();
@@ -35,7 +39,36 @@ export default class ContentHeader extends Container<BasicWidget> {
3539
this.thisElement = this.$widget.get(0)!;
3640

3741
this.resizeObserver.observe(this.thisElement);
38-
this.parentElement.addEventListener("scroll", this.updateSafeMargin.bind(this));
42+
this.parentElement.addEventListener("scroll", this.updateScrollState.bind(this), { passive: true });
43+
}
44+
45+
updateScrollState() {
46+
const currentScrollTop = this.parentElement!.scrollTop;
47+
const isScrollingUp = currentScrollTop < this.previousScrollTop;
48+
const hasMovedEnough = Math.abs(currentScrollTop - this.previousScrollTop) > this.scrollThreshold;
49+
50+
if (hasMovedEnough) {
51+
this.setFloating(isScrollingUp);
52+
this.previousScrollTop = currentScrollTop;
53+
}
54+
55+
this.updateSafeMargin();
56+
}
57+
58+
setFloating(shouldFloat: boolean) {
59+
if (shouldFloat !== this.isFloating) {
60+
this.isFloating = shouldFloat;
61+
62+
if (shouldFloat) {
63+
this.$widget.addClass("floating");
64+
// Set CSS variable so ribbon can position itself below the floating header
65+
this.parentElement!.style.setProperty("--content-header-height", `${this.currentHeight}px`);
66+
} else {
67+
this.$widget.removeClass("floating");
68+
// Reset CSS variable when header is not floating
69+
this.parentElement!.style.removeProperty("--content-header-height");
70+
}
71+
}
3972
}
4073

4174
updateSafeMargin() {
@@ -60,4 +93,4 @@ export default class ContentHeader extends Container<BasicWidget> {
6093
}
6194
}
6295

63-
}
96+
}

apps/client/src/widgets/ribbon/style.css

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
.ribbon-container {
22
margin-bottom: 5px;
3+
position: relative;
4+
transition: position 0.3s ease, z-index 0.3s ease, top 0.3s ease;
5+
z-index: 8;
6+
}
7+
8+
/* When content header is floating, ribbon sticks below it */
9+
.scrolling-container:has(.content-header-widget.floating) .ribbon-container {
10+
position: sticky;
11+
top: var(--content-header-height, 100px);
12+
z-index: 10;
313
}
414

515
.ribbon-top-row {
@@ -404,4 +414,4 @@ body[dir=rtl] .attribute-list-editor {
404414
background-color: transparent !important;
405415
pointer-events: none; /* makes it unclickable */
406416
}
407-
/* #endregion */
417+
/* #endregion */

0 commit comments

Comments
 (0)