diff --git a/src/Modals/FluidPropertyCreatePrompt/FluidPropertyCreatePrompt.ts b/src/Modals/FluidPropertyCreatePrompt/FluidPropertyCreatePrompt.ts index af075af..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; diff --git a/src/Modals/NativePropertyPrompt/NativePropertyPrompt.ts b/src/Modals/NativePropertyPrompt/NativePropertyPrompt.ts index 56ae9c7..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; 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/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 })); 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(