fix(executive-meetings): quick-actions UX + reschedule/cancel polish (Task #563)
User-reported polish on the executive-meetings quick-actions flow on iPad/mobile. Six fixes, all client-side: 1. Quick-actions surface now opens from ANY click on a meeting row (was: only the attendees cell). Root cause: handleRowClick guard skipped any `em-edit-*`, `em-merge-edit-*`, and `em-time-*` descendants. Those affordances only do anything in edit mode, but handleRowClick already early-returns when edit mode is on, so the testid guard was always over-aggressive. Dropped those guards plus the `[role='button']` rule from the interactive selector, kept the real `button`/`input`/`textarea`/`a`/`menuitem`/`checkbox`/etc. guards and the `em-row-actions-*` / `em-row-select-*` testid guards (those affordances are visible outside edit mode and must not trigger the surface). File: artifacts/tx-os/src/pages/executive-meetings.tsx (~L4350). 2. Originating row is now unmistakable on iPad: quickOpenShadow is `inset 0 0 0 4px highlight, 0 0 0 2px highlight` (was a single 3px inset) and the row background tints to `rgba(highlight, 0.08)` while quick-actions is open. File: artifacts/tx-os/src/pages/executive-meetings.tsx (~L4287, L4344). 3. Postpone dialog header was overlapping the Radix close X in RTL. Added `pr-8` on the DialogHeader (same pattern already used for the row-quick mini-dialog). File: artifacts/tx-os/src/components/executive-meetings/ upcoming-meeting-alert.tsx (~L1447). 4. Reschedule tab: date / start / end inputs collided on iPad portrait. Grid is now `grid-cols-1 md:grid-cols-3` (single column through iPad portrait, three across on landscape/desktop), with `min-w-0` on each cell and `w-full` on every Input so Safari's native date/time pickers stop overflowing. Same file (~L1761). 5. Cancel tab X overlap is resolved by the same DialogHeader fix in step 3 (one Dialog wraps all three tabs). 6. Removed the `cancelHint` paragraph + the `cancelHint` translation keys in ar.json (L1252) and en.json (L1164) — fully unused now. NOT pushed to Gitea (Tailscale to desktop-11cj93j still flaky). Pending pushes: #560 (VAPID), #561 (CEO roles), #563 (this).
This commit is contained in:
@@ -1436,7 +1436,12 @@ function PostponeDialog({
|
||||
data-testid="postpone-dialog"
|
||||
className="sm:max-w-2xl"
|
||||
>
|
||||
<DialogHeader className="space-y-1">
|
||||
{/* #563: reserve pr-8 (~2rem) so the Radix close button
|
||||
(positioned right-4 top-4 inside DialogContent) never
|
||||
overlaps the title/description. Physical right padding is
|
||||
correct for BOTH AR (RTL) and LTR because Radix uses CSS
|
||||
physical positioning for the close. */}
|
||||
<DialogHeader className="space-y-1 pr-8">
|
||||
<DialogTitle className="text-base">
|
||||
{t("executiveMeetings.alert.postponeTitle")}
|
||||
</DialogTitle>
|
||||
@@ -1741,11 +1746,17 @@ function PostponeDialog({
|
||||
onBack={() => setCascadePrompt(null)}
|
||||
/>
|
||||
) : null}
|
||||
{/* Responsive grid: 1 col on narrow / 2 cols on sm /
|
||||
3 cols on md+. Prevents horizontal overflow inside the
|
||||
dialog on phones and split panes. */}
|
||||
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<div className="space-y-1">
|
||||
{/* #563: responsive grid — single column on phones AND
|
||||
iPad portrait (`md` and below), then 3 columns in a
|
||||
single row on iPad landscape / desktop. The previous
|
||||
`sm:grid-cols-2` packed date + time-start onto one row
|
||||
on iPad portrait where Safari's native date/time
|
||||
pickers carry wide internal icons + Arabic labels and
|
||||
visibly collided. `min-w-0` lets each column shrink
|
||||
inside the grid track so inputs respect their cell,
|
||||
and `w-full` on the Input ensures it fills the cell. */}
|
||||
<div className="grid grid-cols-1 gap-3 md:grid-cols-3">
|
||||
<div className="space-y-1 min-w-0">
|
||||
<Label className="text-xs">
|
||||
{t("executiveMeetings.alert.rescheduleDate")}
|
||||
</Label>
|
||||
@@ -1755,9 +1766,10 @@ function PostponeDialog({
|
||||
onChange={(e) => setResDate(e.target.value)}
|
||||
disabled={busy !== null}
|
||||
data-testid="reschedule-date"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="space-y-1 min-w-0">
|
||||
<Label className="text-xs">
|
||||
{t("executiveMeetings.alert.rescheduleStart")}
|
||||
</Label>
|
||||
@@ -1767,9 +1779,10 @@ function PostponeDialog({
|
||||
onChange={(e) => setResStart(e.target.value)}
|
||||
disabled={busy !== null}
|
||||
data-testid="reschedule-start"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="space-y-1 min-w-0">
|
||||
<Label className="text-xs">
|
||||
{t("executiveMeetings.alert.rescheduleEnd")}
|
||||
</Label>
|
||||
@@ -1779,6 +1792,7 @@ function PostponeDialog({
|
||||
onChange={(e) => setResEnd(e.target.value)}
|
||||
disabled={busy !== null}
|
||||
data-testid="reschedule-end"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1811,9 +1825,9 @@ function PostponeDialog({
|
||||
className="space-y-2 focus:outline-none"
|
||||
data-testid="postpone-cancel-panel"
|
||||
>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("executiveMeetings.alert.cancelHint")}
|
||||
</p>
|
||||
{/* #563: cancelHint paragraph removed per user request —
|
||||
the destructive button + confirm prompt convey intent
|
||||
without the long disclaimer. */}
|
||||
{!confirmCancel ? (
|
||||
<Button
|
||||
type="button"
|
||||
|
||||
@@ -1249,7 +1249,6 @@
|
||||
"rescheduleEnd": "النهاية",
|
||||
"rescheduleNote": "ملاحظة (اختياري)",
|
||||
"rescheduleApply": "إعادة الجدولة",
|
||||
"cancelHint": "يحدد الاجتماع كمُلغى مع الاحتفاظ بسجل الحضور.",
|
||||
"cancelApply": "إلغاء الاجتماع",
|
||||
"cancelConfirmPrompt": "هل أنت متأكد؟ سيتم وضع الاجتماع كمُلغى.",
|
||||
"cancelConfirmYes": "نعم، إلغاء",
|
||||
|
||||
@@ -1161,7 +1161,6 @@
|
||||
"rescheduleEnd": "End",
|
||||
"rescheduleNote": "Note (optional)",
|
||||
"rescheduleApply": "Reschedule",
|
||||
"cancelHint": "Marks the meeting as cancelled. Attendees keep history.",
|
||||
"cancelApply": "Cancel meeting",
|
||||
"cancelConfirmPrompt": "Are you sure? The meeting will be marked as cancelled.",
|
||||
"cancelConfirmYes": "Yes, cancel",
|
||||
|
||||
@@ -4275,8 +4275,17 @@ function MeetingRow({
|
||||
// #496: belt-and-suspenders — also gate on capability so a stale
|
||||
// `quickOpen=true` from a render before the effect runs cannot
|
||||
// paint the accented frame in edit mode.
|
||||
// #563: thicker inset ring + outer glow + tinted background so the
|
||||
// originating row is unmistakable on iPad where the default 3px
|
||||
// inset is easy to miss against the table grid. The outer
|
||||
// `0 0 0 2px` ring sits OUTSIDE the row box (no inset), so the row
|
||||
// appears framed on all four sides even when adjacent rows would
|
||||
// otherwise visually clip the inset ring.
|
||||
const quickOpenShadow = quickActionsCanMutate && quickOpen
|
||||
? `inset 0 0 0 3px ${highlightColor}`
|
||||
? `inset 0 0 0 4px ${highlightColor}, 0 0 0 2px ${highlightColor}`
|
||||
: null;
|
||||
const quickOpenBg = quickActionsCanMutate && quickOpen
|
||||
? hexToRgba(highlightColor, 0.08)
|
||||
: null;
|
||||
// #499: iPad long-press feedback. dnd-kit's TouchSensor has a 200ms
|
||||
// activation delay before a long-press becomes a drag — during that
|
||||
@@ -4329,7 +4338,7 @@ function MeetingRow({
|
||||
.filter(Boolean)
|
||||
.join(", ");
|
||||
const trStyle: CSSProperties = {
|
||||
backgroundColor: baseBg,
|
||||
backgroundColor: quickOpenBg ?? baseBg,
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
opacity: isDragging ? 0.5 : reordering ? 0.7 : 1,
|
||||
@@ -4340,38 +4349,37 @@ function MeetingRow({
|
||||
if (!quickActionsCanMutate) return;
|
||||
const target = e.target as HTMLElement | null;
|
||||
if (!target) return;
|
||||
// Skip clicks that landed on an interactive child — leave them to
|
||||
// their own handlers. Includes ARIA roles because TimeRangeCell
|
||||
// and a few other affordances render as `div role="button"`
|
||||
// (keyboard-activatable) rather than native `<button>`s, so a
|
||||
// pure tag-name filter would let those clicks fall through to
|
||||
// the row handler and pop the quick-actions menu underneath the
|
||||
// inline editor the user just opened.
|
||||
// #489: scope to descendants — the row itself now carries
|
||||
// role="button" + tabIndex from dnd-kit's `attributes` (because
|
||||
// dragEnabled is no longer gated by edit mode), so an unscoped
|
||||
// closest() would match the row itself and swallow every click.
|
||||
// #563: outside edit mode (which is the only state where
|
||||
// handleRowClick runs — `quickActionsCanMutate` is force-false
|
||||
// in edit mode, see line ~2948), the user expects ANY click
|
||||
// anywhere in the row to open the quick-actions surface, not
|
||||
// just the attendees cell. We only skip clicks that landed on
|
||||
// a *truly* interactive native control (buttons, inputs,
|
||||
// links, menu/checkbox/switch/combobox/dialog roles) — the
|
||||
// `[role='button']` rule is intentionally DROPPED because
|
||||
// TimeRangeCell and EditableInlineText render with role='button'
|
||||
// to mark themselves as keyboard-activatable, but their actual
|
||||
// edit affordance is gated on `editMode === true` — outside
|
||||
// edit mode they are display-only and clicks should bubble.
|
||||
// #489 still holds: scope to descendants so the row's own
|
||||
// dnd-kit `role="button"` doesn't swallow every click.
|
||||
const interactive = target.closest(
|
||||
"button, a, input, textarea, select, [contenteditable='true'], [contenteditable=''], [role='menuitem'], [role='button'], [role='checkbox'], [role='switch'], [role='combobox'], [role='dialog']",
|
||||
"button, a, input, textarea, select, [contenteditable='true'], [contenteditable=''], [role='menuitem'], [role='checkbox'], [role='switch'], [role='combobox'], [role='dialog']",
|
||||
);
|
||||
if (interactive && interactive !== e.currentTarget) {
|
||||
return;
|
||||
}
|
||||
// Skip clicks on row affordances by data-testid prefix so we
|
||||
// don't fight with drag, row-actions, merge-edit, inline-edit,
|
||||
// bulk-select, or the time cell's edit-on-click surface
|
||||
// (em-time-*). The role='button' rule above catches the time
|
||||
// cell's display state too, but the explicit testid guard keeps
|
||||
// the intent obvious and survives any future refactor that
|
||||
// drops the role attribute.
|
||||
// Keep only the affordances that are visible OUTSIDE edit mode
|
||||
// and must not trigger quick-actions: the 3-dot row menu and
|
||||
// the bulk-select checkbox. The previous `em-edit-*`,
|
||||
// `em-merge-edit-*`, and `em-time-*` guards were dropped because
|
||||
// those affordances only do anything in edit mode (where this
|
||||
// handler never fires).
|
||||
if (
|
||||
target.closest(
|
||||
[
|
||||
`[data-testid="em-row-actions-${meeting.id}"]`,
|
||||
`[data-testid="em-row-select-${meeting.id}"]`,
|
||||
`[data-testid^="em-edit-"]`,
|
||||
`[data-testid^="em-merge-edit-"]`,
|
||||
`[data-testid^="em-time-"]`,
|
||||
].join(", "),
|
||||
)
|
||||
) {
|
||||
|
||||
Reference in New Issue
Block a user