From 2a68cce7ca65e373c2d69c135b156a9fc9eb2ca5 Mon Sep 17 00:00:00 2001 From: Christian Bager Bach Houmann Date: Sat, 24 Oct 2026 09:51:13 +0200 Subject: [PATCH 1/4] test(e2e): pin initial focus of the native edit and create prompts Obsidian 1.13's Modal.open() focuses the modal's first focusable element after onOpen(). In both native prompts that element is the type pill, so focus never reaches the value editor (edit prompt) or the key input (create modal). In the edit prompt, the value autocomplete popover the editor opened is left orphaned and its keymap scope swallows Enter on a pill. Both tests fail on the current plugin. --- tests/e2e/fluid-create.test.ts | 26 ++++++++++++++++++++++ tests/e2e/native-properties.test.ts | 34 +++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/tests/e2e/fluid-create.test.ts b/tests/e2e/fluid-create.test.ts index 1a2f367..3898833 100644 --- a/tests/e2e/fluid-create.test.ts +++ b/tests/e2e/fluid-create.test.ts @@ -81,6 +81,32 @@ describe("MetaEdit fluid property creation", () => { expect(await obsidian.dev.runtimeErrors()).toEqual([]); }); + // The type pill precedes the key input, so Obsidian 1.13's Modal.open() would + // move focus onto it after onOpen() unless the modal opts out. + test("the create modal opens with focus in the key input", async () => { + const {obsidian, sandbox} = getContext(); + const notePath = sandbox.path("fluid-create-focus.md"); + await writeLiveFile(obsidian, notePath, "---\n---\nbody\n"); + + const result = await evalJsonAsync<{keyInputFocused: boolean}>( + obsidian, + ` + (async () => { + ${FLUID_HELPERS} + const file = app.vault.getAbstractFileByPath(${JSON.stringify(notePath)}); + const {promise, modal} = await openCreate(file); + const keyInputFocused = document.activeElement === modal.querySelector(".metaedit-fluid-create-key"); + cancel(modal); + await promise; + return {keyInputFocused}; + })() + `, + ); + + expect(result.keyInputFocused).toBe(true); + expect(await obsidian.dev.runtimeErrors()).toEqual([]); + }); + test("layout: no horizontal overflow, compact height, and the floating dropdown never covers the buttons", async () => { const {obsidian, sandbox} = getContext(); const notePath = sandbox.path("fluid-create-layout.md"); diff --git a/tests/e2e/native-properties.test.ts b/tests/e2e/native-properties.test.ts index b9c512a..671d033 100644 --- a/tests/e2e/native-properties.test.ts +++ b/tests/e2e/native-properties.test.ts @@ -283,6 +283,40 @@ describe("MetaEdit native Obsidian property widgets", () => { expect(result.content).toContain("summary: old"); expect(await obsidian.dev.runtimeErrors()).toEqual([]); }); + + // Obsidian 1.13's Modal.open() moves focus to the modal's first focusable + // element - the type pill - after onOpen() unless the modal opts out. The + // sibling note gives the value autocomplete a suggestion, so the popover the + // value editor opened would be left orphaned and swallow the pill's Enter. + test("opens the edit prompt focused on the value editor and edits a pill in place", async () => { + const {obsidian, sandbox} = getContext(); + const notePath = sandbox.path("native-focus.md"); + await writeLiveFile(obsidian, sandbox.path("native-focus-sibling.md"), "---\nreviewers:\n - sibling\n---\nbody\n"); + await writeLiveFile(obsidian, notePath, "---\nreviewers:\n - alpha\n - beta\n---\nbody\n"); + + const result = await evalJsonAsync<{focusedValueEditor: boolean; pills: string[]; content: string}>( + obsidian, + ` + (async () => { + ${NATIVE_PROMPT_HELPERS_JS} + const file = app.vault.getAbstractFileByPath(${JSON.stringify(notePath)}); + app.metadataTypeManager.setType("reviewers", "multitext"); + await sleep(200); + + const {promise, host} = await openNative(file, "reviewers"); + const focusedValueEditor = document.activeElement === host.querySelector(".multi-select-input"); + const pills = await editPillByText(host, "alpha", "gamma"); + await saveOpenModal(promise); + return {focusedValueEditor, pills, content: await app.vault.read(file)}; + })() + `, + ); + + expect(result.focusedValueEditor).toBe(true); + expect(result.pills).toEqual(["gamma", "beta"]); + expect(result.content).toBe("---\nreviewers:\n - gamma\n - beta\n---\nbody\n"); + expect(await obsidian.dev.runtimeErrors()).toEqual([]); + }); }); async function verifyWikilinkSuggestion( From f2e2fccf98a9e9514c41cf75eaf21eea960ecd99 Mon Sep 17 00:00:00 2001 From: Christian Bager Bach Houmann Date: Sat, 24 Oct 2026 09:52:23 +0200 Subject: [PATCH 2/4] fix: keep initial focus on the value editor in the native prompts Obsidian 1.13's Modal.open() runs onOpen() and then focuses the modal's first focusable element unless hasInitialInputFocus is false. Both native prompts render the type pill first, so focus left the value editor (edit prompt) and the key input (create modal) the moment they opened. Typing went nowhere, and in the edit prompt the value autocomplete popover the editor had opened stayed open with its keymap scope, swallowing Enter. Opt both modals out so their own onOpen() focus stands, as Obsidian's own focus-managing modals do. --- .../FluidPropertyCreatePrompt/FluidPropertyCreatePrompt.ts | 4 ++++ src/Modals/NativePropertyPrompt/NativePropertyPrompt.ts | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/Modals/FluidPropertyCreatePrompt/FluidPropertyCreatePrompt.ts b/src/Modals/FluidPropertyCreatePrompt/FluidPropertyCreatePrompt.ts index af075af..c7cd33a 100644 --- a/src/Modals/FluidPropertyCreatePrompt/FluidPropertyCreatePrompt.ts +++ b/src/Modals/FluidPropertyCreatePrompt/FluidPropertyCreatePrompt.ts @@ -75,6 +75,10 @@ export default class FluidPropertyCreatePrompt extends Modal { private constructor(app: App, options: FluidPropertyCreateOptions) { super(app); + // onOpen() focuses the key input. hasInitialInputFocus is a private Modal flag; + // left on, Obsidian 1.13+ then moves focus to the first focusable element + // (the type pill). + (this as unknown as {hasInitialInputFocus: boolean}).hasInitialInputFocus = false; this.opts = options; this.waitForClose = new Promise((resolve) => { this.resolvePromise = resolve; diff --git a/src/Modals/NativePropertyPrompt/NativePropertyPrompt.ts b/src/Modals/NativePropertyPrompt/NativePropertyPrompt.ts index 56ae9c7..d5c3431 100644 --- a/src/Modals/NativePropertyPrompt/NativePropertyPrompt.ts +++ b/src/Modals/NativePropertyPrompt/NativePropertyPrompt.ts @@ -54,6 +54,10 @@ export default class NativePropertyPrompt extends Modal { private constructor(app: App, file: TFile, property: Property) { super(app); + // onOpen() focuses the value editor. hasInitialInputFocus is a private Modal flag; + // left on, Obsidian 1.13+ then moves focus to the first focusable element + // (the type pill) - orphaning the value autocomplete popover. + (this as unknown as {hasInitialInputFocus: boolean}).hasInitialInputFocus = false; this.file = file; this.property = property; this.waitForClose = new Promise((resolve) => { From e323b7db93ebf87097f1c80842d63c07ae011ffb Mon Sep 17 00:00:00 2001 From: Christian Bager Bach Houmann Date: Sat, 24 Oct 2026 09:52:59 +0200 Subject: [PATCH 3/4] test(e2e): scope the legacy-suggester probe to SuggestModal rows driveNativeEdit flagged any `.suggestion-item` as the legacy GenericSuggester. Obsidian's own property-value autocomplete renders the same class in a body-level `.suggestion-container` whenever a native editor has focus, so the scalar tests failed on Obsidian's popover, not on MetaEdit UI. Match only rows inside a SuggestModal's `.prompt-results`, and have the legacy driver wait on the same selector so its passing tests prove the probe can see a real GenericSuggester. --- tests/e2e/multi-value.test.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/e2e/multi-value.test.ts b/tests/e2e/multi-value.test.ts index 6e1c07c..4033904 100644 --- a/tests/e2e/multi-value.test.ts +++ b/tests/e2e/multi-value.test.ts @@ -4,6 +4,13 @@ import { NATIVE_PROMPT_HELPERS_JS } from "./nativePromptHelpers"; const getContext = createMetaEditE2EHarness("multi-value"); +// The legacy GenericSuggester is a SuggestModal, whose rows sit in +// `.prompt-results`. Obsidian's own property-value autocomplete also renders +// `.suggestion-item` rows (in a body-level `.suggestion-container`) whenever a +// native editor has focus, so a bare `.suggestion-item` cannot tell them apart. +// The legacy driver waits on this same selector, proving the probe can see it. +const LEGACY_SUGGESTION_SELECTOR = ".prompt-results .suggestion-item"; + // Live regressions for the multi-value / array editing cluster (#94, #51, #31, #36). // // Since PR #168, `editMetaElement` routes eligible top-level YAML properties @@ -332,7 +339,7 @@ async function driveNativeEdit( } try { const {promise, host} = await openNative(file, ${JSON.stringify(opts.key)}); - const sawLegacySuggester = Boolean(document.querySelector(".suggestion-item")); + const sawLegacySuggester = Boolean(document.querySelector(${JSON.stringify(LEGACY_SUGGESTION_SELECTOR)})); const sawLegacyPrompt = Boolean(document.querySelector(".metaEditPromptInput")); let pills = null; if (action.kind === "editPill") pills = await editPillByText(host, action.from, action.to); @@ -378,7 +385,7 @@ async function driveLegacyListEdit( const editPromise = plugin.controller.editMetaElement(property, props, file); const item = await waitFor( - ".suggestion-item", + ${JSON.stringify(LEGACY_SUGGESTION_SELECTOR)}, (el) => el.textContent?.trim() === ${JSON.stringify(opts.selectText)}, ); item.dispatchEvent(new MouseEvent("mousedown", { bubbles: true })); From f561859f254ce1611c4603f515cc8404e7f87b2c Mon Sep 17 00:00:00 2001 From: Christian Bager Bach Houmann Date: Sat, 24 Oct 2026 11:11:17 +0200 Subject: [PATCH 4/4] refactor: declare the focus opt-out as a class field instead of a cast --- .../FluidPropertyCreatePrompt.ts | 8 ++++---- src/Modals/NativePropertyPrompt/NativePropertyPrompt.ts | 9 +++++---- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/Modals/FluidPropertyCreatePrompt/FluidPropertyCreatePrompt.ts b/src/Modals/FluidPropertyCreatePrompt/FluidPropertyCreatePrompt.ts index c7cd33a..d6db15c 100644 --- a/src/Modals/FluidPropertyCreatePrompt/FluidPropertyCreatePrompt.ts +++ b/src/Modals/FluidPropertyCreatePrompt/FluidPropertyCreatePrompt.ts @@ -44,6 +44,10 @@ export interface FluidPropertyCreateOptions { * value, `autoProperty` to hand a key back to its Auto Property flow, or `cancel`. */ export default class FluidPropertyCreatePrompt extends Modal { + // onOpen() focuses the key input. Obsidian 1.13+ (undocumented flag) would then + // move focus to the first focusable element, the type pill. + hasInitialInputFocus = false; + private readonly opts: FluidPropertyCreateOptions; private resolvePromise: (result: FluidPropertyCreateResult) => void; @@ -75,10 +79,6 @@ export default class FluidPropertyCreatePrompt extends Modal { private constructor(app: App, options: FluidPropertyCreateOptions) { super(app); - // onOpen() focuses the key input. hasInitialInputFocus is a private Modal flag; - // left on, Obsidian 1.13+ then moves focus to the first focusable element - // (the type pill). - (this as unknown as {hasInitialInputFocus: boolean}).hasInitialInputFocus = false; this.opts = options; this.waitForClose = new Promise((resolve) => { this.resolvePromise = resolve; diff --git a/src/Modals/NativePropertyPrompt/NativePropertyPrompt.ts b/src/Modals/NativePropertyPrompt/NativePropertyPrompt.ts index d5c3431..6376dff 100644 --- a/src/Modals/NativePropertyPrompt/NativePropertyPrompt.ts +++ b/src/Modals/NativePropertyPrompt/NativePropertyPrompt.ts @@ -25,6 +25,11 @@ function isReservedTypeKey(key: string): boolean { } export default class NativePropertyPrompt extends Modal { + // onOpen() focuses the value editor. Obsidian 1.13+ (undocumented flag) would then + // move focus to the first focusable element, the type pill, orphaning the value + // autocomplete popover. + hasInitialInputFocus = false; + private readonly file: TFile; private readonly property: Property; private resolvePromise: (result: NativePropertyPromptResult) => void; @@ -54,10 +59,6 @@ export default class NativePropertyPrompt extends Modal { private constructor(app: App, file: TFile, property: Property) { super(app); - // onOpen() focuses the value editor. hasInitialInputFocus is a private Modal flag; - // left on, Obsidian 1.13+ then moves focus to the first focusable element - // (the type pill) - orphaning the value autocomplete popover. - (this as unknown as {hasInitialInputFocus: boolean}).hasInitialInputFocus = false; this.file = file; this.property = property; this.waitForClose = new Promise((resolve) => {