Task #499: fix Quick Actions dialog X overlap + iPad long-press drag feedback

Two scoped UX fixes on Executive Meetings schedule, both confined to
artifacts/tx-os/src/pages/executive-meetings.tsx (shared
src/components/ui/dialog.tsx untouched per task spec).

1) Quick Actions dialog header overlap
   - Radix DialogContent renders the Close (X) at physical
     `right-4 top-4` regardless of `dir`. In AR (RTL) the
     `text-right`-aligned title "إجراءات سريعة" pinned to the same
     edge and visibly overlapped the X.
   - Fix: added `pr-8` to the per-row Quick Actions DialogHeader so
     the title's right edge always clears the close button. Works
     for AR (overlap eliminated) and EN (just adds trailing slack).

2) iPad long-press drag feedback
   - dnd-kit's TouchSensor has a 200ms activation delay; previously
     the row gave no cue during the hold window so users lifted
     early thinking the schedule didn't register.
   - Fix: added `isPressing` state toggled via capture-phase
     onPointerDown/Up/Cancel handlers on the row tr, gated on
     `pointerType === "touch"` && `effectiveDragEnabled`. Capture
     phase chosen so handlers don't collide with safeRowDragListeners
     (which spreads dnd-kit's bubble-phase onPointerDown).
   - Visual cue: subtle `inset 0 0 0 2px hexToRgba(highlightColor,
     0.45)` ring composed AFTER quickOpen/selection/current rings so
     it never outranks a real selection state. Two defensive
     useEffects clear `isPressing`: one when drag becomes disabled
     (capability flip / edit-mode toggle), one tied to the
     `isDragging` lifecycle so dnd-kit-managed release paths (drop
     outside the row, sensor-managed cancel) can't leave a stale
     ring behind.
   - data-pressing="true" attribute exposed on tr for tests.
   - TouchSensor delay (200ms) and tolerance (8px) unchanged.

Tests added:
 - executive-meetings-quick-actions-dialog-layout.spec.mjs: asserts
   close button + title bounding boxes don't overlap in AR + EN.
 - executive-meetings-row-touch-press-feedback.spec.mjs: uses
   hasTouch+isMobile + CDP Input.dispatchTouchEvent, asserts
   data-pressing flips true on touchstart, clears on touchend, and
   tap-to-open quick-actions dialog still works for short taps;
   second test verifies the cue persists past the 200ms window.

Verification: pnpm tsc --noEmit clean. Pre-existing `test` workflow
failures (rotate-content/font-settings/notes-share) are unrelated to
this change.
This commit is contained in:
riyadhafraa
2026-05-11 15:40:48 +00:00
parent 27c5cc381c
commit 9b0dbb037c
2 changed files with 98 additions and 0 deletions
@@ -163,6 +163,90 @@ test("Touch (iPad): pressing a row paints data-pressing=true within the 200ms To
await expect(surface).toBeVisible({ timeout: 5_000 });
});
test("Touch (iPad): drag activation + drop reliably clears data-pressing (no stale ring after drag)", async ({
page,
}) => {
// #499 review: dnd-kit owns the touch sequence once a drag
// activates and on some release paths the row's own pointerup
// does not fire. The MeetingRow has a defensive effect that
// watches `isDragging` and force-clears `isPressing` when drag
// activates / ends — this test guards that path: long-press
// (drag activates), move (drag in progress), release (drag ends),
// assert `data-pressing` is cleared after the drop.
await page.addInitScript(() => {
try {
window.localStorage.setItem("tx-lang", "en");
} catch {
/* ignore */
}
});
const date = uniqueFutureDate(3);
const idA = await insertMeeting({
meetingDate: date,
titleEn: `Touch Drag Cleanup A ${Date.now().toString(36)}`,
startTime: "13:00:00",
endTime: "14:00:00",
});
const idB = await insertMeeting({
meetingDate: date,
titleEn: `Touch Drag Cleanup B ${Date.now().toString(36)}`,
startTime: "15:00:00",
endTime: "16:00:00",
});
await loginViaUi(page, "admin", "admin123");
await page.goto("/executive-meetings");
await page.locator('input[type="date"]').first().fill(date);
const rowA = page.getByTestId(`em-row-${idA}`);
const rowB = page.getByTestId(`em-row-${idB}`);
await expect(rowA).toBeVisible({ timeout: 10_000 });
await expect(rowB).toBeVisible();
const numberCellA = rowA.locator("td").first();
await numberCellA.scrollIntoViewIfNeeded();
const numBox = await numberCellA.boundingBox();
const targetBox = await rowB.boundingBox();
expect(numBox).not.toBeNull();
expect(targetBox).not.toBeNull();
const startX = numBox.x + numBox.width / 2;
const startY = numBox.y + numBox.height / 2;
const endY = targetBox.y + targetBox.height - 4;
const cdp = await page.context().newCDPSession(page);
await cdp.send("Input.dispatchTouchEvent", {
type: "touchStart",
touchPoints: [{ x: startX, y: startY, id: 1 }],
});
// Hold past 200ms TouchSensor activation delay.
await page.waitForTimeout(350);
// Drag down toward rowB.
const steps = 12;
for (let i = 1; i <= steps; i++) {
const y = startY + ((endY - startY) * i) / steps;
await cdp.send("Input.dispatchTouchEvent", {
type: "touchMove",
touchPoints: [{ x: startX, y, id: 1 }],
});
await page.waitForTimeout(20);
}
// Release — drag ends. Whether the row's own pointerup fires or
// dnd-kit cancels it, the `isDragging` cleanup effect must clear
// `data-pressing`.
await cdp.send("Input.dispatchTouchEvent", {
type: "touchEnd",
touchPoints: [],
});
// Both rows should settle without any lingering pressing state.
await expect(rowA).not.toHaveAttribute("data-pressing", "true", {
timeout: 1_000,
});
await expect(rowB).not.toHaveAttribute("data-pressing", "true", {
timeout: 1_000,
});
});
test("Touch (iPad): press cue persists during the long-press hold (>200ms TouchSensor delay)", async ({
page,
}) => {