mirror of
https://github.com/photoprism/photoprism.git
synced 2026-07-18 00:59:38 +00:00
Frontend: Stop chip-selector from committing on ArrowDown blur #4966
Vuetify's v-combobox shifts DOM focus from the input to the first v-list-item in the dropdown menu when the user presses ArrowDown. The chip-selector's @blur handler treated that focus shift as "user is leaving the field — commit pending text," which silently turned typed prefixes (e.g. `ca`) into brand-new chips before the user could highlight `Camping` and press Enter. The blur handler now inspects relatedTarget and skips the commit when focus moves into a v-list-item or any descendant of a v-overlay-container / v-list / v-menu. Tabbing or clicking to a sibling field still commits as before. The chip-selector backs the Labels and Albums inputs in the batch-edit dialog (and the file dialog), so the regression was visible in both surfaces.
This commit is contained in:
parent
9788c2b06f
commit
a81082a267
2 changed files with 72 additions and 1 deletions
|
|
@ -51,7 +51,7 @@
|
|||
class="chip-selector__input"
|
||||
@click:control="focusInput"
|
||||
@keydown.enter.prevent="onEnter"
|
||||
@blur="addNewItem"
|
||||
@blur="onInputBlur"
|
||||
@update:model-value="onComboboxChange"
|
||||
@update:menu="onMenuUpdate"
|
||||
>
|
||||
|
|
@ -333,6 +333,25 @@ export default {
|
|||
this.addNewItem();
|
||||
},
|
||||
|
||||
// Blur on the combobox input fires for two distinct reasons:
|
||||
// (a) the user is leaving the field (Tab, click elsewhere) — pending
|
||||
// text should commit as a new chip, mirroring the inline-text
|
||||
// auto-commit-on-blur contract elsewhere in the app.
|
||||
// (b) Vuetify shifts DOM focus into the dropdown menu (ArrowDown,
|
||||
// Home, End) — the user is mid-navigation; committing the
|
||||
// typed prefix here would silently turn `ca` into a brand-new
|
||||
// chip before they can pick `Camping` from the suggestions.
|
||||
// The menu list items are teleported to a v-overlay-container, so the
|
||||
// shared DOM parent is `<body>`; detect them via `relatedTarget` class
|
||||
// / ancestor lookup and skip the commit in case (b).
|
||||
onInputBlur(event) {
|
||||
const rt = event?.relatedTarget;
|
||||
if (rt && (rt.classList?.contains("v-list-item") || rt.closest?.(".v-overlay-container, .v-list, .v-menu"))) {
|
||||
return;
|
||||
}
|
||||
this.addNewItem();
|
||||
},
|
||||
|
||||
onMenuUpdate(val) {
|
||||
if (val && this.suppressMenuOpen) {
|
||||
this.menuOpen = false;
|
||||
|
|
|
|||
|
|
@ -232,6 +232,58 @@ describe("component/file/chip-selector", () => {
|
|||
|
||||
expect(wrapper.emitted("update:items")).toBeFalsy();
|
||||
});
|
||||
|
||||
// Repros the user-reported regression: typing `ca` and pressing ArrowDown
|
||||
// used to commit `ca` as a brand-new chip before the user could pick
|
||||
// `Camping` from the dropdown. The trigger is `@blur` firing on the
|
||||
// input as Vuetify shifts DOM focus into the teleported v-list-item
|
||||
// menu. onInputBlur skips the commit when relatedTarget is a menu item.
|
||||
it("skips addNewItem on blur when focus shifts into the dropdown menu (ArrowDown)", () => {
|
||||
wrapper.vm.newItemTitle = "ca";
|
||||
|
||||
// relatedTarget mimics the v-list-item Vuetify focuses on ArrowDown.
|
||||
const menuItem = document.createElement("div");
|
||||
menuItem.className = "v-list-item v-list-item--link";
|
||||
|
||||
wrapper.vm.onInputBlur({ relatedTarget: menuItem });
|
||||
|
||||
expect(wrapper.emitted("update:items")).toBeFalsy();
|
||||
});
|
||||
|
||||
it("skips addNewItem on blur when relatedTarget is inside a v-overlay-container", () => {
|
||||
wrapper.vm.newItemTitle = "ca";
|
||||
|
||||
// Some Vuetify menu items render under .v-overlay-container without
|
||||
// the v-list-item class on the focused descendant; assert the ancestor
|
||||
// check catches them too.
|
||||
const overlay = document.createElement("div");
|
||||
overlay.className = "v-overlay-container";
|
||||
const focusableInside = document.createElement("div");
|
||||
overlay.appendChild(focusableInside);
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
try {
|
||||
wrapper.vm.onInputBlur({ relatedTarget: focusableInside });
|
||||
expect(wrapper.emitted("update:items")).toBeFalsy();
|
||||
} finally {
|
||||
document.body.removeChild(overlay);
|
||||
}
|
||||
});
|
||||
|
||||
it("commits pending text on blur when relatedTarget is outside the dropdown menu", () => {
|
||||
wrapper.vm.newItemTitle = "Manually-Typed";
|
||||
|
||||
// Tabbing to a sibling field — relatedTarget has no v-list-item /
|
||||
// v-overlay-container ancestry. The blur should still commit.
|
||||
const sibling = document.createElement("button");
|
||||
sibling.className = "some-other-button";
|
||||
|
||||
wrapper.vm.onInputBlur({ relatedTarget: sibling });
|
||||
|
||||
const emitted = wrapper.emitted("update:items");
|
||||
expect(emitted).toBeTruthy();
|
||||
expect(emitted[0][0]).toEqual(expect.arrayContaining([expect.objectContaining({ title: "Manually-Typed", action: "add", isNew: true })]));
|
||||
});
|
||||
});
|
||||
|
||||
describe("Label resolver and normalization", () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue