diff --git a/editor.planx.uk/src/@planx/components/List/Public/Context.tsx b/editor.planx.uk/src/@planx/components/List/Public/Context.tsx index 6bdf20e69c..844120862c 100644 --- a/editor.planx.uk/src/@planx/components/List/Public/Context.tsx +++ b/editor.planx.uk/src/@planx/components/List/Public/Context.tsx @@ -1,6 +1,6 @@ -import React, { createContext, ReactNode,useContext, useState } from "react"; +import React, { createContext, ReactNode, useContext, useState } from "react"; -import { generateNewItem,Schema, UserData } from "../model"; +import { generateNewItem, Schema, UserData } from "../model"; interface ListContextValue { schema: Schema; @@ -43,7 +43,16 @@ export const ListProvider: React.FC = ({ const editItem = (index: number) => setActiveIndex(index); const removeItem = (index: number) => { - if (index === activeIndex || index === 0) cancelEditItem(); + // If item is currently in Edit mode, exit Edit mode + if (index === activeIndex || index === 0) { + cancelEditItem(); + } + // If item is before currently active card, retain active card + if (activeIndex && index < activeIndex) { + setActiveIndex((prev) => (prev === undefined ? 0 : prev - 1)); + } + + // Remove item from userData setUserData((prev) => prev.filter((_, i) => i !== index)); }; diff --git a/editor.planx.uk/src/@planx/components/List/Public/index.test.tsx b/editor.planx.uk/src/@planx/components/List/Public/index.test.tsx index d4c1d036b4..16d17c1452 100644 --- a/editor.planx.uk/src/@planx/components/List/Public/index.test.tsx +++ b/editor.planx.uk/src/@planx/components/List/Public/index.test.tsx @@ -1,3 +1,5 @@ +import { within } from "@testing-library/react"; +import { cloneDeep, merge } from "lodash"; import React from "react"; import { axe, setup } from "testUtils"; @@ -99,9 +101,269 @@ describe("Navigating back", () => { }); describe("Building a list", () => { - test.todo("Adding an item"); - test.todo("Editing an item"); - test.todo("Removing an item"); + it("does not display a default item if the schema has no required minimum", () => { + const mockWithMinZero = merge(cloneDeep(mockProps), { schema: { min: 0 } }); + const { queryByRole, getByRole } = setup( + , + ); + + // No card present + const activeListHeading = queryByRole("heading", { + level: 2, + name: "Animal 1", + }); + expect(activeListHeading).toBeNull(); + + // Button is present allow additional items to be added + const addItemButton = getByRole("button", { + name: /Add a new animal type/, + }); + expect(addItemButton).toBeInTheDocument(); + expect(addItemButton).not.toBeDisabled(); + }); + + it("displays a default item if the schema has a required minimum", () => { + const { getByRole, queryByLabelText } = setup( + , + ); + + // Card present... + const activeListHeading = getByRole("heading", { + level: 2, + name: "Animal 1", + }); + expect(activeListHeading).toBeInTheDocument(); + + // ...with active fields + const inputField = queryByLabelText(/What's their name?/); + expect(inputField).toBeInTheDocument(); + expect(inputField).not.toBeDisabled(); + }); + + test("Adding an item", async () => { + const { getAllByRole, getByRole, user } = setup( + , + ); + + let cards = getAllByRole("heading", { level: 2 }).map((el) => + el.closest("div"), + ); + expect(cards).toHaveLength(1); + + const addItemButton = getByRole("button", { + name: /Add a new animal type/, + }); + await user.click(addItemButton); + + // Item successfully added + cards = getAllByRole("heading", { level: 2 }).map((el) => + el.closest("div"), + ); + expect(cards).toHaveLength(2); + + // Old item is inactive + const [firstCard, secondCard] = cards; + expect(firstCard).not.toBeNull(); + expect( + within(firstCard!).queryByLabelText(/What's their name?/), + ).toBeNull(); + + // New item is active + expect(secondCard).not.toBeNull(); + expect( + within(secondCard!).getByLabelText(/What's their name?/), + ).toBeInTheDocument(); + }); + + test("Editing an item", async () => { + // Setup three cards + const { getAllByRole, getByRole, user, findByLabelText } = setup( + , + ); + + const addItemButton = getByRole("button", { + name: /Add a new animal type/, + }); + await user.click(addItemButton); + await user.click(addItemButton); + + let cards = getAllByRole("heading", { level: 2 }).map((el) => + el.closest("div"), + ); + expect(cards).toHaveLength(3); + + let [firstCard, secondCard, thirdCard] = cards; + + // Final card is currently active + expect(thirdCard).not.toBeNull(); + expect( + within(thirdCard!).getByLabelText(/What's their name?/), + ).toBeInTheDocument(); + + // Hitting "cancel" takes us out of Edit mode + const thirdCardCancelButton = within(thirdCard!).getByRole("button", { + name: /Cancel/, + }); + await user.click(thirdCardCancelButton); + + cards = getAllByRole("heading", { level: 2 }).map((el) => + el.closest("div"), + ); + [firstCard, secondCard, thirdCard] = cards; + + // No cards currently active + expect( + within(firstCard!).queryByLabelText(/What's their name?/), + ).toBeNull(); + expect( + within(secondCard!).queryByLabelText(/What's their name?/), + ).toBeNull(); + expect( + within(thirdCard!).queryByLabelText(/What's their name?/), + ).toBeNull(); + + // All card in view only / summary mode + expect(within(firstCard!).getByText(/What's their name?/)).toBeVisible(); + expect(within(secondCard!).getByText(/What's their name?/)).toBeVisible(); + expect(within(thirdCard!).getByText(/What's their name?/)).toBeVisible(); + + // Hit "Edit" on second card + const secondCardEditButton = within(secondCard!).getByRole("button", { + name: /Edit/, + }); + await user.click(secondCardEditButton); + + cards = getAllByRole("heading", { level: 2 }).map((el) => + el.closest("div"), + ); + [firstCard, secondCard, thirdCard] = cards; + + // Second card now editable + expect( + within(secondCard!).getByLabelText(/What's their name?/), + ).toBeInTheDocument(); + }); + + test("Removing an item when all cards are inactive", async () => { + // Setup three cards + const { getAllByRole, getByRole, user, getByLabelText, queryAllByRole } = + setup(); + + const addItemButton = getByRole("button", { + name: /Add a new animal type/, + }); + await user.click(addItemButton); + await user.click(addItemButton); + + let cards = getAllByRole("heading", { level: 2 }).map((el) => + el.closest("div"), + ); + expect(cards).toHaveLength(3); + + let [firstCard, secondCard, thirdCard] = cards; + + const thirdCardCancelButton = within(thirdCard!).getByRole("button", { + name: /Cancel/, + }); + await user.click(thirdCardCancelButton); + + [firstCard, secondCard, thirdCard] = getAllByRole("heading", { + level: 2, + }).map((el) => el.closest("div")); + + // Remove third card + const thirdCardRemoveButton = within(thirdCard!).getByRole("button", { + name: /Remove/, + }); + await user.click(thirdCardRemoveButton); + cards = getAllByRole("heading", { level: 2 }).map((el) => + el.closest("div"), + ); + expect(cards).toHaveLength(2); + + [firstCard, secondCard] = getAllByRole("heading", { level: 2 }).map((el) => + el.closest("div"), + ); + + // Previous items remain inactive + expect( + within(firstCard!).queryByLabelText(/What's their name?/), + ).toBeNull(); + expect( + within(secondCard!).queryByLabelText(/What's their name?/), + ).toBeNull(); + + // Remove second card + const secondCardRemoveButton = within(secondCard!).getByRole("button", { + name: /Remove/, + }); + await user.click(secondCardRemoveButton); + cards = getAllByRole("heading", { level: 2 }).map((el) => + el.closest("div"), + ); + expect(cards).toHaveLength(1); + + [firstCard] = getAllByRole("heading", { level: 2 }).map((el) => + el.closest("div"), + ); + + // Previous items remain inactive + expect( + within(firstCard!).queryByLabelText(/What's their name?/), + ).toBeNull(); + + // Remove first card + const firstCardRemoveButton = within(firstCard!).getByRole("button", { + name: /Remove/, + }); + await user.click(firstCardRemoveButton); + cards = queryAllByRole("heading", { level: 2 }).map((el) => + el.closest("div"), + ); + expect(cards).toHaveLength(0); + + // Add item back + await user.click(addItemButton); + + // This is now editable and active + const newFirstCardInput = getByLabelText(/What's their name?/); + expect(newFirstCardInput).toBeInTheDocument(); + }); + + test("Removing an item when another card is active", async () => { + // Setup two cards + const { getAllByRole, getByRole, user, getByLabelText, queryAllByRole } = + setup(); + + const addItemButton = getByRole("button", { + name: /Add a new animal type/, + }); + await user.click(addItemButton); + + const [firstCard, secondCard] = getAllByRole("heading", { level: 2 }).map( + (el) => el.closest("div"), + ); + + // Second card is active + expect( + within(secondCard!).getByLabelText(/What's their name?/), + ).toBeInTheDocument(); + + // Remove first + const firstCardRemoveButton = within(firstCard!).getByRole("button", { + name: /Remove/, + }); + await user.click(firstCardRemoveButton); + const cards = getAllByRole("heading", { level: 2 }).map((el) => + el.closest("div"), + ); + expect(cards).toHaveLength(1); + + // First card is active + expect( + within(cards[0]!).getByLabelText(/What's their name?/), + ).toBeInTheDocument(); + }); }); describe("Form validation and error handling", () => { diff --git a/editor.planx.uk/src/@planx/components/List/Public/index.tsx b/editor.planx.uk/src/@planx/components/List/Public/index.tsx index 6365444821..c1af007c9d 100644 --- a/editor.planx.uk/src/@planx/components/List/Public/index.tsx +++ b/editor.planx.uk/src/@planx/components/List/Public/index.tsx @@ -4,6 +4,7 @@ import Box from "@mui/material/Box"; import Button from "@mui/material/Button"; import { styled } from "@mui/material/styles"; import Table from "@mui/material/Table"; +import TableBody from "@mui/material/TableBody"; import TableCell from "@mui/material/TableCell"; import TableRow from "@mui/material/TableRow"; import Typography from "@mui/material/Typography"; @@ -100,23 +101,25 @@ const InactiveListCard: React.FC<{ {schema.type} {index + 1} - {schema.fields.map((field, i) => ( - - - {field.data.title} - - {userData[index][i]?.val} - - ))} + + {schema.fields.map((field, i) => ( + + + {field.data.title} + + {userData[index][i]?.val} + + ))} +
removeItem(index)}> - + Remove editItem(index)}> {/* TODO: Is primary colour really right here? */} - + Edit