fix(FN-821): fix Activity Log modal layout and close control
- Fix modal sizing to properly fit within viewport on various screen sizes - Fix close button visibility and accessibility in ActivityLogModal component - Refactor CSS to simplify modal styles (remove unused/verbose rules) - Add regression tests for modal layout and close button behavior - Update Activity Log docs to reflect modal sizing changes
This commit is contained in:
@@ -234,7 +234,7 @@ View a centralized timeline of all task lifecycle events. Click the history icon
|
||||
- **Auto-refresh**: Log updates automatically every 30 seconds when the modal is open
|
||||
- **Pagination**: "Load More" button fetches older entries (100 entries per request, max 1000)
|
||||
- **Clear Log**: Maintenance function to clear all activity history (with confirmation)
|
||||
- **Responsive Layout**: On narrow screens (≤768px), the modal adapts with a stacked header, full-width filter controls, wrapped active-filters bar, reflowed entry text, and vertically stacked confirmation actions — preserving access to filters, task links, and clear-log on mobile devices
|
||||
- **Responsive Layout**: The modal uses the shared `modal-lg` width (640px) and standard `modal-header` pattern with a dedicated close button, consistent with other dashboard modals. On narrow screens (≤768px), the modal adapts with a stacked header, full-width filter controls, wrapped active-filters bar, reflowed entry text, and vertically stacked confirmation actions — preserving access to filters, task links, and clear-log on mobile devices
|
||||
|
||||
**Event Metadata**:
|
||||
- Task moves show from/to column transitions
|
||||
|
||||
@@ -40,6 +40,23 @@ describe("activity-log-mobile-layout.css", () => {
|
||||
|
||||
const mobileCss = extractMobileMediaBlocks(cssContent);
|
||||
|
||||
// ── Modal sizing ────────────────────────────────────────────────────
|
||||
|
||||
it("uses modal-lg base class for consistent wide sizing", () => {
|
||||
// The activity-log-modal should NOT set its own max-width; modal-lg handles width
|
||||
const modalBlock = cssContent.match(/\.activity-log-modal\s*\{[^}]*\}/)?.[0];
|
||||
expect(modalBlock).toBeTruthy();
|
||||
// Should NOT contain max-width (handled by modal-lg base class)
|
||||
expect(modalBlock).not.toMatch(/max-width:\s*\d+px/);
|
||||
});
|
||||
|
||||
// ── Close button ────────────────────────────────────────────────────
|
||||
|
||||
it("does not define a custom activity-log-close style (uses shared modal-close)", () => {
|
||||
// The modal should use the shared .modal-close class instead of a custom close button
|
||||
expect(cssContent).not.toMatch(/\.activity-log-close\s*\{/);
|
||||
});
|
||||
|
||||
// ── Modal header / actions ──────────────────────────────────────────
|
||||
|
||||
it("has mobile rule for activity-log-header to wrap on narrow screens", () => {
|
||||
|
||||
@@ -182,9 +182,9 @@ export function ActivityLogModal({
|
||||
}}
|
||||
data-testid="activity-log-modal-overlay"
|
||||
>
|
||||
<div className="modal activity-log-modal" data-testid="activity-log-modal">
|
||||
{/* Header */}
|
||||
<div className="activity-log-header">
|
||||
<div className="modal modal-lg activity-log-modal" data-testid="activity-log-modal">
|
||||
{/* Header — uses shared modal-header pattern for consistent close control */}
|
||||
<div className="modal-header activity-log-header">
|
||||
<div className="activity-log-title">
|
||||
<History size={18} />
|
||||
<span>Activity Log</span>
|
||||
@@ -250,17 +250,17 @@ export function ActivityLogModal({
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Close button */}
|
||||
<button
|
||||
className="activity-log-close"
|
||||
onClick={onClose}
|
||||
title="Close"
|
||||
data-testid="activity-close"
|
||||
>
|
||||
<X size={18} />
|
||||
</button>
|
||||
</div>
|
||||
{/* Close button — uses shared modal-close for consistent sizing and alignment */}
|
||||
<button
|
||||
className="modal-close"
|
||||
onClick={onClose}
|
||||
aria-label="Close"
|
||||
title="Close"
|
||||
data-testid="activity-close"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Active filters display */}
|
||||
|
||||
@@ -492,6 +492,10 @@ describe("ActivityLogModal", () => {
|
||||
// Verify key structural classes that the mobile CSS targets
|
||||
const modal = container.querySelector(".activity-log-modal");
|
||||
expect(modal).toBeTruthy();
|
||||
// Modal uses shared modal-lg for consistent wide sizing
|
||||
expect(modal!.classList.contains("modal-lg")).toBe(true);
|
||||
// Header uses shared modal-header pattern
|
||||
expect(modal!.querySelector(".modal-header")).toBeTruthy();
|
||||
expect(modal!.querySelector(".activity-log-header")).toBeTruthy();
|
||||
expect(modal!.querySelector(".activity-log-title")).toBeTruthy();
|
||||
expect(modal!.querySelector(".activity-log-actions")).toBeTruthy();
|
||||
@@ -500,6 +504,31 @@ describe("ActivityLogModal", () => {
|
||||
expect(modal!.querySelector(".activity-log-entry")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders close button with shared modal-close class and accessibility attributes", async () => {
|
||||
const { container } = render(
|
||||
<ActivityLogModal
|
||||
isOpen={true}
|
||||
onClose={mockOnClose}
|
||||
tasks={mockTasks}
|
||||
onOpenTaskDetail={mockOnOpenTaskDetail}
|
||||
/>
|
||||
);
|
||||
|
||||
const closeButton = await screen.findByTestId("activity-close");
|
||||
expect(closeButton).toBeTruthy();
|
||||
// Uses shared modal-close class for consistent styling
|
||||
expect(closeButton.classList.contains("modal-close")).toBe(true);
|
||||
// Has accessibility label
|
||||
expect(closeButton.getAttribute("aria-label")).toBe("Close");
|
||||
// Close button is a direct child of the header, NOT inside the actions row
|
||||
const header = container.querySelector(".modal-header");
|
||||
expect(header).toBeTruthy();
|
||||
expect(header!.contains(closeButton)).toBe(true);
|
||||
const actions = container.querySelector(".activity-log-actions");
|
||||
expect(actions).toBeTruthy();
|
||||
expect(actions!.contains(closeButton)).toBe(false);
|
||||
});
|
||||
|
||||
it("renders entry header and details within each entry for mobile reflow", async () => {
|
||||
const { container } = render(
|
||||
<ActivityLogModal
|
||||
|
||||
@@ -11919,20 +11919,14 @@ html .column.drag-over * {
|
||||
/* ── Activity Log Modal ─────────────────────────────────────────── */
|
||||
|
||||
.activity-log-modal {
|
||||
max-width: 600px;
|
||||
max-height: 80vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.activity-log-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--space-lg) 20px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: var(--bg-secondary);
|
||||
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
|
||||
/* Extends shared .modal-header with activity-log-specific overrides */
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.activity-log-title {
|
||||
@@ -11971,8 +11965,7 @@ html .column.drag-over * {
|
||||
}
|
||||
|
||||
.activity-log-refresh,
|
||||
.activity-log-clear,
|
||||
.activity-log-close {
|
||||
.activity-log-clear {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -11992,11 +11985,6 @@ html .column.drag-over * {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.activity-log-close:hover {
|
||||
background: var(--color-red, rgba(239, 68, 68, 0.1));
|
||||
color: var(--color-red, #ef4444);
|
||||
}
|
||||
|
||||
.activity-log-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
@@ -12379,8 +12367,7 @@ html .column.drag-over * {
|
||||
|
||||
/* Action buttons stay inline but shrink */
|
||||
.activity-log-refresh,
|
||||
.activity-log-clear,
|
||||
.activity-log-close {
|
||||
.activity-log-clear {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
flex-shrink: 0;
|
||||
|
||||
Reference in New Issue
Block a user