diff --git a/src/content/ui/QuestionPanel.svelte b/src/content/ui/QuestionPanel.svelte index 523ec54..fe827e8 100644 --- a/src/content/ui/QuestionPanel.svelte +++ b/src/content/ui/QuestionPanel.svelte @@ -149,6 +149,53 @@ return Boolean(editableParent && editableParent.getAttribute("contenteditable") !== "false"); } + function stopNativeEvent(event) { + event?.stopPropagation?.(); + event?.stopImmediatePropagation?.(); + } + + function focusInputInContainer(container) { + const input = container?.querySelector?.("input"); + if (input) setTimeout(() => input.focus(), 0); + } + + function stopPanelPointerEvents(node) { + const events = ["pointerdown", "mousedown"]; + events.forEach((eventName) => node.addEventListener(eventName, stopNativeEvent)); + + return { + destroy() { + events.forEach((eventName) => node.removeEventListener(eventName, stopNativeEvent)); + }, + }; + } + + function focusContainerInputAction(node, options = {}) { + const handlePointerDown = (event) => { + stopNativeEvent(event); + if (event?.target?.closest?.("input, button")) return; + focusInputInContainer(node); + }; + const handleClick = (event) => { + if (event?.target?.closest?.("button")) return; + stopNativeEvent(event); + focusInputInContainer(node); + if (options.selectOther) selectSingleOption("Other"); + }; + + node.addEventListener("pointerdown", handlePointerDown); + node.addEventListener("mousedown", stopNativeEvent); + node.addEventListener("click", handleClick); + + return { + destroy() { + node.removeEventListener("pointerdown", handlePointerDown); + node.removeEventListener("mousedown", stopNativeEvent); + node.removeEventListener("click", handleClick); + }, + }; + } + function prevQuestion() { if (currentQuestionIndex > 0) { currentQuestionIndex--; @@ -310,7 +357,11 @@ {@const q = questions[currentQuestionIndex]} {@const key = q.id || `q_${currentQuestionIndex}`} -
+

{q.question}

@@ -348,11 +399,7 @@ class="bds-option-item custom-item {answers[key] === 'Other' ? 'selected' : ''} {focusedOptionIndex === (q.options?.length || 0) ? 'focused' : ''}" role="button" tabindex="0" - onclick={(e) => { - const input = e.currentTarget.querySelector('input'); - if (input) input.focus(); - selectSingleOption("Other"); - }} + use:focusContainerInputAction={{ selectOther: true }} onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); @@ -375,10 +422,11 @@ placeholder={t('questionPanel.somethingElse')} bind:value={customAnswers[key]} oninput={() => answers[key] = "Other"} - onmousedown={(e) => e.stopPropagation()} - onclick={(e) => e.stopPropagation()} + onpointerdown={stopNativeEvent} + onmousedown={stopNativeEvent} + onclick={stopNativeEvent} onkeydown={(e) => { - e.stopPropagation(); + stopNativeEvent(e); if (e.key === 'Enter') { e.preventDefault(); if (customAnswers[key].trim()) { @@ -387,7 +435,7 @@ } } }} - onkeyup={(e) => e.stopPropagation()} + onkeyup={stopNativeEvent} /> {#if answers[key] === "Other" && customAnswers[key].trim()} @@ -414,10 +462,7 @@ class="bds-option-item custom-item {focusedOptionIndex === (q.options?.length || 0) ? 'focused' : ''}" role="button" tabindex="0" - onclick={(e) => { - const input = e.currentTarget.querySelector('input'); - if (input) input.focus(); - }} + use:focusContainerInputAction onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); @@ -438,39 +483,38 @@ class="bds-custom-text-input" placeholder={t('questionPanel.somethingElse')} bind:value={customAnswers[key]} - onmousedown={(e) => e.stopPropagation()} - onclick={(e) => e.stopPropagation()} - onkeydown={(e) => e.stopPropagation()} - onkeyup={(e) => e.stopPropagation()} + onpointerdown={stopNativeEvent} + onmousedown={stopNativeEvent} + onclick={stopNativeEvent} + onkeydown={stopNativeEvent} + onkeyup={stopNativeEvent} />
{/if}
{:else} -
{ - const input = e.currentTarget.querySelector('input'); - if (input) input.focus(); - }} + use:focusContainerInputAction > e.stopPropagation()} - onclick={(e) => e.stopPropagation()} + placeholder={t('questionPanel.typeAnswer')} + bind:value={customAnswers[key]} + onpointerdown={stopNativeEvent} + onmousedown={stopNativeEvent} + onclick={stopNativeEvent} onkeydown={(e) => { - e.stopPropagation(); + stopNativeEvent(e); if (e.key === 'Enter') { e.preventDefault(); nextOrSubmit(); } }} - onkeyup={(e) => e.stopPropagation()} + onkeyup={stopNativeEvent} autofocus />
@@ -696,6 +740,8 @@ color: var(--bds-text-primary, #ececec); font-size: 14px; flex-grow: 1; + width: 100%; + min-width: 0; outline: none; padding: 0; font-family: inherit; @@ -721,6 +767,7 @@ border: 1px solid var(--bds-border, #3a3b3f); border-radius: var(--bds-radius, 14px); padding: 12px; + cursor: text; } .bds-text-input { @@ -729,6 +776,7 @@ color: var(--bds-text-primary, #ececec); font-size: 14px; width: 100%; + min-width: 0; outline: none; font-family: inherit; } diff --git a/tests/integration/ui/QuestionPanel.test.js b/tests/integration/ui/QuestionPanel.test.js index 7116f50..30fa184 100644 --- a/tests/integration/ui/QuestionPanel.test.js +++ b/tests/integration/ui/QuestionPanel.test.js @@ -54,6 +54,35 @@ describe("QuestionPanel integration", () => { cleanup(); }); + it("keeps option button clicks working", async () => { + const { cleanup } = renderSvelte(QuestionPanel); + await flushUi(); + + window.dispatchEvent( + new CustomEvent("bds-ask-questions", { + detail: { + questions: [ + { + id: "q1", + question: "Pick one", + type: "single", + options: ["Alpha", "Beta"], + }, + ], + }, + }), + ); + await vi.advanceTimersByTimeAsync(150); + await flushUi(); + + document.querySelectorAll(".bds-option-item")[1].dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true })); + await vi.advanceTimersByTimeAsync(1000); + + expect(document.querySelector("#chat-input").value).toContain("Beta"); + expect(document.querySelector('button[title="Send message"]').click).toHaveBeenCalled(); + cleanup(); + }); + it("attaches to a contenteditable composer when no textarea is present", async () => { document.body.innerHTML = `
@@ -122,4 +151,84 @@ describe("QuestionPanel integration", () => { expect(event.defaultPrevented).toBe(false); cleanup(); }); + + it("focuses free text input from wrapper clicks without leaking to composer", async () => { + const { cleanup } = renderSvelte(QuestionPanel); + await flushUi(); + + window.dispatchEvent( + new CustomEvent("bds-ask-questions", { + detail: { + questions: [ + { + id: "q1", + question: "Explain", + type: "input", + }, + ], + }, + }), + ); + await vi.advanceTimersByTimeAsync(150); + await flushUi(); + + const composerPointerDown = vi.fn(); + document.querySelector(".ds-textarea").addEventListener("pointerdown", composerPointerDown); + const wrapper = document.querySelector(".bds-free-input-wrapper"); + const input = document.querySelector(".bds-text-input"); + document.querySelector("#chat-input").focus(); + + wrapper.dispatchEvent(new MouseEvent("pointerdown", { bubbles: true, cancelable: true })); + wrapper.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true })); + await vi.advanceTimersByTimeAsync(0); + await flushUi(); + + expect(document.activeElement).toBe(input); + expect(composerPointerDown).not.toHaveBeenCalled(); + cleanup(); + }); + + it("focuses custom answer input when clicking the custom option row", async () => { + const { cleanup } = renderSvelte(QuestionPanel); + await flushUi(); + + window.dispatchEvent( + new CustomEvent("bds-ask-questions", { + detail: { + questions: [ + { + id: "q1", + question: "Pick one", + type: "single", + options: ["Alpha", "Beta"], + allowCustom: true, + }, + ], + }, + }), + ); + await vi.advanceTimersByTimeAsync(150); + await flushUi(); + + const row = document.querySelector(".custom-item"); + const input = row.querySelector(".bds-custom-text-input"); + document.querySelector("#chat-input").focus(); + + row.dispatchEvent(new MouseEvent("pointerdown", { bubbles: true, cancelable: true })); + row.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true })); + await vi.advanceTimersByTimeAsync(0); + await flushUi(); + + expect(document.activeElement).toBe(input); + + input.value = "Gamma"; + input.dispatchEvent(new Event("input", { bubbles: true })); + await flushUi(); + document.querySelector(".bds-custom-confirm").dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true })); + await vi.advanceTimersByTimeAsync(1000); + + expect(document.querySelector("#chat-input").value).toContain("Gamma"); + expect(document.querySelector('button[title="Send message"]').click).toHaveBeenCalled(); + cleanup(); + }); });