Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/gui/choiceList/AddChoiceControls.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts">
import { Menu } from "obsidian";
import { Menu, Platform } from "obsidian";
import type { ChoiceType } from "../../types/choices/choiceType";
import ObsidianIcon from "../components/ObsidianIcon.svelte";
import { DOER_CHOICE_TYPES, defaultChoiceName } from "./choiceTypeMeta";
Expand Down Expand Up @@ -39,7 +39,8 @@
for (const meta of DOER_CHOICE_TYPES) {
menu.addItem((item) =>
item
.setTitle(`${meta.label} — ${meta.description}`)
// A phone's menu rows are one ellipsized line, too narrow for the description.
.setTitle(Platform.isPhone ? meta.label : `${meta.label} — ${meta.description}`)
.setIcon(meta.iconId)
.onClick((clickEvt) => {
// Alt/⌥ scaffolds without opening the builder (batch path).
Expand Down
35 changes: 35 additions & 0 deletions src/gui/choiceList/AddChoiceControls.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { fireEvent, render } from "@testing-library/svelte";
import { Menu, Platform } from "obsidian";
import AddChoiceControls from "./AddChoiceControls.svelte";
import { DOER_CHOICE_TYPES } from "./choiceTypeMeta";

// The vitest obsidian stub records the last Menu shown.
const MenuStub = Menu as unknown as { lastShown: { items: Array<{ title: string }> } | null };

describe("New choice menu", () => {
afterEach(() => {
Platform.isPhone = false;
});

const titles = async () => {
const { getByLabelText } = render(AddChoiceControls, { props: { onAddChoice: vi.fn() } });
await fireEvent.click(getByLabelText("New choice"));
return MenuStub.lastShown?.items.map((item) => item.title);
};

it("describes each choice type on desktop", async () => {
const shown = await titles();
expect(shown).toHaveLength(DOER_CHOICE_TYPES.length);
DOER_CHOICE_TYPES.forEach((meta, index) => {
expect(shown?.[index]).toContain(meta.label);
expect(shown?.[index]).toContain(meta.description);
});
});

// A phone's menu rows are one ellipsized line, which cut the descriptions mid-word.
it("names only the choice type on a phone", async () => {
Platform.isPhone = true;
expect(await titles()).toEqual(["Template", "Capture", "Macro"]);
});
});
20 changes: 16 additions & 4 deletions src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -1137,6 +1137,20 @@
margin-right: 1em;
}

/* On phones Obsidian makes every input and button in a modal's setting control
full width, so the row's input and its Add button split it and the input's
placeholder is cut mid-word. Give the input the room and the buttons their
own width. */
.is-phone .quickAddModal .setting-item-control:has(> .qa-command-sequence-input) > .qa-command-sequence-input {
flex: 1 1 auto;
min-width: 0;
}

.is-phone .quickAddModal .setting-item-control:has(> .qa-command-sequence-input) > button:not(.clickable-icon) {
flex: 0 0 auto;
width: auto;
}

/* The command editor renders into its own wrapper, so its last row ("Choices")
matches Obsidian's modal `.setting-item:last-child { padding-bottom: 0 }` even
though the Macro builder's own settings follow it. Restore the row padding so
Expand Down Expand Up @@ -1819,22 +1833,20 @@
flex: 1;
}

/* The margin keeps the next row's top rule off the list's border. */
.qaMultiSuggester .qa-multi-list {
display: flex;
flex: 1 1 auto;
flex-direction: column;
min-height: 0;
margin-bottom: var(--size-4-2);
}

.qaMultiSuggester .qa-multi-custom,
.qaMultiSuggester .qa-multi-actions {
flex: 0 0 auto;
}

.qaMultiSuggester .qa-multi-custom {
margin-top: var(--size-4-2);
}

/* Priority indicators - subtle left border */
.qa-suggest-exact::before {
content: '';
Expand Down
56 changes: 56 additions & 0 deletions tests/e2e/macro-builder-layout.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { expect, it } from "vitest";
import { MacroChoice } from "../../src/types/choices/MacroChoice";
import type IChoice from "../../src/types/choices/IChoice";
import { createQuickAddE2EHarness } from "./e2eVault";
import { POLL_OPTS, waitForElement } from "./uiHelpers";

const getContext = createQuickAddE2EHarness("macro-builder-layout");

// #2128: on a phone Obsidian makes every input and button in a modal's setting
// row full width, so a row's input and its Add button split it and the
// placeholders were cut mid-word.
it("gives the macro builder's inputs the row on a phone", async () => {
const { obsidian, plugin } = getContext();
const macro = new MacroChoice("Phone layout macro");
await plugin.data<{ choices: IChoice[] }>().patch((data) => {
data.choices = [macro];
});
await plugin.reload({ waitUntilReady: true });
try {
await obsidian.dev.evalJson("app.setting.open(); app.setting.openTabById('quickadd'); true");
await waitForElement(obsidian, '[aria-label="Configure Phone layout macro"]');
await obsidian.dev.evalJson(`document.querySelector('[aria-label="Configure Phone layout macro"]').click(), true`);
await waitForElement(obsidian, ".macroBuilder .qa-command-sequence-input");
await obsidian.dev.evalJson(`(() => {
window.__qaPhoneClasses = document.body.className;
document.body.classList.remove("is-tablet");
document.body.classList.add("is-mobile", "is-phone");
return true;
})()`);

const rows = await obsidian.dev.evalJson<{ field: number; button: number }[]>(`(() =>
[...document.querySelectorAll(".macroBuilder .setting-item-control:has(> .qa-command-sequence-input)")].map((row) => {
const button = [...row.querySelectorAll(":scope > button")].find((b) => b.getClientRects().length);
return {
field: row.querySelector(":scope > .qa-command-sequence-input").getBoundingClientRect().width,
button: button.getBoundingClientRect().width,
};
}))()`);
expect(rows).toHaveLength(4);
for (const { field, button } of rows) {
expect(button).toBeLessThan(100);
expect(field).toBeGreaterThan(button * 2);
}
} finally {
await obsidian.dev.evalJson(`(() => {
if (window.__qaPhoneClasses !== undefined) document.body.className = window.__qaPhoneClasses;
delete window.__qaPhoneClasses;
[...document.querySelectorAll(".macroBuilder button")].find((b) => b.textContent.trim() === "Done")?.click();
app.setting.close();
return true;
})()`);
await expect.poll(() => obsidian.dev.evalJson<number>(
'document.querySelectorAll(".modal-container").length',
), POLL_OPTS).toBe(0);
}
});
32 changes: 32 additions & 0 deletions tests/e2e/multi-select-layout.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { expect, it } from "vitest";
import { createQuickAddE2EHarness } from "./e2eVault";
import { jsLiteral, POLL_OPTS, pressKey, waitForElement } from "./uiHelpers";

const getContext = createQuickAddE2EHarness("multi-select-layout");

// #2128: without the custom-value row, the footer's top rule sat on the list's
// bottom border (0px apart; 8px with the custom row).
it.each([
["without", "{{VALUE:Alpha,Beta,Gamma|multi}}"],
["with", "{{VALUE:Alpha,Beta,Gamma|multi|custom}}"],
])("keeps the multi-select list off the next row %s a custom-value row", async (_name, format) => {
const { obsidian } = getContext();
await obsidian.dev.evalJson(`(() => {
app.plugins.plugins.quickadd.api.format(${jsLiteral(format)}).catch(() => {});
return true;
})()`);
try {
await waitForElement(obsidian, ".qaMultiSuggester .qa-searchable-multi-select__list");
const gap = await obsidian.dev.evalJson<number>(`(() => {
const list = document.querySelector(".qaMultiSuggester .qa-searchable-multi-select__list").getBoundingClientRect();
const next = document.querySelector(".qaMultiSuggester .qa-multi-list").nextElementSibling.getBoundingClientRect();
return Math.round(next.top - list.bottom);
})()`);
expect(gap).toBe(8);
} finally {
await pressKey(obsidian, "Escape");
await expect.poll(() => obsidian.dev.evalJson<boolean>(
'Boolean(document.querySelector(".qaMultiSuggester"))',
), POLL_OPTS).toBe(false);
}
});
Loading