Skip to content

Commit

Permalink
feat: scroll focused dashboard widget into view (#7875)
Browse files Browse the repository at this point in the history
  • Loading branch information
tomivirkki authored Sep 26, 2024
1 parent c272a0d commit 7f0b377
Show file tree
Hide file tree
Showing 2 changed files with 70 additions and 6 deletions.
30 changes: 24 additions & 6 deletions packages/dashboard/src/vaadin-dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
46 changes: 46 additions & 0 deletions packages/dashboard/test/dashboard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
getParentSection,
getRemoveButton,
getResizeHandle,
getScrollingContainer,
onceResized,
setGap,
setMaximumColumnWidth,
Expand Down Expand Up @@ -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;
Expand Down

0 comments on commit 7f0b377

Please sign in to comment.