diff --git a/frontend/src/components/ol-search-bar.js b/frontend/src/components/ol-search-bar.js index f478e8b..22618d0 100644 --- a/frontend/src/components/ol-search-bar.js +++ b/frontend/src/components/ol-search-bar.js @@ -124,6 +124,7 @@ export class OlSearchBar extends LitElement { super.disconnectedCallback(); document.removeEventListener('click', this._onDoc, true); window.removeEventListener('resize', this._onWinResize); + if (this._mobileExpanded) document.body.style.overflow = ''; this._acAbort?.abort(); this._authorAbort?.abort(); this._subjectAbort?.abort(); @@ -147,6 +148,10 @@ export class OlSearchBar extends LitElement { } // Sync full-screen overlay class on the host element. this.classList.toggle('mobile-exp', this._mobileExpanded); + // Prevent body scroll while overlay is active so the page behind doesn't scroll. + if (changed.has('_mobileExpanded')) { + document.body.style.overflow = this._mobileExpanded ? 'hidden' : ''; + } // Anchor the panel to the trigger and focus the panel-input when it opens. if (changed.has('_open') && this._open && this.showFacets) { if (!this._mobileExpanded) this._positionPanel(); @@ -665,18 +670,18 @@ export class OlSearchBar extends LitElement { background: white; overflow: hidden; } :host(.mobile-exp) .search-outer { - flex: 1; display: flex; flex-direction: column; + flex: 1; min-height: 0; display: flex; flex-direction: column; } :host(.mobile-exp) .input-row { display: none; } :host(.mobile-exp) .panel { - position: static; flex: 1; + position: static; flex: 1; min-height: 0; width: 100%; box-sizing: border-box; - overflow: visible; max-height: none; + display: flex; flex-direction: column; overflow: hidden; max-height: none; border: none; box-shadow: none; border-radius: 0; border-top: none; } :host(.mobile-exp) .panel-chips { max-height: none; } - :host(.mobile-exp) .ac-scroll { max-height: 40vh; } + :host(.mobile-exp) .ac-scroll { flex: 1; min-height: 0; max-height: none; overflow-y: auto; } :host(.mobile-exp) .pf-bar { overflow: visible; flex-wrap: wrap; } /* Back button shown at top of the expanded panel */ @@ -697,7 +702,7 @@ export class OlSearchBar extends LitElement { .pf-bar::-webkit-scrollbar { display: none; } .pf-btn { font-size: 10px; padding: 11px 4px; } .submit { padding: 6px 10px; } - .ac-scroll { max-height: 40vh; } + :host(:not(.mobile-exp)) .ac-scroll { max-height: 40vh; } .panel-chips { max-height: 72px; overflow-y: auto; } } `; diff --git a/frontend/src/components/ol-search-bar.mobile-overlay.test.js b/frontend/src/components/ol-search-bar.mobile-overlay.test.js index 517d918..b6ad2f8 100644 --- a/frontend/src/components/ol-search-bar.mobile-overlay.test.js +++ b/frontend/src/components/ol-search-bar.mobile-overlay.test.js @@ -37,16 +37,39 @@ describe('ol-search-bar mobile overlay CSS contract', () => { expect(src).toMatch(/:host\(\.mobile-exp\)\s+\.panel[^}]*max-height\s*:\s*none/); }); - it(':host(.mobile-exp) .ac-scroll has a vh-based max-height for results scrolling', () => { - expect(src).toMatch(/:host\(\.mobile-exp\)\s+\.ac-scroll[^}]*max-height\s*:\s*\d+vh/); + it(':host(.mobile-exp) .ac-scroll grows to fill remaining panel height via flex: 1', () => { + expect(src).toMatch(/:host\(\.mobile-exp\)\s+\.ac-scroll[^}]*flex\s*:\s*1/); + }); + + it(':host(.mobile-exp) .ac-scroll has min-height:0 so it can shrink below content size', () => { + expect(src).toMatch(/:host\(\.mobile-exp\)\s+\.ac-scroll[^}]*min-height\s*:\s*0/); + }); + + it(':host(.mobile-exp) .panel is a flex column so children stack and ac-scroll can flex-grow', () => { + expect(src).toMatch(/:host\(\.mobile-exp\)\s+\.panel[^}]*display\s*:\s*flex/); + expect(src).toMatch(/:host\(\.mobile-exp\)\s+\.panel[^}]*flex-direction\s*:\s*column/); + }); + + it('@media 600px .ac-scroll rule is scoped to :host(:not(.mobile-exp)) — no specificity conflict', () => { + const mediaBlock = src.slice(src.indexOf('@media (max-width: 600px)'), src.indexOf('@media (max-width: 600px)') + 400); + expect(mediaBlock).toMatch(/:host\(:not\(\.mobile-exp\)\)\s+\.ac-scroll/); + expect(mediaBlock).not.toMatch(/[^)]\s+\.ac-scroll\s*\{[^}]*max-height/); // plain .ac-scroll should not appear }); it(':host(.mobile-exp) .panel sets width:100% to override the desktop CSS var', () => { expect(src).toMatch(/:host\(\.mobile-exp\)\s+\.panel[^}]*width\s*:\s*100%/); }); - it(':host(.mobile-exp) .panel uses overflow:visible so facet dropdowns are not clipped', () => { - expect(src).toMatch(/:host\(\.mobile-exp\)\s+\.panel[^}]*overflow\s*:\s*visible/); + it(':host(.mobile-exp) .panel uses overflow:hidden to bound the flex scroll region', () => { + // In full-screen mode the panel IS the viewport, so facet dropdowns remain + // within its bounds and are not clipped; overflow:hidden is needed so that + // flex children (ac-scroll) are properly height-constrained. + expect(src).toMatch(/:host\(\.mobile-exp\)\s+\.panel[^}]*overflow\s*:\s*hidden/); + }); + + it(':host(.mobile-exp) .panel and .search-outer have min-height:0 so flex chain can shrink', () => { + expect(src).toMatch(/:host\(\.mobile-exp\)\s+\.panel[^}]*min-height\s*:\s*0/); + expect(src).toMatch(/:host\(\.mobile-exp\)\s+\.search-outer[^}]*min-height\s*:\s*0/); }); }); @@ -82,4 +105,15 @@ describe('ol-search-bar mobile overlay JS contract', () => { it('toggles mobile-exp class on :host via updated()', () => { expect(src).toMatch(/classList\.toggle\s*\(\s*['"]mobile-exp['"]\s*,\s*this\._mobileExpanded\s*\)/); }); + + it('locks body scroll when mobile overlay opens and restores it when it closes', () => { + const updatedFn = src.slice(src.indexOf('updated(changed)'), src.indexOf('updated(changed)') + 1200); + expect(updatedFn).toMatch(/document\.body\.style\.overflow/); + expect(updatedFn).toMatch(/_mobileExpanded.*hidden|hidden.*_mobileExpanded/s); + }); + + it('restores body scroll in disconnectedCallback in case component is removed while expanded', () => { + const dcFn = src.slice(src.indexOf('disconnectedCallback()'), src.indexOf('disconnectedCallback()') + 400); + expect(dcFn).toMatch(/document\.body\.style\.overflow/); + }); }); diff --git a/frontend/src/components/ol-search-bar.panel-overlay.test.js b/frontend/src/components/ol-search-bar.panel-overlay.test.js index f1b8e02..ce51141 100644 --- a/frontend/src/components/ol-search-bar.panel-overlay.test.js +++ b/frontend/src/components/ol-search-bar.panel-overlay.test.js @@ -99,7 +99,7 @@ describe('ol-search-bar panel overlay — panel-input contract', () => { }); it('updated() focuses the panel-input when the panel opens', () => { - const updFn = src.slice(src.indexOf('updated(changed)'), src.indexOf('updated(changed)') + 1000); + const updFn = src.slice(src.indexOf('updated(changed)'), src.indexOf('updated(changed)') + 1200); expect(updFn).toMatch(/panel-input/); }); });