+
+
+
+
+
diff --git a/front_end/elements/ElementsBreadcrumbs.ts b/front_end/elements/ElementsBreadcrumbs.ts
index 2b93bfb346..4d5fd5ddc9 100644
--- a/front_end/elements/ElementsBreadcrumbs.ts
+++ b/front_end/elements/ElementsBreadcrumbs.ts
@@ -17,17 +17,19 @@ export interface ElementsBreadcrumbsData {
}
export class ElementsBreadcrumbs extends HTMLElement {
private readonly shadow = this.attachShadow({mode: 'open'});
- private readonly resizeObserver = new ResizeObserver(() => this.update());
+ private readonly resizeObserver = new ResizeObserver(() => this.checkForOverflowOnResize());
private crumbsData: ReadonlyArray = [];
private selectedDOMNode: Readonly|null = null;
private overflowing = false;
private userScrollPosition: UserScrollPosition = 'start';
private isObservingResize = false;
+ private userHasManuallyScrolled = false;
set data(data: ElementsBreadcrumbsData) {
this.selectedDOMNode = data.selectedNode;
this.crumbsData = data.crumbs;
+ this.userHasManuallyScrolled = false;
this.update();
}
@@ -43,6 +45,34 @@ export class ElementsBreadcrumbs extends HTMLElement {
};
}
+ /*
+ * When the window is resized, we need to check if we either:
+ * 1) overflowing, and now the window is big enough that we don't need to
+ * 2) not overflowing, and now the window is small and we do need to
+ *
+ * If either of these are true, we toggle the overflowing state accordingly and trigger a re-render.
+ */
+ private checkForOverflowOnResize() {
+ const wrappingElement = this.shadow.querySelector('.crumbs');
+ const crumbs = this.shadow.querySelector('.crumbs-scroll-container');
+ if (!wrappingElement || !crumbs) {
+ return;
+ }
+
+ const totalContainingWidth = wrappingElement.clientWidth;
+ const totalCrumbsWidth = crumbs.clientWidth;
+
+ if (totalCrumbsWidth >= totalContainingWidth && this.overflowing === false) {
+ this.overflowing = true;
+ this.userScrollPosition = 'start';
+ this.render();
+ } else if (totalCrumbsWidth < totalContainingWidth && this.overflowing === true) {
+ this.overflowing = false;
+ this.userScrollPosition = 'start';
+ this.render();
+ }
+ }
+
private update() {
this.overflowing = false;
this.userScrollPosition = 'start';
@@ -157,6 +187,7 @@ export class ElementsBreadcrumbs extends HTMLElement {
private onOverflowClick(direction: 'left'|'right') {
return () => {
+ this.userHasManuallyScrolled = true;
const scrollWindow = this.shadow.querySelector('.crumbs-window');
if (!scrollWindow) {
@@ -309,14 +340,26 @@ export class ElementsBreadcrumbs extends HTMLElement {
}
private ensureSelectedNodeIsVisible() {
- if (!this.selectedDOMNode || !this.shadow || !this.overflowing) {
+ /*
+ * If the user has manually scrolled the crumbs in either direction, we
+ * effectively hand control over the scrolling down to them. This is to
+ * prevent the user manually scrolling to the end, and then us scrolling
+ * them back to the selected node. The moment they click either scroll
+ * button we set userHasManuallyScrolled, and we reset it when we get new
+ * data in. This means if the user clicks on a different element in the
+ * tree, we will auto-scroll that element into view, because we'll get new
+ * data and hence the flag will be reset.
+ */
+ if (!this.selectedDOMNode || !this.shadow || !this.overflowing || this.userHasManuallyScrolled) {
return;
}
const activeCrumbId = this.selectedDOMNode.id;
const activeCrumb = this.shadow.querySelector(`.crumb[data-node-id="${activeCrumbId}"]`);
if (activeCrumb) {
- activeCrumb.scrollIntoView();
+ activeCrumb.scrollIntoView({
+ behavior: 'smooth',
+ });
}
}
}
diff --git a/test/unittests/front_end/elements/ElementsBreadcrumbs_test.ts b/test/unittests/front_end/elements/ElementsBreadcrumbs_test.ts
index 069797dff0..6c2f5dd59e 100644
--- a/test/unittests/front_end/elements/ElementsBreadcrumbs_test.ts
+++ b/test/unittests/front_end/elements/ElementsBreadcrumbs_test.ts
@@ -4,7 +4,7 @@
import {ElementsBreadcrumbs} from '../../../../front_end/elements/ElementsBreadcrumbs.js';
import {crumbsToRender, determineElementTitle, DOMNode} from '../../../../front_end/elements/ElementsBreadcrumbsUtils.js';
-import {assertElement, assertElements, assertShadowRoot, dispatchClickEvent, renderElementIntoDOM, waitForScrollLeft} from '../helpers/DOMHelpers.js';
+import {assertElement, assertElements, assertShadowRoot, dispatchClickEvent, doubleRaf, renderElementIntoDOM, waitForScrollLeft} from '../helpers/DOMHelpers.js';
import {withNoMutations} from '../helpers/MutationHelpers.js';
const {assert} = chai;
@@ -349,6 +349,70 @@ describe('ElementsBreadcrumbs', () => {
assert.isTrue(rightButton.disabled);
});
});
+
+ it('hides the overflow buttons should the user resize the window to be large enough', async () => {
+ const thinWrapper = document.createElement('div');
+ thinWrapper.style.width = '400px';
+
+ const component = new ElementsBreadcrumbs();
+ thinWrapper.appendChild(component);
+
+ renderElementIntoDOM(thinWrapper);
+
+ component.data = {
+ crumbs: [divCrumb, bodyCrumb],
+ selectedNode: bodyCrumb,
+ };
+
+ assertShadowRoot(component.shadowRoot);
+
+ const leftButton = component.shadowRoot.querySelector('button.overflow.left');
+ assertElement(leftButton, HTMLButtonElement);
+ const rightButton = component.shadowRoot.querySelector('button.overflow.right');
+ assertElement(rightButton, HTMLButtonElement);
+
+ assert.isFalse(leftButton.classList.contains('hidden'));
+ assert.isFalse(rightButton.classList.contains('hidden'));
+
+ thinWrapper.style.width = '800px';
+ // So the browser has time to paint
+ await doubleRaf();
+
+ assert.isTrue(leftButton.classList.contains('hidden'));
+ assert.isTrue(rightButton.classList.contains('hidden'));
+ });
+
+ it('shows the overflow buttons should the user resize the window down to be small', async () => {
+ const thinWrapper = document.createElement('div');
+ thinWrapper.style.width = '800px';
+
+ const component = new ElementsBreadcrumbs();
+ thinWrapper.appendChild(component);
+
+ renderElementIntoDOM(thinWrapper);
+
+ component.data = {
+ crumbs: [divCrumb, bodyCrumb],
+ selectedNode: bodyCrumb,
+ };
+
+ assertShadowRoot(component.shadowRoot);
+
+ const leftButton = component.shadowRoot.querySelector('button.overflow.left');
+ assertElement(leftButton, HTMLButtonElement);
+ const rightButton = component.shadowRoot.querySelector('button.overflow.right');
+ assertElement(rightButton, HTMLButtonElement);
+
+ assert.isTrue(leftButton.classList.contains('hidden'));
+ assert.isTrue(rightButton.classList.contains('hidden'));
+
+ thinWrapper.style.width = '400px';
+ // So the browser has time to paint
+ await doubleRaf();
+
+ assert.isFalse(leftButton.classList.contains('hidden'));
+ assert.isFalse(rightButton.classList.contains('hidden'));
+ });
});
});
diff --git a/test/unittests/front_end/helpers/DOMHelpers.ts b/test/unittests/front_end/helpers/DOMHelpers.ts
index ca1d1ee22b..b22293648d 100644
--- a/test/unittests/front_end/helpers/DOMHelpers.ts
+++ b/test/unittests/front_end/helpers/DOMHelpers.ts
@@ -130,3 +130,7 @@ export function dispatchClickEvent(element: T, options: Mouse
assert.fail('Failed to trigger click event successfully.');
}
}
+
+export async function doubleRaf() {
+ await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
+}