diff --git a/resources/js/components/dropdown.js b/resources/js/components/dropdown.js index 5dd5dd93b01..d2b044ee1ca 100644 --- a/resources/js/components/dropdown.js +++ b/resources/js/components/dropdown.js @@ -1,4 +1,4 @@ -import {onSelect} from '../services/dom.ts'; +import {findClosestScrollContainer, onSelect} from '../services/dom.ts'; import {KeyboardNavigationHandler} from '../services/keyboard-navigation.ts'; import {Component} from './component'; @@ -33,7 +33,8 @@ export class Dropdown extends Component { const menuOriginalRect = this.menu.getBoundingClientRect(); let heightOffset = 0; const toggleHeight = this.toggle.getBoundingClientRect().height; - const dropUpwards = menuOriginalRect.bottom > window.innerHeight; + const containerBounds = findClosestScrollContainer(this.menu).getBoundingClientRect(); + const dropUpwards = menuOriginalRect.bottom > containerBounds.bottom; const containerRect = this.container.getBoundingClientRect(); // If enabled, Move to body to prevent being trapped within scrollable sections diff --git a/resources/js/components/tri-layout.js b/resources/js/components/tri-layout.ts similarity index 57% rename from resources/js/components/tri-layout.js rename to resources/js/components/tri-layout.ts index be9388e8d46..40a2d369104 100644 --- a/resources/js/components/tri-layout.js +++ b/resources/js/components/tri-layout.ts @@ -1,18 +1,22 @@ import {Component} from './component'; export class TriLayout extends Component { - - setup() { + private container!: HTMLElement; + private tabs!: HTMLElement[]; + private sidebarScrollContainers!: HTMLElement[]; + + private lastLayoutType = 'none'; + private onDestroy: (()=>void)|null = null; + private scrollCache: Record = { + content: 0, + info: 0, + }; + private lastTabShown = 'content'; + + setup(): void { this.container = this.$refs.container; this.tabs = this.$manyRefs.tab; - - this.lastLayoutType = 'none'; - this.onDestroy = null; - this.scrollCache = { - content: 0, - info: 0, - }; - this.lastTabShown = 'content'; + this.sidebarScrollContainers = this.$manyRefs.sidebarScrollContainer; // Bind any listeners this.mobileTabClick = this.mobileTabClick.bind(this); @@ -22,9 +26,11 @@ export class TriLayout extends Component { window.addEventListener('resize', () => { this.updateLayout(); }, {passive: true}); + + this.setupSidebarScrollHandlers(); } - updateLayout() { + updateLayout(): void { let newLayout = 'tablet'; if (window.innerWidth <= 1000) newLayout = 'mobile'; if (window.innerWidth > 1400) newLayout = 'desktop'; @@ -56,16 +62,15 @@ export class TriLayout extends Component { }; } - setupDesktop() { + setupDesktop(): void { // } /** * Action to run when the mobile info toggle bar is clicked/tapped - * @param event */ - mobileTabClick(event) { - const {tab} = event.target.dataset; + mobileTabClick(event: MouseEvent): void { + const tab = (event.target as HTMLElement).dataset.tab || ''; this.showTab(tab); } @@ -73,16 +78,14 @@ export class TriLayout extends Component { * Show the content tab. * Used by the page-display component. */ - showContent() { + showContent(): void { this.showTab('content', false); } /** * Show the given tab - * @param {String} tabName - * @param {Boolean }scroll */ - showTab(tabName, scroll = true) { + showTab(tabName: string, scroll: boolean = true): void { this.scrollCache[this.lastTabShown] = document.documentElement.scrollTop; // Set tab status @@ -97,7 +100,7 @@ export class TriLayout extends Component { // Set the scroll position from cache if (scroll) { - const pageHeader = document.querySelector('header'); + const pageHeader = document.querySelector('header') as HTMLElement; const defaultScrollTop = pageHeader.getBoundingClientRect().bottom; document.documentElement.scrollTop = this.scrollCache[tabName] || defaultScrollTop; setTimeout(() => { @@ -108,4 +111,30 @@ export class TriLayout extends Component { this.lastTabShown = tabName; } + setupSidebarScrollHandlers(): void { + for (const sidebar of this.sidebarScrollContainers) { + sidebar.addEventListener('scroll', () => this.handleSidebarScroll(sidebar), { + passive: true, + }); + this.handleSidebarScroll(sidebar); + } + + window.addEventListener('resize', () => { + for (const sidebar of this.sidebarScrollContainers) { + this.handleSidebarScroll(sidebar); + } + }); + } + + handleSidebarScroll(sidebar: HTMLElement): void { + const scrollable = sidebar.clientHeight !== sidebar.scrollHeight; + const atTop = sidebar.scrollTop === 0; + const atBottom = (sidebar.scrollTop + sidebar.clientHeight) === sidebar.scrollHeight; + + if (sidebar.parentElement) { + sidebar.parentElement.classList.toggle('scroll-away-from-top', !atTop && scrollable); + sidebar.parentElement.classList.toggle('scroll-away-from-bottom', !atBottom && scrollable); + } + } + } diff --git a/resources/js/services/dom.ts b/resources/js/services/dom.ts index c3817536c85..8696fe81639 100644 --- a/resources/js/services/dom.ts +++ b/resources/js/services/dom.ts @@ -256,4 +256,22 @@ export function findTargetNodeAndOffset(parentNode: HTMLElement, offset: number) export function hashElement(element: HTMLElement): string { const normalisedElemText = (element.textContent || '').replace(/\s{2,}/g, ''); return cyrb53(normalisedElemText); +} + +/** + * Find the closest scroll container parent for the given element + * otherwise will default to the body element. + */ +export function findClosestScrollContainer(start: HTMLElement): HTMLElement { + let el: HTMLElement|null = start; + do { + const computed = window.getComputedStyle(el); + if (computed.overflowY === 'scroll') { + return el; + } + + el = el.parentElement; + } while (el); + + return document.body; } \ No newline at end of file diff --git a/resources/sass/_layout.scss b/resources/sass/_layout.scss index 8175db948a5..48b4b0ca22e 100644 --- a/resources/sass/_layout.scss +++ b/resources/sass/_layout.scss @@ -389,10 +389,12 @@ body.flexbox { .tri-layout-right { grid-area: c; min-width: 0; + position: relative; } .tri-layout-left { grid-area: a; min-width: 0; + position: relative; } @include mixins.larger-than(vars.$bp-xxl) { @@ -431,7 +433,8 @@ body.flexbox { grid-template-areas: "a b b"; grid-template-columns: 1fr 3fr; grid-template-rows: min-content min-content 1fr; - padding-inline-end: vars.$l; + margin-inline-start: (vars.$m + vars.$xxs); + margin-inline-end: (vars.$m + vars.$xxs); } .tri-layout-sides { grid-column-start: a; @@ -452,6 +455,8 @@ body.flexbox { height: 100%; scrollbar-width: none; -ms-overflow-style: none; + padding-inline: vars.$m; + margin-inline: -(vars.$m); &::-webkit-scrollbar { display: none; } @@ -520,4 +525,26 @@ body.flexbox { margin-inline-start: 0; margin-inline-end: 0; } +} + +/** + * Scroll Indicators + */ +.scroll-away-from-top:before, +.scroll-away-from-bottom:after { + content: ''; + display: block; + position: absolute; + @include mixins.lightDark(color, #F2F2F2, #111); + left: 0; + top: 0; + width: 100%; + height: 50px; + background: linear-gradient(to bottom, currentColor, transparent); + z-index: 2; +} +.scroll-away-from-bottom:after { + top: auto; + bottom: 0; + background: linear-gradient(to top, currentColor, transparent); } \ No newline at end of file diff --git a/resources/views/layouts/tri.blade.php b/resources/views/layouts/tri.blade.php index c3cedf0fbc2..061cc69945c 100644 --- a/resources/views/layouts/tri.blade.php +++ b/resources/views/layouts/tri.blade.php @@ -28,15 +28,15 @@ class="tri-layout-mobile-tab px-m py-m text-link active">