diff --git a/src/ChatBasedContentEditor/Presentation/Resources/assets/controllers/prompt_suggestions_controller.ts b/src/ChatBasedContentEditor/Presentation/Resources/assets/controllers/prompt_suggestions_controller.ts index d54c025..f9b8d90 100644 --- a/src/ChatBasedContentEditor/Presentation/Resources/assets/controllers/prompt_suggestions_controller.ts +++ b/src/ChatBasedContentEditor/Presentation/Resources/assets/controllers/prompt_suggestions_controller.ts @@ -7,6 +7,7 @@ import { Controller } from "@hotwired/stimulus"; export default class extends Controller { static targets = [ "suggestion", + "suggestionRow", "expandButton", "collapseButton", "expandCollapseWrapper", @@ -39,6 +40,7 @@ export default class extends Controller { }; declare readonly suggestionTargets: HTMLButtonElement[]; + declare readonly suggestionRowTargets: HTMLElement[]; declare readonly hasExpandButtonTarget: boolean; declare readonly expandButtonTarget: HTMLButtonElement; declare readonly hasCollapseButtonTarget: boolean; @@ -76,6 +78,15 @@ export default class extends Controller { /** Index of the suggestion pending deletion */ deleteIndex: number | null = null; + /** Whether the suggestions list is currently expanded */ + expanded = false; + + connect(): void { + this.expanded = false; + this.applySuggestionVisibility(); + this.updateExpandCollapseState(this.getSuggestionRows().length); + } + // ─── Display: insert, hover, expand/collapse ──────────────── /** @@ -114,41 +125,18 @@ export default class extends Controller { * Show all hidden suggestions and toggle expand/collapse buttons. */ expand(): void { - // Show all hidden suggestion rows (the parent wrapper divs) - this.suggestionTargets.forEach((button) => { - const row = button.closest("[data-index]") as HTMLElement | null; - if (row) { - row.classList.remove("hidden"); - } - }); - - if (this.hasExpandButtonTarget) { - this.expandButtonTarget.classList.add("hidden"); - } - if (this.hasCollapseButtonTarget) { - this.collapseButtonTarget.classList.remove("hidden"); - } + this.expanded = true; + this.applySuggestionVisibility(); + this.updateExpandCollapseState(this.getSuggestionRows().length); } /** * Hide suggestions beyond maxVisible and toggle expand/collapse buttons. */ collapse(): void { - this.suggestionTargets.forEach((button, index) => { - if (index >= this.maxVisibleValue) { - const row = button.closest("[data-index]") as HTMLElement | null; - if (row) { - row.classList.add("hidden"); - } - } - }); - - if (this.hasExpandButtonTarget) { - this.expandButtonTarget.classList.remove("hidden"); - } - if (this.hasCollapseButtonTarget) { - this.collapseButtonTarget.classList.add("hidden"); - } + this.expanded = false; + this.applySuggestionVisibility(); + this.updateExpandCollapseState(this.getSuggestionRows().length); } // ─── Add / Edit modal ─────────────────────────────────────── @@ -363,6 +351,7 @@ export default class extends Controller { const row = document.createElement("div"); row.className = "group flex items-start gap-1"; row.dataset.index = String(index); + row.dataset.promptSuggestionsTarget = "suggestionRow"; const button = document.createElement("button"); button.type = "button"; @@ -381,10 +370,6 @@ export default class extends Controller { actions.className = "flex-shrink-0 flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity"; - if (index >= this.maxVisibleValue) { - row.classList.add("hidden"); - } - // Edit button const editBtn = document.createElement("button"); editBtn.type = "button"; @@ -414,7 +399,8 @@ export default class extends Controller { container.appendChild(row); }); - // Update expand/collapse buttons based on the new suggestion count + this.expanded = false; + this.applySuggestionVisibility(); this.updateExpandCollapseState(suggestions.length); } @@ -432,23 +418,44 @@ export default class extends Controller { if (hiddenCount > 0) { this.expandCollapseWrapperTarget.classList.remove("hidden"); - // Reset to collapsed state if (this.hasExpandButtonTarget) { - this.expandButtonTarget.classList.remove("hidden"); this.expandButtonTarget.textContent = this.showMoreTemplateValue.replace( "{count}", String(hiddenCount), ); + this.expandButtonTarget.classList.toggle("hidden", this.expanded); + this.expandButtonTarget.setAttribute("aria-expanded", String(this.expanded)); } if (this.hasCollapseButtonTarget) { - this.collapseButtonTarget.classList.add("hidden"); this.collapseButtonTarget.textContent = this.showLessLabelValue; + this.collapseButtonTarget.classList.toggle("hidden", !this.expanded); + this.collapseButtonTarget.setAttribute("aria-expanded", String(this.expanded)); } } else { + this.expanded = false; this.expandCollapseWrapperTarget.classList.add("hidden"); } } + private getSuggestionRows(): HTMLElement[] { + if (!this.hasSuggestionListTarget) { + return this.suggestionRowTargets; + } + + return Array.from( + this.suggestionListTarget.querySelectorAll( + '[data-prompt-suggestions-target~="suggestionRow"]', + ), + ); + } + + private applySuggestionVisibility(): void { + this.getSuggestionRows().forEach((row, index) => { + const shouldHide = !this.expanded && index >= this.maxVisibleValue; + row.classList.toggle("hidden", shouldHide); + }); + } + private sanitizeText(raw: string): string { return ( raw diff --git a/src/ChatBasedContentEditor/Presentation/Resources/templates/_prompt_suggestions.twig b/src/ChatBasedContentEditor/Presentation/Resources/templates/_prompt_suggestions.twig index 2f1529f..7ad52e8 100644 --- a/src/ChatBasedContentEditor/Presentation/Resources/templates/_prompt_suggestions.twig +++ b/src/ChatBasedContentEditor/Presentation/Resources/templates/_prompt_suggestions.twig @@ -31,9 +31,12 @@ {# Suggestions list #}
{% for suggestion in promptSuggestions %} -
+
diff --git a/tests/frontend/unit/ChatBasedContentEditor/prompt_suggestions_controller.test.ts b/tests/frontend/unit/ChatBasedContentEditor/prompt_suggestions_controller.test.ts index 449da19..f8ac6dd 100644 --- a/tests/frontend/unit/ChatBasedContentEditor/prompt_suggestions_controller.test.ts +++ b/tests/frontend/unit/ChatBasedContentEditor/prompt_suggestions_controller.test.ts @@ -1,10 +1,12 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { Application } from "@hotwired/stimulus"; import PromptSuggestionsController from "../../../../src/ChatBasedContentEditor/Presentation/Resources/assets/controllers/prompt_suggestions_controller.ts"; describe("PromptSuggestionsController", () => { interface ControllerFixture { controller: PromptSuggestionsController; suggestionButtons: HTMLButtonElement[]; + suggestionRows: HTMLElement[]; expandButton: HTMLButtonElement; collapseButton: HTMLButtonElement; expandCollapseWrapper: HTMLElement; @@ -22,9 +24,11 @@ describe("PromptSuggestionsController", () => { // Create suggestion buttons wrapped in row divs const suggestionList = document.createElement("div"); const suggestionButtons: HTMLButtonElement[] = []; + const suggestionRows: HTMLElement[] = []; for (let i = 0; i < 5; i++) { const row = document.createElement("div"); row.dataset.index = String(i); + row.dataset.promptSuggestionsTarget = "suggestionRow"; row.className = "group flex items-start gap-1"; if (i >= 3) { @@ -41,6 +45,7 @@ describe("PromptSuggestionsController", () => { row.appendChild(btn); row.appendChild(actions); suggestionList.appendChild(row); + suggestionRows.push(row); } // Create expand/collapse buttons wrapped in a container @@ -64,6 +69,7 @@ describe("PromptSuggestionsController", () => { // Set up controller state const state = controller as unknown as { suggestionTargets: HTMLButtonElement[]; + suggestionRowTargets: HTMLElement[]; hasExpandButtonTarget: boolean; expandButtonTarget: HTMLButtonElement; hasCollapseButtonTarget: boolean; @@ -96,6 +102,7 @@ describe("PromptSuggestionsController", () => { }; state.suggestionTargets = suggestionButtons; + state.suggestionRowTargets = suggestionRows; state.hasExpandButtonTarget = true; state.expandButtonTarget = expandButton; state.hasCollapseButtonTarget = true; @@ -129,6 +136,7 @@ describe("PromptSuggestionsController", () => { return { controller, suggestionButtons, + suggestionRows, expandButton, collapseButton, expandCollapseWrapper, @@ -780,3 +788,154 @@ describe("PromptSuggestionsController", () => { }); }); }); + +describe("PromptSuggestionsController click integration", () => { + let application: Application; + + const flushMicrotasks = async (): Promise => { + await Promise.resolve(); + await Promise.resolve(); + }; + + const buildControllerHtml = (count: number): string => { + const rows = Array.from({ length: count }) + .map((_, index) => { + const hiddenClass = index >= 3 ? "hidden" : ""; + return ` +
+ +
+ `; + }) + .join(""); + + const hiddenCount = Math.max(count - 3, 0); + + return ` +
+
+ ${rows} +
+
+ + +
+
+ `; + }; + + beforeEach(() => { + document.body.innerHTML = ""; + application = Application.start(); + application.register("prompt-suggestions", PromptSuggestionsController); + }); + + afterEach(() => { + application.stop(); + vi.restoreAllMocks(); + }); + + it("clicking +X more reveals hidden suggestions and toggles buttons", async () => { + document.body.innerHTML = buildControllerHtml(5); + await flushMicrotasks(); + + const expandButton = document.querySelector( + '[data-prompt-suggestions-target="expandButton"]', + ) as HTMLButtonElement; + const collapseButton = document.querySelector( + '[data-prompt-suggestions-target="collapseButton"]', + ) as HTMLButtonElement; + const rows = Array.from( + document.querySelectorAll('[data-prompt-suggestions-target="suggestionRow"]'), + ) as HTMLElement[]; + + expect(rows[3].classList.contains("hidden")).toBe(true); + expect(rows[4].classList.contains("hidden")).toBe(true); + + expandButton.click(); + await flushMicrotasks(); + + rows.forEach((row) => { + expect(row.classList.contains("hidden")).toBe(false); + }); + expect(expandButton.classList.contains("hidden")).toBe(true); + expect(collapseButton.classList.contains("hidden")).toBe(false); + expect(expandButton.getAttribute("aria-expanded")).toBe("true"); + expect(collapseButton.getAttribute("aria-expanded")).toBe("true"); + }); + + it("clicking Show less restores collapsed state after expansion", async () => { + document.body.innerHTML = buildControllerHtml(5); + await flushMicrotasks(); + + const expandButton = document.querySelector( + '[data-prompt-suggestions-target="expandButton"]', + ) as HTMLButtonElement; + const collapseButton = document.querySelector( + '[data-prompt-suggestions-target="collapseButton"]', + ) as HTMLButtonElement; + const rows = Array.from( + document.querySelectorAll('[data-prompt-suggestions-target="suggestionRow"]'), + ) as HTMLElement[]; + + expandButton.click(); + await flushMicrotasks(); + collapseButton.click(); + await flushMicrotasks(); + + expect(rows[0].classList.contains("hidden")).toBe(false); + expect(rows[1].classList.contains("hidden")).toBe(false); + expect(rows[2].classList.contains("hidden")).toBe(false); + expect(rows[3].classList.contains("hidden")).toBe(true); + expect(rows[4].classList.contains("hidden")).toBe(true); + expect(expandButton.classList.contains("hidden")).toBe(false); + expect(collapseButton.classList.contains("hidden")).toBe(true); + expect(expandButton.getAttribute("aria-expanded")).toBe("false"); + expect(collapseButton.getAttribute("aria-expanded")).toBe("false"); + }); + + it("renders the correct +X more label for larger suggestion lists", async () => { + document.body.innerHTML = buildControllerHtml(8); + await flushMicrotasks(); + + const expandButton = document.querySelector( + '[data-prompt-suggestions-target="expandButton"]', + ) as HTMLButtonElement; + const rows = Array.from( + document.querySelectorAll('[data-prompt-suggestions-target="suggestionRow"]'), + ) as HTMLElement[]; + + expect(expandButton.textContent?.trim()).toBe("+5 more"); + expect(rows[7].classList.contains("hidden")).toBe(true); + + expandButton.click(); + await flushMicrotasks(); + + rows.forEach((row) => { + expect(row.classList.contains("hidden")).toBe(false); + }); + }); +});