fix(traces): open the service side panel as a non-modal drawer to avoid flicker (v1.1.0) (#15342) (#15440)

This commit is contained in:
Omkar Kesarkhane
2026-10-16 11:13:44 +05:30
committed by GitHub
parent c2e07c11ba
commit 6a27de5e23
6 changed files with 76 additions and 4 deletions
@@ -275,6 +275,24 @@ describe("ODrawer", () => {
expect(document.activeElement).toBe(panel.element);
wrapper.unmount();
});
it("focuses without scrolling so a contained portal target is not dragged sideways", async () => {
const focusSpy = vi.spyOn(HTMLElement.prototype, "focus");
const wrapper = mount(ODrawer, {
attachTo: document.body,
props: { open: true, title: "Contained" },
slots: { default: '<button autofocus data-testid="autofocus-close">Close</button>' },
});
const panel = findDrawerPanel(wrapper);
focusSpy.mockClear();
await panel.vm.$emit("openAutoFocus", new Event("openAutoFocus", { cancelable: true }));
await new Promise((resolve) => setTimeout(resolve));
expect(focusSpy).toHaveBeenCalledWith({ preventScroll: true });
focusSpy.mockRestore();
wrapper.unmount();
});
});
describe("Escape key behaviour", () => {
+5 -4
View File
@@ -305,24 +305,25 @@ function findAutoFocusTarget(root: Element): HTMLElement | null {
function handleOpenAutoFocus(event: Event) {
event.preventDefault();
// The panel is still translated off-screen by the slide-in, so a scrolling focus would drag a contained portal target sideways.
nextTick(() => {
const body = bodyRef.value;
const field = body ? findAutoFocusTarget(body) : null;
if (field) {
field.focus();
field.focus({ preventScroll: true });
return;
}
const autofocus = body?.querySelector<HTMLElement>("[autofocus]");
if (autofocus) {
autofocus.focus();
autofocus.focus({ preventScroll: true });
return;
}
const btnEl = (primaryBtnRef.value as any)?.$el as HTMLElement | undefined;
if (btnEl) {
btnEl.focus();
btnEl.focus({ preventScroll: true });
return;
}
body?.closest<HTMLElement>("[data-o2-drawer]")?.focus();
body?.closest<HTMLElement>("[data-o2-drawer]")?.focus({ preventScroll: true });
});
}
@@ -164,6 +164,7 @@ const ODrawerStub = {
"open",
"width",
"seamless",
"modal",
"portalTarget",
"title",
"subTitle",
@@ -311,6 +312,14 @@ describe("ServiceGraphNodeSidePanel", () => {
});
});
describe("drawer behaviour", () => {
it("renders a non-modal drawer so the page behind stays interactive", () => {
wrapper = mountPanel({ selectedNode: baseNode });
const drawer = wrapper.findComponent(ODrawerStub);
expect(drawer.props("modal")).toBe(false);
});
});
// -------------------------------------------------------------------------
// Service name display
// -------------------------------------------------------------------------
@@ -20,6 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
:open="visible"
:width="60"
:seamless="true"
:modal="false"
:portal-target="containerEl"
:title="selectedNode?.name || selectedNode?.label || selectedNode?.id"
data-test="service-graph-side-panel"
@@ -181,6 +181,7 @@ const ODrawerStub = {
"open",
"width",
"seamless",
"modal",
"portalTarget",
"title",
"subTitle",
@@ -1408,6 +1408,48 @@ describe("ServicesCatalog", () => {
expect(wrapper.vm.showSidePanel).toBe(false);
expect(wrapper.vm.selectedServiceRow).toBeNull();
});
it("should switch the open side panel to another row in place", async () => {
mockFetchQueryDataWithHttpStream.mockImplementation((_req: any, callbacks: any) => {
const hits = mockServices.slice(0, 2).map((s) => ({
service_name: s.service_name,
total_requests: s.total_requests,
error_count: s.error_count,
error_rate: s.error_rate,
avg_duration_ns: s.avg_duration_ns,
max_duration_ns: s.max_duration_ns,
p50_latency_ns: s.p50_latency_ns,
p95_latency_ns: s.p95_latency_ns,
p99_latency_ns: s.p99_latency_ns,
}));
if (callbacks?.data) {
callbacks.data(null, {
type: "search_response_hits",
content: { results: { hits } },
});
}
if (callbacks?.complete) {
callbacks.complete(null, {});
}
});
wrapper = mountServicesCatalog();
await flushPromises();
await wrapper.find('[data-test="services-catalog-row-name-api-gateway"]').trigger("click");
expect(wrapper.vm.showSidePanel).toBe(true);
expect(wrapper.vm.selectedServiceRow?.service_name).toBe("api-gateway");
const panelBefore = wrapper.findComponent('[data-test="services-catalog-node-side-panel"]');
expect(panelBefore.exists()).toBe(true);
await wrapper.find('[data-test="services-catalog-row-name-auth-service"]').trigger("click");
expect(wrapper.vm.showSidePanel).toBe(true);
expect(wrapper.vm.selectedServiceRow?.service_name).toBe("auth-service");
const panelAfter = wrapper.findComponent('[data-test="services-catalog-node-side-panel"]');
expect(panelAfter.vm).toBe(panelBefore.vm);
expect(panelAfter.props("selectedNode")?.name).toBe("auth-service");
});
});
// -----------------------------------------------------------------------