mirror of
https://github.com/photoprism/photoprism.git
synced 2026-01-23 02:24:24 +00:00
UX: Improve people name editing and confirm dialog focus management (#5307)
* Frontend: Improve people name editing and confirmation UX Adds better menu control and confirmation dialog handling for editing people names in photo edit and new people pages. Ensures only one menu is open at a time, improves keyboard accessibility, and prevents conflicting confirmation dialogs. Also updates event handling and emits for dialog and people components. * Frotend: clear model fields on cancel in people dialogs #5145 * Frontend: Enable menu opening on click in people edit and new pages #5145 * Frontend: Enhance confirmation dialog with improved keyboard accessibility #5145 * Frontend: Refactor name confirmation handling in people edit and new pages #5145 * Frontend: Update name setting logic in people edit and new pages #5145 * People: Adjust menuProps and add focus trap to PConfirmDialog #5307 Signed-off-by: Michael Mayer <michael@photoprism.app> --------- Signed-off-by: Michael Mayer <michael@photoprism.app> Co-authored-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
parent
70821fb7d0
commit
2cc186074a
3 changed files with 102 additions and 26 deletions
|
|
@ -1,21 +1,29 @@
|
|||
<template>
|
||||
<v-dialog
|
||||
ref="dialog"
|
||||
:model-value="visible"
|
||||
:close-delay="0"
|
||||
:open-delay="0"
|
||||
persistent
|
||||
scrim
|
||||
max-width="360"
|
||||
class="p-dialog p-confirm-dialog"
|
||||
@keydown.esc.exact="close"
|
||||
@keyup.esc.exact="close"
|
||||
@keyup.enter.exact="confirm"
|
||||
@after-enter="afterEnter"
|
||||
@after-leave="afterLeave"
|
||||
@focusout="onFocusOut"
|
||||
>
|
||||
<v-card>
|
||||
<v-card ref="content" tabindex="0">
|
||||
<v-card-title class="d-flex justify-start align-center ga-3">
|
||||
<v-icon :icon="icon" :size="iconSize" color="primary"></v-icon>
|
||||
<div class="text-subtitle-1">{{ text ? text : $gettext(`Are you sure?`) }}</div>
|
||||
</v-card-title>
|
||||
<v-card-actions class="action-buttons">
|
||||
<v-btn variant="flat" color="button" class="action-cancel action-close" @click.stop="close">
|
||||
<v-btn variant="flat" tabindex="0" color="button" class="action-cancel action-close" @click.stop="close">
|
||||
{{ $gettext(`Cancel`) }}
|
||||
</v-btn>
|
||||
<v-btn color="highlight" variant="flat" class="action-confirm" @click.stop="confirm">
|
||||
<v-btn color="highlight" tabindex="0" variant="flat" class="action-confirm" @click.stop="confirm">
|
||||
{{ action ? action : $gettext(`Yes`) }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
|
|
@ -47,10 +55,36 @@ export default {
|
|||
default: "",
|
||||
},
|
||||
},
|
||||
emits: ["close", "confirm"],
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
methods: {
|
||||
afterEnter() {
|
||||
this.$view.enter(this);
|
||||
},
|
||||
afterLeave() {
|
||||
this.$view.leave(this);
|
||||
},
|
||||
onFocusOut(ev) {
|
||||
if (!this.$view.isActive(this)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const el = this.$refs.content?.$el;
|
||||
|
||||
if (!ev || !ev.target || !(ev.target instanceof HTMLElement) || !(el instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const next = ev.relatedTarget;
|
||||
const leavingDialog = !next || !(next instanceof Node) || !el.contains(next);
|
||||
|
||||
if (leavingDialog) {
|
||||
el.focus();
|
||||
ev.preventDefault();
|
||||
}
|
||||
},
|
||||
close() {
|
||||
this.$emit("close");
|
||||
},
|
||||
|
|
|
|||
|
|
@ -79,19 +79,19 @@
|
|||
item-title="Name"
|
||||
item-value="Name"
|
||||
:disabled="busy"
|
||||
:menu-props="menuProps"
|
||||
return-object
|
||||
hide-no-data
|
||||
:menu-props="menuProps"
|
||||
hide-details
|
||||
single-line
|
||||
open-on-clear
|
||||
append-icon=""
|
||||
prepend-inner-icon="mdi-account-plus"
|
||||
density="comfortable"
|
||||
class="input-name pa-0 ma-0"
|
||||
@blur="onSetName(m)"
|
||||
class="input-name pa-0 ma-0 text-selectable"
|
||||
@update:model-value="(person) => onSetPerson(m, person)"
|
||||
@keyup.enter.native="onSetName(m)"
|
||||
@blur="(ev) => onSetName(m, ev)"
|
||||
@keyup.enter="(ev) => onSetName(m, ev)"
|
||||
>
|
||||
</v-combobox>
|
||||
</v-card-actions>
|
||||
|
|
@ -140,11 +140,18 @@ export default {
|
|||
text: this.$gettext("Add person?"),
|
||||
},
|
||||
menuProps: {
|
||||
closeOnClick: false,
|
||||
openOnClick: true,
|
||||
openOnFocus: true,
|
||||
closeOnBack: true,
|
||||
closeOnContentClick: true,
|
||||
openOnClick: false,
|
||||
persistent: false,
|
||||
scrim: true,
|
||||
openDelay: 0,
|
||||
closeDelay: 0,
|
||||
opacity: 0,
|
||||
density: "compact",
|
||||
maxHeight: 300,
|
||||
scrollStrategy: "reposition",
|
||||
},
|
||||
textRule: (v) => {
|
||||
if (!v || !v.length) {
|
||||
|
|
@ -322,11 +329,16 @@ export default {
|
|||
|
||||
return true;
|
||||
},
|
||||
onSetName(model) {
|
||||
onSetName(model, ev) {
|
||||
if (this.busy || !model) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If there's a pending confirmation for a different face, don't process new input
|
||||
if (this.confirm.visible && this.confirm.model && this.confirm.model.UID !== model.UID) {
|
||||
return;
|
||||
}
|
||||
|
||||
const name = model?.Name;
|
||||
|
||||
if (!name) {
|
||||
|
|
@ -343,14 +355,21 @@ export default {
|
|||
if (found) {
|
||||
model.Name = found.Name;
|
||||
model.SubjUID = found.UID;
|
||||
this.setName(model);
|
||||
if (model.wasChanged()) {
|
||||
this.setName(model);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
model.Name = name;
|
||||
model.SubjUID = "";
|
||||
this.confirm.visible = true;
|
||||
|
||||
if (ev && ev.key === "Enter" && !ev.isComposing && !ev.repeat) {
|
||||
this.setName(model);
|
||||
} else {
|
||||
this.confirm.visible = true;
|
||||
}
|
||||
},
|
||||
onConfirmSetName() {
|
||||
if (!this.confirm?.model?.Name) {
|
||||
|
|
@ -360,6 +379,10 @@ export default {
|
|||
this.setName(this.confirm.model);
|
||||
},
|
||||
onCancelSetName() {
|
||||
if (this.confirm && this.confirm.model) {
|
||||
this.confirm.model.Name = "";
|
||||
this.confirm.model.SubjUID = "";
|
||||
}
|
||||
this.confirm.visible = false;
|
||||
},
|
||||
setName(model) {
|
||||
|
|
|
|||
|
|
@ -85,8 +85,8 @@
|
|||
single-line
|
||||
density="comfortable"
|
||||
class="input-name pa-0 ma-0"
|
||||
@blur="onSetName(m, false)"
|
||||
@keyup.enter="onSetName(m, false)"
|
||||
@blur="(ev) => onSetName(m, ev)"
|
||||
@keyup.enter="(ev) => onSetName(m, ev)"
|
||||
></v-text-field>
|
||||
<v-combobox
|
||||
v-else
|
||||
|
|
@ -95,9 +95,9 @@
|
|||
item-title="Name"
|
||||
item-value="Name"
|
||||
:readonly="readonly"
|
||||
:menu-props="menuProps"
|
||||
return-object
|
||||
hide-no-data
|
||||
:menu-props="menuProps"
|
||||
hide-details
|
||||
single-line
|
||||
open-on-clear
|
||||
|
|
@ -105,10 +105,10 @@
|
|||
prepend-inner-icon="mdi-account-plus"
|
||||
autocomplete="off"
|
||||
density="comfortable"
|
||||
class="input-name pa-0 ma-0"
|
||||
@blur="onSetName(m, true)"
|
||||
class="input-name pa-0 ma-0 text-selectable"
|
||||
@update:model-value="(person) => onSetPerson(m, person)"
|
||||
@keyup.enter.native="onSetName(m, false)"
|
||||
@blur="(ev) => onSetName(m, ev)"
|
||||
@keyup.enter="(ev) => onSetName(m, ev)"
|
||||
>
|
||||
</v-combobox>
|
||||
</v-card-actions>
|
||||
|
|
@ -155,6 +155,7 @@ export default {
|
|||
},
|
||||
active: Boolean,
|
||||
},
|
||||
emits: ["updateFaceCount"],
|
||||
data() {
|
||||
const query = this.$route.query;
|
||||
const routeName = this.$route.name;
|
||||
|
|
@ -193,11 +194,18 @@ export default {
|
|||
text: this.$gettext("Add person?"),
|
||||
},
|
||||
menuProps: {
|
||||
closeOnClick: false,
|
||||
openOnClick: true,
|
||||
openOnFocus: true,
|
||||
closeOnBack: true,
|
||||
closeOnContentClick: true,
|
||||
openOnClick: false,
|
||||
persistent: false,
|
||||
scrim: true,
|
||||
openDelay: 0,
|
||||
closeDelay: 0,
|
||||
opacity: 0,
|
||||
density: "compact",
|
||||
maxHeight: 300,
|
||||
scrollStrategy: "reposition",
|
||||
},
|
||||
textRule: (v) => {
|
||||
if (!v || !v.length) {
|
||||
|
|
@ -643,11 +651,16 @@ export default {
|
|||
|
||||
return true;
|
||||
},
|
||||
onSetName(model, confirm) {
|
||||
onSetName(model, ev) {
|
||||
if (this.busy || !model) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If there's a pending confirmation for a different face, don't process new input
|
||||
if (this.confirm.visible && this.confirm.model && this.confirm.model.ID !== model.ID) {
|
||||
return;
|
||||
}
|
||||
|
||||
const name = model?.Name;
|
||||
|
||||
if (!name) {
|
||||
|
|
@ -674,10 +687,12 @@ export default {
|
|||
model.Name = name;
|
||||
model.SubjUID = "";
|
||||
|
||||
if (confirm && model.wasChanged()) {
|
||||
this.confirm.visible = true;
|
||||
} else {
|
||||
this.onConfirmRename();
|
||||
if (model.Name) {
|
||||
if (ev && ev.key === "Enter" && !ev.isComposing && !ev.repeat) {
|
||||
this.setName(model, model.Name);
|
||||
} else {
|
||||
this.confirm.visible = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
onConfirmRename() {
|
||||
|
|
@ -693,6 +708,10 @@ export default {
|
|||
}
|
||||
},
|
||||
onCancelRename() {
|
||||
if (this.confirm && this.confirm.model) {
|
||||
this.confirm.model.Name = "";
|
||||
this.confirm.model.SubjUID = "";
|
||||
}
|
||||
this.confirm.visible = false;
|
||||
},
|
||||
setName(model, newName) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue