From 38dba60ceb11d24d796df61416552abfc7190776 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Thu, 26 Mar 2026 07:36:32 -0700 Subject: [PATCH] Enhancement: auto-hide the search bar on mobile (#12404) --- .../app-frame/app-frame.component.html | 7 +-- .../app-frame/app-frame.component.scss | 17 ++++++ .../app-frame/app-frame.component.spec.ts | 53 +++++++++++++++++++ .../app-frame/app-frame.component.ts | 40 ++++++++++++++ .../document-list.component.scss | 9 +++- 5 files changed, 122 insertions(+), 4 deletions(-) diff --git a/src-ui/src/app/components/app-frame/app-frame.component.html b/src-ui/src/app/components/app-frame/app-frame.component.html index d876e28ea..11a4aefe2 100644 --- a/src-ui/src/app/components/app-frame/app-frame.component.html +++ b/src-ui/src/app/components/app-frame/app-frame.component.html @@ -1,7 +1,7 @@
-
+
@@ -378,7 +379,7 @@
-
diff --git a/src-ui/src/app/components/app-frame/app-frame.component.scss b/src-ui/src/app/components/app-frame/app-frame.component.scss index 77bf9bf21..c069eeefc 100644 --- a/src-ui/src/app/components/app-frame/app-frame.component.scss +++ b/src-ui/src/app/components/app-frame/app-frame.component.scss @@ -44,6 +44,23 @@ .sidebar { top: 3.5rem; } + + .search-container { + max-height: 4.5rem; + overflow: hidden; + transition: max-height .2s ease, opacity .2s ease, padding-top .2s ease, padding-bottom .2s ease; + + &.mobile-hidden { + max-height: 0; + opacity: 0; + padding-top: 0 !important; + padding-bottom: 0 !important; + } + } + + main.mobile-search-hidden { + padding-top: 56px; + } } main { diff --git a/src-ui/src/app/components/app-frame/app-frame.component.spec.ts b/src-ui/src/app/components/app-frame/app-frame.component.spec.ts index 931f46254..1341c6e5a 100644 --- a/src-ui/src/app/components/app-frame/app-frame.component.spec.ts +++ b/src-ui/src/app/components/app-frame/app-frame.component.spec.ts @@ -293,6 +293,59 @@ describe('AppFrameComponent', () => { expect(component.isMenuCollapsed).toBeTruthy() }) + it('should hide mobile search when scrolling down and show it when scrolling up', () => { + Object.defineProperty(globalThis, 'innerWidth', { + value: 767, + }) + + component.ngOnInit() + + Object.defineProperty(globalThis, 'scrollY', { + configurable: true, + value: 40, + }) + component.onWindowScroll() + expect(component.mobileSearchHidden).toBe(true) + + Object.defineProperty(globalThis, 'scrollY', { + configurable: true, + value: 0, + }) + component.onWindowScroll() + expect(component.mobileSearchHidden).toBe(false) + }) + + it('should keep mobile search visible on desktop scroll or resize', () => { + Object.defineProperty(globalThis, 'innerWidth', { + value: 1024, + }) + component.ngOnInit() + component.mobileSearchHidden = true + + component.onWindowScroll() + + expect(component.mobileSearchHidden).toBe(false) + + component.mobileSearchHidden = true + component.onWindowResize() + }) + + it('should keep mobile search visible while the mobile menu is expanded', () => { + Object.defineProperty(globalThis, 'innerWidth', { + value: 767, + }) + component.ngOnInit() + component.isMenuCollapsed = false + + Object.defineProperty(globalThis, 'scrollY', { + configurable: true, + value: 40, + }) + component.onWindowScroll() + + expect(component.mobileSearchHidden).toBe(false) + }) + it('should support close document & navigate on close current doc', () => { const closeSpy = jest.spyOn(openDocumentsService, 'closeDocument') closeSpy.mockReturnValue(of(true)) diff --git a/src-ui/src/app/components/app-frame/app-frame.component.ts b/src-ui/src/app/components/app-frame/app-frame.component.ts index 5218d829c..7989eb3e1 100644 --- a/src-ui/src/app/components/app-frame/app-frame.component.ts +++ b/src-ui/src/app/components/app-frame/app-frame.component.ts @@ -51,6 +51,8 @@ import { ComponentWithPermissions } from '../with-permissions/with-permissions.c import { GlobalSearchComponent } from './global-search/global-search.component' import { ToastsDropdownComponent } from './toasts-dropdown/toasts-dropdown.component' +const SCROLL_THRESHOLD = 16 + @Component({ selector: 'pngx-app-frame', templateUrl: './app-frame.component.html', @@ -94,6 +96,10 @@ export class AppFrameComponent slimSidebarAnimating: boolean = false + public mobileSearchHidden: boolean = false + + private lastScrollY: number = 0 + constructor() { super() const permissionsService = this.permissionsService @@ -111,6 +117,8 @@ export class AppFrameComponent } ngOnInit(): void { + this.lastScrollY = window.scrollY + if (this.settingsService.get(SETTINGS_KEYS.UPDATE_CHECKING_ENABLED)) { this.checkForUpdates() } @@ -263,6 +271,38 @@ export class AppFrameComponent return this.settingsService.get(SETTINGS_KEYS.AI_ENABLED) } + @HostListener('window:resize') + onWindowResize(): void { + if (!this.isMobileViewport()) { + this.mobileSearchHidden = false + } + } + + @HostListener('window:scroll') + onWindowScroll(): void { + const currentScrollY = window.scrollY + + if (!this.isMobileViewport() || this.isMenuCollapsed === false) { + this.mobileSearchHidden = false + this.lastScrollY = currentScrollY + return + } + + const delta = currentScrollY - this.lastScrollY + + if (currentScrollY <= 0 || delta < -SCROLL_THRESHOLD) { + this.mobileSearchHidden = false + } else if (currentScrollY > SCROLL_THRESHOLD && delta > SCROLL_THRESHOLD) { + this.mobileSearchHidden = true + } + + this.lastScrollY = currentScrollY + } + + private isMobileViewport(): boolean { + return window.innerWidth < 768 + } + closeMenu() { this.isMenuCollapsed = true } diff --git a/src-ui/src/app/components/document-list/document-list.component.scss b/src-ui/src/app/components/document-list/document-list.component.scss index 0e10b83da..06f4c4531 100644 --- a/src-ui/src/app/components/document-list/document-list.component.scss +++ b/src-ui/src/app/components/document-list/document-list.component.scss @@ -56,13 +56,20 @@ $paperless-card-breakpoints: ( .sticky-top { z-index: 990; // below main navbar - top: calc(7rem - 2px); // height of navbar (mobile) + top: calc(7rem - 2px); // height of navbar + search row (mobile) + transition: top 0.2s ease; @media (min-width: 580px) { top: 3.5rem; // height of navbar } } +@media (max-width: 579.98px) { + :host-context(main.mobile-search-hidden) .sticky-top { + top: calc(3.5rem - 2px); // height of navbar only when search is hidden + } +} + .table .form-check { padding: 0.2rem; min-height: 0;