From c721b1da6a4c1549cbd0e4b3047287572e389911 Mon Sep 17 00:00:00 2001 From: AJ Slater Date: Tue, 29 Sep 2026 16:38:03 -0700 Subject: [PATCH] fix(browser): picking a saved view no longer reopens its menu After a saved view was picked from the settings drawer's "Load Saved View" combobox, the view loaded but the menu popped back open with focus still in the input. Vuetify closes the menu on select and keeps focus in the combobox. When the view's settings land, the component's deep settings watcher clears the combobox model. VCombobox's model watcher then sets its search text to '', and its search watcher opens the menu whenever the field is focused and the menu is closed. The watcher now blurs the combobox before clearing a picked view, so the name still clears and the menu stays shut. It only blurs when the model holds a picked view (an object with a pk). Every browse page load re-sets settings.breadcrumbs, which fires the same watcher. An unconditional blur closed a menu the user had opened while a page was still loading. Checked in a real browser: mouse and keyboard picks, a view whose settings match the current ones, and opening the menu mid page load. Also checked merged with the unpushed per-view trash icon branch: a pick from its item slot, and its Delete View dialog. Co-Authored-By: Claude Opus 5.5 --- NEWS.md | 1 + .../browser/drawer/browser-settings-saved.vue | 5 + .../unit/browser-saved-views-menu.test.js | 111 ++++++++++++++++++ 3 files changed, 117 insertions(+) create mode 100644 frontend/tests/unit/browser-saved-views-menu.test.js diff --git a/NEWS.md b/NEWS.md index d94949db1..75591e939 100644 --- a/NEWS.md +++ b/NEWS.md @@ -15,6 +15,7 @@ border-radius: 128px; of a list with no way back. - Clearing a search or loading a saved view no longer leaves a list with no way back, and saved views keep their top collection. + - The saved views menu no longer reopens after loading a view. ## v2.5.0 - Admin Defaults diff --git a/frontend/src/components/browser/drawer/browser-settings-saved.vue b/frontend/src/components/browser/drawer/browser-settings-saved.vue index 1d780e929..8a9b4dd49 100644 --- a/frontend/src/components/browser/drawer/browser-settings-saved.vue +++ b/frontend/src/components/browser/drawer/browser-settings-saved.vue @@ -3,6 +3,7 @@
Saved Views
({ + default: { currentRoute: { value: { params: {}, query: {} } } }, +})); +vi.mock("@/api/v4/browser", () => ({ getSavedSettingsList: vi.fn() })); + +import BrowserSettingsSaved from "@/components/browser/drawer/browser-settings-saved.vue"; +import vuetify from "@/plugins/vuetify"; +import { useBrowserStore } from "@/stores/browser"; + +const SAVED = [ + { pk: 3, name: "Added Recently" }, + { pk: 7, name: "DC by Year" }, +]; + +describe("picking a saved view", () => { + let wrapper; + + beforeAll(() => { + // VOverlay reads the bare global; happy-dom has no visual viewport. + globalThis.visualViewport ??= { + width: 1024, + height: 768, + offsetLeft: 0, + offsetTop: 0, + scale: 1, + addEventListener() {}, + removeEventListener() {}, + }; + }); + + afterEach(() => { + wrapper?.unmount(); + document.body.replaceChildren(); + }); + + const openMenu = async () => { + const pinia = createTestingPinia({ + initialState: { browser: { savedSettingsList: SAVED } }, + }); + wrapper = mount(BrowserSettingsSaved, { + attachTo: document.body, + global: { plugins: [vuetify, pinia] }, + }); + wrapper.find("input").element.focus(); + await wrapper.find(".v-field").trigger("mousedown"); + await flushPromises(); + return { + combobox: wrapper.findComponent(VCombobox), + store: useBrowserStore(pinia), + }; + }; + + const pickView = async (name) => { + const opened = await openMenu(); + const option = [...document.querySelectorAll('[role="option"]')].find( + (el) => el.textContent.trim() === name, + ); + option.click(); + await flushPromises(); + return opened; + }; + + test("the menu stays closed when the view's settings land", async () => { + const { combobox, store } = await pickView("DC by Year"); + expect(store.loadSavedSettings).toHaveBeenCalledWith(7); + expect(combobox.vm.menu).toBe(false); + + // What loadSavedSettings does once the view arrives. + store.settings.orderBy = "date"; + await flushPromises(); + + expect(combobox.vm.menu).toBe(false); + }); + + test("the loaded view's name is cleared", async () => { + const { combobox, store } = await pickView("DC by Year"); + expect(combobox.vm.search).toBe("DC by Year"); + + store.settings.orderBy = "date"; + await flushPromises(); + + expect(combobox.props("modelValue")).toBeNull(); + expect(combobox.vm.search).toBe(""); + }); + + test("a page load doesn't close a menu opened after it started", async () => { + const { combobox, store } = await openMenu(); + expect(combobox.vm.menu).toBe(true); + + // loadBrowserPage re-sets the breadcrumbs on every response. + store.settings.breadcrumbs = []; + await flushPromises(); + + expect(combobox.vm.menu).toBe(true); + expect(combobox.vm.isFocused).toBe(true); + }); +}); + +export default {};