From 823b993b74f29eddace8e2beb0a34d3f4ce7079c Mon Sep 17 00:00:00 2001 From: Tomi Virkki Date: Thu, 26 Sep 2024 13:54:00 +0300 Subject: [PATCH] feat: scroll focused dashboard widget into view --- packages/dashboard/src/vaadin-dashboard.js | 30 +++++++++++--- packages/dashboard/test/dashboard.test.ts | 46 ++++++++++++++++++++++ 2 files changed, 70 insertions(+), 6 deletions(-) diff --git a/packages/dashboard/src/vaadin-dashboard.js b/packages/dashboard/src/vaadin-dashboard.js index 4eb70f6e5e..1ebe4f0c82 100644 --- a/packages/dashboard/src/vaadin-dashboard.js +++ b/packages/dashboard/src/vaadin-dashboard.js @@ -244,12 +244,30 @@ class Dashboard extends ControllerMixin(DashboardLayoutMixin(ElementMixin(Themab // Remove the unused wrappers wrappers.forEach((wrapper) => wrapper.remove()); - if (focusedWrapperWillBeRemoved) { - // The wrapper containing the focused element was removed. Try to focus the element in the closest wrapper. - requestAnimationFrame(() => - this.__focusWrapperContent(wrapperClosestToRemovedFocused || this.querySelector(WRAPPER_LOCAL_NAME)), - ); - } + requestAnimationFrame(() => { + if (focusedWrapperWillBeRemoved) { + // The wrapper containing the focused element was removed. Try to focus the element in the closest wrapper. + this.__focusWrapperContent(wrapperClosestToRemovedFocused || this.querySelector(WRAPPER_LOCAL_NAME)); + } + + const focusedItem = this.querySelector('[focused]'); + if (focusedItem && !this.__insideViewport(focusedItem)) { + // If the focused wrapper is not in the viewport, scroll it into view + focusedItem.scrollIntoView(); + } + }); + } + + /** @private */ + __insideViewport(element) { + const rect = element.getBoundingClientRect(); + const dashboardRect = this.getBoundingClientRect(); + return ( + rect.bottom >= dashboardRect.top && + rect.right >= dashboardRect.left && + rect.top <= dashboardRect.bottom && + rect.left <= dashboardRect.right + ); } /** @private */ diff --git a/packages/dashboard/test/dashboard.test.ts b/packages/dashboard/test/dashboard.test.ts index 8dc6fe340e..d4da2a9073 100644 --- a/packages/dashboard/test/dashboard.test.ts +++ b/packages/dashboard/test/dashboard.test.ts @@ -13,6 +13,7 @@ import { getParentSection, getRemoveButton, getResizeHandle, + getScrollingContainer, onceResized, setGap, setMaximumColumnWidth, @@ -569,6 +570,51 @@ describe('dashboard', () => { expect(removeButton.getBoundingClientRect().height).to.be.above(0); }); + it('should scroll the focused item into view on render', async () => { + // Limit the dashboard height to force scrolling + dashboard.style.height = '300px'; + await onceResized(dashboard); + // Focus the first item + getElementFromCell(dashboard, 0, 0)!.focus(); + + // Add enough items to push the focused item out of view + dashboard.items = Array.from({ length: 10 }, (_, i) => ({ id: i.toString() })).reverse(); + await nextFrame(); + await nextFrame(); + + // Expect the focused item to have been scrolled back into view + const widgetRect = document.activeElement!.getBoundingClientRect(); + const dashboardRect = dashboard.getBoundingClientRect(); + expect(widgetRect.bottom).to.be.above(dashboardRect.top); + expect(widgetRect.top).to.be.below(dashboardRect.bottom); + }); + + it('should not scroll the focused item into view if it is partially visible', async () => { + // Limit the dashboard height to force scrolling + dashboard.style.height = '300px'; + await onceResized(dashboard); + // Focus the first item + getElementFromCell(dashboard, 0, 0)!.focus(); + + // Add enough items to make the dashboard scrollable + dashboard.items = Array.from({ length: 10 }, (_, i) => ({ id: i.toString() })); + await nextFrame(); + await nextFrame(); + + // Scroll the dashboard to make the focused item partially visible + const scrollingContainer = getScrollingContainer(dashboard); + const scrollTop = Math.round(document.activeElement!.getBoundingClientRect().height / 2); + scrollingContainer.scrollTop = scrollTop; + + // Change the items to trigger a render + dashboard.items = dashboard.items.slice(0, -1); + await nextFrame(); + await nextFrame(); + + // Expect no scrolling to have occurred + expect(scrollingContainer.scrollTop).to.equal(scrollTop); + }); + describe('focus restore on focused item removal', () => { beforeEach(async () => { dashboard.editable = true;