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
62 changes: 62 additions & 0 deletions e2e/extension.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,68 @@ test.describe("Slash Commands", () => {

await context.close();
});

test("hint tooltip appears on focus and hides on blur", async () => {
const context = await chromium.launch({ headless: false });
const page = await context.newPage();

await page.goto(`http://localhost:${testServer.port}/`);
await page.waitForLoadState("domcontentloaded");
await injectContentScript(page);
await page.waitForTimeout(500);

const textarea = page.locator("#test-textarea");
await expect(textarea).toBeVisible();

// Focus the textarea - tooltip should appear
await textarea.click();
await page.waitForTimeout(200);

const tooltip = page.locator("#slashPaletteHint");
await expect(tooltip).toBeVisible({ timeout: 2000 });

// Tooltip should contain the "//" hint text
const tooltipText = await tooltip.textContent();
expect(tooltipText).toContain("//");

// Blur the textarea - tooltip should hide
await page.keyboard.press("Tab");
await page.waitForTimeout(200);
await expect(tooltip).not.toBeVisible();

await context.close();
});

test("hint tooltip hides when picker opens", async () => {
const context = await chromium.launch({ headless: false });
const page = await context.newPage();

await page.goto(`http://localhost:${testServer.port}/`);
await page.waitForLoadState("domcontentloaded");
await injectContentScript(page);
await page.waitForTimeout(500);

const textarea = page.locator("#test-textarea");
await textarea.click();
await page.waitForTimeout(200);

// Tooltip should be visible after focus
const tooltip = page.locator("#slashPaletteHint");
await expect(tooltip).toBeVisible({ timeout: 2000 });

// Type // to open picker
await textarea.fill("//");
await page.waitForTimeout(500);

// Picker should be visible
const picker = page.locator("#slashPalettePicker");
await expect(picker).toBeVisible({ timeout: 3000 });

// Tooltip should be hidden
await expect(tooltip).not.toBeVisible();

await context.close();
});
});

test.describe("Options Page - Giphy Image Settings", () => {
Expand Down
73 changes: 73 additions & 0 deletions src/content/hint-tooltip.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/**
* Hint tooltip shown near qualifying markdown textareas.
* Displays a small indicator hinting to type "//" to open the command list.
*/

import { isDarkMode, fontSystemUi, fontSansSerif } from "../utils/theme.ts"
import { COMMAND_PREFIX } from "../utils/command-prefix.ts"

let tooltipEl: HTMLElement | null = null

function applyTooltipStyles(el: HTMLElement): void {
const dark = isDarkMode()
el.style.position = "fixed"
el.style.zIndex = "999998"
el.style.fontSize = "12px"
el.style.padding = "3px 7px"
el.style.borderRadius = "4px"
el.style.pointerEvents = "none"
el.style.whiteSpace = "nowrap"
el.style.fontFamily = fontSystemUi() + ", " + fontSansSerif()
el.style.color = dark ? "#8d96a0" : "#656d76"
el.style.border = `1px solid ${dark ? "#3d444d" : "#d0d7de"}`
el.style.backgroundColor = dark ? "#161b22" : "#ffffff"
el.style.boxShadow = dark ? "0 2px 8px rgba(1,4,9,0.4)" : "0 2px 8px rgba(140,149,159,0.15)"
}

function ensureTooltip(): HTMLElement {
if (!tooltipEl || !document.body.contains(tooltipEl)) {
const el = document.createElement("div")
el.id = "slashPaletteHint"
el.textContent = `Type ${COMMAND_PREFIX} for commands`
el.style.display = "none"
applyTooltipStyles(el)
document.body.appendChild(el)
tooltipEl = el
}
return tooltipEl
}

function positionTooltip(field: HTMLTextAreaElement): void {
const el = tooltipEl
if (!el) return
const rect = field.getBoundingClientRect()
const elWidth = el.offsetWidth || 160
const left = Math.max(0, rect.right - elWidth)
const top = rect.bottom + 4
el.style.left = `${left}px`
el.style.top = `${top}px`
}

export function showHintTooltip(field: HTMLTextAreaElement): void {
const el = ensureTooltip()
applyTooltipStyles(el)
el.style.display = "block"
positionTooltip(field)
}

export function hideHintTooltip(): void {
if (tooltipEl) {
tooltipEl.style.display = "none"
}
}

export function repositionHintTooltip(field: HTMLTextAreaElement): void {
if (!tooltipEl || tooltipEl.style.display === "none") return
positionTooltip(field)
}

export function refreshHintTooltipTheme(): void {
if (tooltipEl && tooltipEl.style.display !== "none") {
applyTooltipStyles(tooltipEl)
}
}
22 changes: 19 additions & 3 deletions src/content/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ import {
moveSelectionGrid,
applyPickerStyles,
} from "./picker/index.ts"
import {
showHintTooltip,
hideHintTooltip,
repositionHintTooltip,
refreshHintTooltipTheme,
} from "./hint-tooltip.ts"

// Import commands to register them
import "./commands/giphy/index.ts"
Expand Down Expand Up @@ -119,6 +125,7 @@ async function handleCommandInput(
state.activeCommand = cmdName
setHeader("Slash Palette", COMMAND_PREFIX + cmdName + (query ? " " + query : ""))

hideHintTooltip()
showPicker(field)
positionPickerAtCaret(field)

Expand Down Expand Up @@ -303,6 +310,9 @@ function attachToField(field: HTMLTextAreaElement): void {
;(field as unknown as { __slashPaletteBound: boolean }).__slashPaletteBound = true

field.addEventListener("input", () => handleFieldInput(field))
field.addEventListener("focus", () => {
if (!isPickerVisible()) showHintTooltip(field)
})
field.addEventListener("keyup", (ev) => {
// Skip action keys that are handled elsewhere
if (ev.key === "Escape") return
Expand Down Expand Up @@ -336,10 +346,12 @@ function attachToField(field: HTMLTextAreaElement): void {
})
field.addEventListener("scroll", () => {
if (isPickerVisible()) positionPickerAtCaret(field)
repositionHintTooltip(field)
})
field.addEventListener("keydown", (ev) => onFieldKeyDown(ev, field))

field.addEventListener("blur", () => {
hideHintTooltip()
setTimeout(() => {
// Never hide while settings panel is open - user is interacting with controls
if (state.showingSettings) return
Expand Down Expand Up @@ -420,6 +432,7 @@ function boot(): void {
if (isPickerVisible() && state.activeField) {
positionPickerAtCaret(state.activeField)
}
if (state.activeField) repositionHintTooltip(state.activeField)
},
{ passive: true }
)
Expand All @@ -428,13 +441,16 @@ function boot(): void {
if (isPickerVisible() && state.activeField) {
positionPickerAtCaret(state.activeField)
}
if (state.activeField) repositionHintTooltip(state.activeField)
})

// Handle theme changes
onThemeChange(() => {
if (!isPickerVisible()) return
if (state.pickerEl) applyPickerStyles(state.pickerEl)
refreshSelectionStyles()
if (isPickerVisible()) {
if (state.pickerEl) applyPickerStyles(state.pickerEl)
refreshSelectionStyles()
}
refreshHintTooltipTheme()
})
}

Expand Down