feat: Update event modal UI with header icons and improved actions layout

- Add expand and close icons to ResponsiveDialog header in normal mode
- Change 'Show More' button to 'More options' with Add icon
- Hide Cancel button in normal mode, show only in extended mode
- Add showHeaderActions prop to ResponsiveDialog for UI control
- Preserve EventDisplayPreview modal with old UI (no header icons)
- Update all affected test cases to match new UI behavior
This commit is contained in:
lenhanphung
2025-10-13 14:02:31 +07:00
committed by Benoit TELLIER
parent 4936b89e8b
commit 4c18bf3a52
9 changed files with 141 additions and 17 deletions
@@ -95,6 +95,9 @@ describe("EventDuplication", () => {
fireEvent.click(screen.getByText("Duplicate event")); fireEvent.click(screen.getByText("Duplicate event"));
// Cancel button only appears in expanded mode
fireEvent.click(screen.getByText("More options"));
fireEvent.click(screen.getByRole("button", { name: /Cancel/i })); fireEvent.click(screen.getByRole("button", { name: /Cancel/i }));
expect(handleClose).toHaveBeenCalled(); expect(handleClose).toHaveBeenCalled();
+2 -2
View File
@@ -102,8 +102,8 @@ async function setupEventPopover(
const titleInput = screen.getByLabelText("Title"); const titleInput = screen.getByLabelText("Title");
fireEvent.change(titleInput, { target: { value: "Meeting" } }); fireEvent.change(titleInput, { target: { value: "Meeting" } });
// Click Show More to expand the dialog // Click More options to expand the dialog
const showMoreButton = screen.getByText("Show More"); const showMoreButton = screen.getByText("More options");
fireEvent.click(showMoreButton); fireEvent.click(showMoreButton);
// Check Repeat checkbox to show repeat options // Check Repeat checkbox to show repeat options
@@ -295,4 +295,80 @@ describe("ResponsiveDialog", () => {
expect(screen.getByText("Extended Content")).toBeInTheDocument(); expect(screen.getByText("Extended Content")).toBeInTheDocument();
expect(screen.getByLabelText("show less")).toBeInTheDocument(); expect(screen.getByLabelText("show less")).toBeInTheDocument();
}); });
it("renders expand and close icons in normal mode when showHeaderActions is true", () => {
render(
<ResponsiveDialog
open={true}
onClose={mockOnClose}
title="Test"
isExpanded={false}
onExpandToggle={mockOnExpandToggle}
showHeaderActions={true}
>
<div>Content</div>
</ResponsiveDialog>
);
expect(screen.getByLabelText("expand")).toBeInTheDocument();
expect(screen.getByLabelText("close")).toBeInTheDocument();
});
it("does not render header icons when showHeaderActions is false", () => {
render(
<ResponsiveDialog
open={true}
onClose={mockOnClose}
title="Test Title"
isExpanded={false}
onExpandToggle={mockOnExpandToggle}
showHeaderActions={false}
>
<div>Content</div>
</ResponsiveDialog>
);
expect(screen.queryByLabelText("expand")).not.toBeInTheDocument();
expect(screen.queryByLabelText("close")).not.toBeInTheDocument();
expect(screen.getByText("Test Title")).toBeInTheDocument();
});
it("calls onClose when close icon is clicked", () => {
render(
<ResponsiveDialog
open={true}
onClose={mockOnClose}
title="Test"
isExpanded={false}
showHeaderActions={true}
>
<div>Content</div>
</ResponsiveDialog>
);
const closeButton = screen.getByLabelText("close");
fireEvent.click(closeButton);
expect(mockOnClose).toHaveBeenCalledTimes(1);
});
it("calls onExpandToggle when expand icon is clicked", () => {
render(
<ResponsiveDialog
open={true}
onClose={mockOnClose}
title="Test"
isExpanded={false}
onExpandToggle={mockOnExpandToggle}
showHeaderActions={true}
>
<div>Content</div>
</ResponsiveDialog>
);
const expandButton = screen.getByLabelText("expand");
fireEvent.click(expandButton);
expect(mockOnExpandToggle).toHaveBeenCalledTimes(1);
});
}); });
@@ -325,9 +325,12 @@ describe("CalendarPopover - Tabs Scenarios", () => {
const input = screen.getByLabelText(/CalDAV access/i); const input = screen.getByLabelText(/CalDAV access/i);
expect(input).toHaveValue("https://cal.example.org/calendars/user1/cal1"); expect(input).toHaveValue("https://cal.example.org/calendars/user1/cal1");
// Click copy button // Click copy button (find button containing ContentCopyIcon)
const copyButton = screen.getAllByRole("button")[0]; const copyIcon = screen.getByTestId("ContentCopyIcon");
fireEvent.click(copyButton); const copyButton = copyIcon.closest("button");
if (copyButton) {
fireEvent.click(copyButton);
}
expect(navigator.clipboard.writeText).toHaveBeenCalledWith( expect(navigator.clipboard.writeText).toHaveBeenCalledWith(
"https://cal.example.org/calendars/user1/cal1" "https://cal.example.org/calendars/user1/cal1"
+5 -2
View File
@@ -124,10 +124,10 @@ describe("EventPopover", () => {
expect(calendarSelect).toBeInTheDocument(); expect(calendarSelect).toBeInTheDocument();
// Check button // Check button
expect(screen.getByText("Show More")).toBeInTheDocument(); expect(screen.getByText("More options")).toBeInTheDocument();
// Extended mode // Extended mode
fireEvent.click(screen.getByText("Show More")); fireEvent.click(screen.getByText("More options"));
// Back button appears // Back button appears
expect(screen.getByLabelText("show less")).toBeInTheDocument(); expect(screen.getByLabelText("show less")).toBeInTheDocument();
@@ -330,6 +330,9 @@ describe("EventPopover", () => {
it("calls onClose when Cancel clicked", () => { it("calls onClose when Cancel clicked", () => {
renderPopover(); renderPopover();
// Cancel button only appears in expanded mode
fireEvent.click(screen.getByText("More options"));
fireEvent.click(screen.getByText("Cancel")); fireEvent.click(screen.getByText("Cancel"));
expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick"); expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick");
@@ -10,8 +10,11 @@ import {
Stack, Stack,
SxProps, SxProps,
Theme, Theme,
Box,
} from "@mui/material"; } from "@mui/material";
import ArrowBackIcon from "@mui/icons-material/ArrowBack"; import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import CloseIcon from "@mui/icons-material/Close";
import OpenInFullIcon from "@mui/icons-material/OpenInFull";
import React, { ReactNode } from "react"; import React, { ReactNode } from "react";
/** /**
@@ -73,6 +76,8 @@ interface ResponsiveDialogProps
dialogTitleProps?: Omit<DialogTitleProps, "sx">; dialogTitleProps?: Omit<DialogTitleProps, "sx">;
/** Whether to display dividers between title/content/actions */ /** Whether to display dividers between title/content/actions */
dividers?: boolean; dividers?: boolean;
/** Whether to show header action icons (expand/close) in normal mode (default: true) */
showHeaderActions?: boolean;
} }
function ResponsiveDialog({ function ResponsiveDialog({
@@ -93,6 +98,7 @@ function ResponsiveDialog({
dialogContentProps, dialogContentProps,
dialogTitleProps, dialogTitleProps,
dividers = false, dividers = false,
showHeaderActions = true,
sx, sx,
...otherDialogProps ...otherDialogProps
}: ResponsiveDialogProps) { }: ResponsiveDialogProps) {
@@ -150,6 +156,29 @@ function ResponsiveDialog({
> >
<ArrowBackIcon /> <ArrowBackIcon />
</IconButton> </IconButton>
) : showHeaderActions ? (
<Box
display="flex"
justifyContent="space-between"
alignItems="center"
width="100%"
>
<Box>{title}</Box>
<Box>
{onExpandToggle && (
<IconButton
onClick={onExpandToggle}
aria-label="expand"
size="small"
>
<OpenInFullIcon />
</IconButton>
)}
<IconButton onClick={onClose} aria-label="close" size="small">
<CloseIcon />
</IconButton>
</Box>
</Box>
) : ( ) : (
title title
)} )}
@@ -115,6 +115,7 @@ export default function EventPreviewModal({
<ResponsiveDialog <ResponsiveDialog
open={open && !hidePreview} open={open && !hidePreview}
onClose={() => onClose({}, "backdropClick")} onClose={() => onClose({}, "backdropClick")}
showHeaderActions={false}
style={{ overflow: "auto" }} style={{ overflow: "auto" }}
title={ title={
event.title && ( event.title && (
+10 -6
View File
@@ -1,7 +1,5 @@
import { CalendarApi, DateSelectArg } from "@fullcalendar/core"; import { CalendarApi, DateSelectArg } from "@fullcalendar/core";
import { import {
Box,
Button,
Checkbox, Checkbox,
FormControl, FormControl,
FormControlLabel, FormControlLabel,
@@ -23,6 +21,8 @@ import {
ContentCopy as CopyIcon, ContentCopy as CopyIcon,
Close as DeleteIcon, Close as DeleteIcon,
} from "@mui/icons-material"; } from "@mui/icons-material";
import { Box, Button } from "@mui/material";
import AddIcon from "@mui/icons-material/Add";
import React, { import React, {
useEffect, useEffect,
useState, useState,
@@ -409,12 +409,16 @@ function EventPopover({
const dialogActions = ( const dialogActions = (
<Box display="flex" justifyContent="space-between" width="100%" px={2}> <Box display="flex" justifyContent="space-between" width="100%" px={2}>
{!showMore && ( {!showMore && (
<Button onClick={() => setShowMore(!showMore)}>Show More</Button> <Button startIcon={<AddIcon />} onClick={() => setShowMore(!showMore)}>
More options
</Button>
)} )}
<Box display="flex" gap={1} ml={showMore ? "auto" : 0}> <Box display="flex" gap={1} ml={showMore ? "auto" : 0}>
<Button variant="outlined" onClick={handleClose}> {showMore && (
Cancel <Button variant="outlined" onClick={handleClose}>
</Button> Cancel
</Button>
)}
<Button variant="contained" onClick={handleSave} disabled={!title}> <Button variant="contained" onClick={handleSave} disabled={!title}>
Save Save
</Button> </Button>
+9 -4
View File
@@ -1,4 +1,5 @@
import { Box, Button } from "@mui/material"; import { Box, Button } from "@mui/material";
import AddIcon from "@mui/icons-material/Add";
import React, { import React, {
useEffect, useEffect,
useState, useState,
@@ -658,12 +659,16 @@ function EventUpdateModal({
const dialogActions = ( const dialogActions = (
<Box display="flex" justifyContent="space-between" width="100%" px={2}> <Box display="flex" justifyContent="space-between" width="100%" px={2}>
{!showMore && ( {!showMore && (
<Button onClick={() => setShowMore(!showMore)}>Show More</Button> <Button startIcon={<AddIcon />} onClick={() => setShowMore(!showMore)}>
More options
</Button>
)} )}
<Box display="flex" gap={1} ml={showMore ? "auto" : 0}> <Box display="flex" gap={1} ml={showMore ? "auto" : 0}>
<Button variant="outlined" onClick={handleClose}> {showMore && (
Cancel <Button variant="outlined" onClick={handleClose}>
</Button> Cancel
</Button>
)}
<Button variant="contained" onClick={handleSave} disabled={!title}> <Button variant="contained" onClick={handleSave} disabled={!title}>
Save Save
</Button> </Button>