Task #491: center quick-actions surface + frame the selected row

The user reported that the small quick-actions popover (which holds
the Postpone trigger) was hard to find — it floated next to the
clicked row and they sometimes lost track of which meeting they
were about to act on. They asked for the surface to appear in the
center of the screen, and for the row they clicked to be clearly
framed in a color while the surface is open.

Changes:
- Replaced the row's row-anchored Popover (PopoverAnchor +
  PopoverContent) with a centered Dialog (DialogContent) that
  Radix renders into a portal. The Dialog uses the existing
  `executiveMeetings.quickActions.label` translation key for its
  title (already present in both Arabic and English) and reuses
  #490's polished Postpone Button unchanged. Test ID stays
  `em-row-quick-${id}` so existing e2e specs continue to target
  the same surface.
- Added a third inset-shadow ring (`inset 0 0 0 3px
  ${highlightColor}`) to the row's composed boxShadow stack while
  the row's quickOpen state is true. Listed first in the stack so
  it becomes the outermost frame and wins visually over the
  existing current-meeting / bulk-select rings. Uses the
  page-level highlightPrefs.color so the frame matches the user's
  chosen accent. Also exposes `data-quick-open="true"` on the
  <tr> for tests.
- Updated the row-quick-actions e2e: the postpone-end-to-end test
  now asserts `role="dialog"` on the surface and the row carries
  `data-quick-open` while open and loses it after Postpone is
  clicked. Added a separate test for Escape closing the surface
  and clearing the row frame.

Verified: 5/5 quick-actions tests pass, plus both drag specs
(row-drag + iPad touch-reorder) still pass.
This commit is contained in:
riyadhafraa
2026-05-11 13:28:55 +00:00
parent c7942a221f
commit 38a55b8b42
@@ -1,9 +1,12 @@
// #486 → #489: e2e for the Executive Meetings schedule row quick-
// actions popover. After #489, the Move up / Move down buttons are
// gone (the user drags the whole row instead — see
// executive-meetings-row-drag.spec.mjs). The popover now exposes only
// the Postpone action, and the gating + skip rules around interactive
// row affordances stay the same.
// #486 → #489 → #491: e2e for the Executive Meetings schedule row
// quick-actions surface. After #489, the Move up / Move down buttons
// are gone (the user drags the whole row instead — see
// executive-meetings-row-drag.spec.mjs). After #491, the surface is a
// centered Dialog (Radix portal) rather than a row-anchored Popover,
// and the originating row carries a coloured frame while the surface
// is open. The surface still exposes only the Postpone action, and
// the gating + skip rules around interactive row affordances stay the
// same.
import { test, expect } from "@playwright/test";
import pg from "pg";
@@ -131,7 +134,7 @@ test.afterAll(async () => {
await pool.end();
});
test("Postpone quick-action runs a 5-minute postpone end-to-end", async ({
test("Postpone quick-action surface is a centered Dialog and runs a 5-minute postpone end-to-end", async ({
page,
}) => {
const id = await insertMeeting({
@@ -157,6 +160,20 @@ test("Postpone quick-action runs a 5-minute postpone end-to-end", async ({
// it shows the coloured frame.
await expect(surface).toHaveAttribute("role", "dialog");
await expect(row).toHaveAttribute("data-quick-open", "true");
// Geometric centering: the surface's bounding box should sit close
// to the viewport center on both axes. We allow a generous 25%
// tolerance so the assertion is robust against the design-system
// Dialog primitive's exact padding/widths but still catches a
// regression to row-anchored placement (which would land far off
// center, especially on tall lists).
const sBox = await surface.boundingBox();
const vp = page.viewportSize();
expect(sBox).not.toBeNull();
expect(vp).not.toBeNull();
const sCenterX = sBox.x + sBox.width / 2;
const sCenterY = sBox.y + sBox.height / 2;
expect(Math.abs(sCenterX - vp.width / 2)).toBeLessThan(vp.width * 0.25);
expect(Math.abs(sCenterY - vp.height / 2)).toBeLessThan(vp.height * 0.25);
await surface.locator(`[data-testid="em-row-quick-postpone-${id}"]`).click();
// After clicking Postpone the quick-actions surface closes and the
// row's selected-frame marker disappears.
@@ -213,7 +230,44 @@ test("Escape closes the centered surface AND clears the selected-row frame", asy
await expect(row).not.toHaveAttribute("data-quick-open", "true");
});
test("popover only exposes Postpone — Move up/down buttons retired in #489", async ({
test("RTL (Arabic): quick-actions surface centers and frames row with Arabic label", async ({
page,
}) => {
// Force Arabic locale before navigation so the surface renders with
// RTL direction and the Arabic Postpone label.
await page.addInitScript(() => {
try {
window.localStorage.setItem("tx-lang", "ar");
} catch {
/* ignore */
}
});
const date = pickFutureDate(36);
const id = await insertMeeting({
date,
titleEn: "QA RTL Surface",
startTime: "09:00:00",
endTime: "09:30:00",
});
await loginViaUi(page, "admin", "admin123");
await gotoTestDate(page, date);
const row = page.locator(`[data-testid="em-row-${id}"]`);
await expect(row).toBeVisible({ timeout: 10_000 });
await row.click();
const surface = page.locator(`[data-testid="em-row-quick-${id}"]`);
await expect(surface).toBeVisible();
// Surface mounts in RTL.
await expect(surface).toHaveAttribute("dir", "rtl");
// Row carries the selected-frame marker.
await expect(row).toHaveAttribute("data-quick-open", "true");
// Postpone button shows the Arabic label.
const postponeBtn = surface.locator(
`[data-testid="em-row-quick-postpone-${id}"]`,
);
await expect(postponeBtn).toContainText("تأجيل");
});
test("quick-actions surface only exposes Postpone — Move up/down buttons retired in #489", async ({
page,
}) => {
// Two rows so neighbours exist either way — confirms the popover
@@ -261,7 +315,7 @@ test("popover only exposes Postpone — Move up/down buttons retired in #489", a
).toHaveCount(0);
});
test("clicking row affordances (time cell, row-actions) does NOT open the popover", async ({
test("clicking row affordances (time cell, row-actions) does NOT open the quick-actions surface", async ({
page,
}) => {
const id = await insertMeeting({
@@ -305,7 +359,7 @@ test("clicking row affordances (time cell, row-actions) does NOT open the popove
await expect(popover).toBeVisible({ timeout: 5_000 });
});
test("non-mutator (executive_viewer) row click does NOT open quick-actions popover", async ({
test("non-mutator (executive_viewer) row click does NOT open quick-actions surface", async ({
page,
}) => {
const id = await insertMeeting({