-
Notifications
You must be signed in to change notification settings - Fork 84
Enter submits repeatable field-item dialog + autofocus (#127) #322
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from 4 commits
a1a16b9
1fa69d3
eb2b084
95b783f
95f3775
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -65,6 +65,11 @@ export let FieldRenderer = { | |
| modelValue() { | ||
| this.val = this.modelValue; | ||
| this.update(); | ||
| }, | ||
| fieldItem(val) { | ||
| if (val) { | ||
| this.$nextTick(() => this.focusFieldItem()); | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| } | ||
| }, | ||
|
|
||
|
|
@@ -171,6 +176,84 @@ export let FieldRenderer = { | |
| this.fieldItem = null; | ||
| }, | ||
|
|
||
| getFocusableFieldItemInput(dialog) { | ||
|
|
||
| const candidates = dialog.querySelectorAll('input, textarea, select, [contenteditable]'); | ||
|
|
||
| for (const el of candidates) { | ||
|
|
||
| if (el.disabled) { | ||
| continue; | ||
| } | ||
|
|
||
| if (el.hasAttribute('contenteditable') && el.getAttribute('contenteditable') === 'false') { | ||
| continue; | ||
| } | ||
|
|
||
| // skip offscreen / not-yet-sized controls, e.g. CodeMirror's hidden measuring textarea | ||
| if (el.offsetWidth < 10 || el.offsetHeight < 10) { | ||
| continue; | ||
| } | ||
|
|
||
| return el; | ||
| } | ||
|
|
||
| return null; | ||
| }, | ||
|
|
||
| focusFieldItem(attempt = 0) { | ||
|
|
||
| const dialog = document.querySelector(`kiss-dialog[data-field-render-uid="${this.uid}"]`); | ||
|
|
||
| if (!dialog) { | ||
| return; | ||
| } | ||
|
|
||
| const input = this.getFocusableFieldItemInput(dialog); | ||
|
|
||
| if (input) { | ||
| input.focus(); | ||
| return; | ||
| } | ||
|
|
||
| // the nested field-renderer's own fieldTypes is resolved asynchronously, | ||
| // so its input may not exist in the DOM yet — poll a bounded number of frames | ||
| if (attempt >= 60) { | ||
| return; | ||
| } | ||
|
|
||
| requestAnimationFrame(() => this.focusFieldItem(attempt + 1)); | ||
| }, | ||
|
|
||
| onFieldItemKeydown(evt) { | ||
|
|
||
| // ignore Enter that commits an IME (CJK/etc) composition | ||
| if (evt.isComposing) { | ||
| return; | ||
| } | ||
|
|
||
| // another widget on the same element (e.g. app-tags) already handled this | ||
| // keydown and called preventDefault() - don't also save/close the dialog | ||
| if (evt.defaultPrevented) { | ||
| return; | ||
| } | ||
|
|
||
| const tag = evt.target.tagName; | ||
|
|
||
| // buttons/links already trigger their own action on enter, don't also save | ||
| if (tag === 'BUTTON' || tag === 'A') { | ||
| return; | ||
| } | ||
|
|
||
| // let textareas / contenteditable areas keep their own newline behaviour | ||
| if (tag === 'TEXTAREA' || evt.target.isContentEditable) { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Blocking (correctness): the Concrete counter-example in this repo — the case "Enter":
e.preventDefault();
... this.addTag(inputValue);Its editable control is a plain Two further gaps that
Suggested shape: move to
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 1fa69d3, taking the suggested shape: switched to |
||
| return; | ||
| } | ||
|
|
||
| evt.preventDefault(); | ||
| this.saveFieldItem(); | ||
| }, | ||
|
|
||
| removeFieldItem(list, index) { | ||
| list.splice(index, 1); | ||
| }, | ||
|
|
@@ -241,7 +324,7 @@ export let FieldRenderer = { | |
|
|
||
| <teleport to="body"> | ||
| <kiss-dialog open="true" size="large" :data-field-render-uid="uid" v-if="fieldItem"> | ||
| <kiss-content class="animated fadeInUp faster"> | ||
| <kiss-content class="animated fadeInUp faster" @keydown.enter="onFieldItemKeydown"> | ||
|
|
||
| <div class="kiss-flex kiss-flex-middle"> | ||
| <div> | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Blocking (correctness): this autofocus is almost certainly a no-op — the dialog has no input yet when
focusFieldItem()runs.The dialog body renders the field via
<fields-renderer>→field-renderer, and the innerFieldRenderertemplate is gated onv-if="fieldTypes".fieldTypesis only populated in its ownmounted()hook, fromFieldTypes.get()(modules/System/assets/js/settings.js), which is declaredasync get()— so even on the cachedthis._fieldspath it resolves through extra microtask ticks, and assigningthis.fieldTypesthen requires a further render flush.Microtask ordering when
fieldItemis set:field-renderermounts withfieldTypes === null→ renders nothing;FieldTypes.get().then(...)queued.$nextTickcallback (registered on the flush promise) →focusFieldItem()runs.fieldTypesget assigned and a second flush render the actual<input>.At step 2
dialog.querySelector("input, textarea, select, [contenteditable]")returnsnull,if (input)short-circuits, and nothing is focused — silently. So the second half of #127 ("input field should be auto focused") is not actually delivered, and the failure mode is invisible because of the null guard.Please verify in a browser. If confirmed, focus needs to be driven by something that waits for the field to exist — e.g. focus from the inner component once it has rendered, or a bounded
MutationObserver/ retry on the dialog element — not a single$nextTick.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Confirmed and fixed in 1fa69d3.
focusFieldItem()now polls via a boundedrequestAnimationFrameloop (~60 frames) until a genuinely focusable control exists in the dialog, instead of relying on a single $nextTick that fires before the nested field-renderer's async fieldTypes resolves.