/* eslint-disable jsx-a11y/no-autofocus */ import * as React from "react"; import {render, act, fireEvent} from "@testing-library/react"; import {CalendarDate} from "@internationalized/date"; import {keyCodes, triggerPress, pointerMap, type} from "@heroui/test-utils"; import userEvent from "@testing-library/user-event"; import {RangeCalendar as RangeCalendarCalendarBase, RangeCalendarProps} from "../src"; let cellFormatter = new Intl.DateTimeFormat("en-US", { weekday: "long", day: "numeric", month: "long", year: "numeric", }); /** * Custom range-calendar to disable animations and avoid issues with react-motion and jest */ const RangeCalendar = React.forwardRef( (props: RangeCalendarProps, ref: React.Ref) => { return ; }, ); RangeCalendar.displayName = "RangeCalendar"; describe("RangeCalendar", () => { let user; beforeAll(() => { user = userEvent.setup({delay: null, pointerMap}); jest.useFakeTimers(); }); afterEach(() => { act(() => { jest.runAllTimers(); }); }); describe("Basics", () => { it("should render correctly", () => { const wrapper = render(); expect(() => wrapper.unmount()).not.toThrow(); }); it("ref should be forwarded", () => { const ref = React.createRef(); render(); expect(ref.current).not.toBeNull(); }); it("should render with defaultValue", () => { let {getAllByLabelText, getByRole, getAllByRole} = render( , ); let heading = getByRole("heading"); expect(heading).toHaveTextContent("June 2019"); let gridCells = getAllByRole("gridcell").filter( (cell) => cell.getAttribute("aria-disabled") !== "true", ); expect(gridCells.length).toBe(30); let selectedDates = getAllByLabelText("Selected", {exact: false}); let labels = [ "Selected Range: Wednesday, June 5 to Monday, June 10, 2019, Wednesday, June 5, 2019 selected", "Thursday, June 6, 2019 selected", "Friday, June 7, 2019 selected", "Saturday, June 8, 2019 selected", "Sunday, June 9, 2019 selected", "Selected Range: Wednesday, June 5 to Monday, June 10, 2019, Monday, June 10, 2019 selected", ]; expect(selectedDates.length).toBe(6); let i = 0; for (let cell of selectedDates) { expect(cell.parentElement).toHaveAttribute("role", "gridcell"); expect(cell.parentElement).toHaveAttribute("aria-selected", "true"); expect(cell).toHaveAttribute("aria-label", labels[i++]); } }); it("should render with value", () => { let {getAllByLabelText, getByRole, getAllByRole} = render( , ); let heading = getByRole("heading"); expect(heading).toHaveTextContent("June 2019"); let gridCells = getAllByRole("gridcell").filter( (cell) => cell.getAttribute("aria-disabled") !== "true", ); expect(gridCells.length).toBe(30); let selectedDates = getAllByLabelText("Selected", {exact: false}); let labels = [ "Selected Range: Wednesday, June 5 to Monday, June 10, 2019, Wednesday, June 5, 2019 selected", "Thursday, June 6, 2019 selected", "Friday, June 7, 2019 selected", "Saturday, June 8, 2019 selected", "Sunday, June 9, 2019 selected", "Selected Range: Wednesday, June 5 to Monday, June 10, 2019, Monday, June 10, 2019 selected", ]; expect(selectedDates.length).toBe(6); let i = 0; for (let cell of selectedDates) { expect(cell.parentElement).toHaveAttribute("role", "gridcell"); expect(cell.parentElement).toHaveAttribute("aria-selected", "true"); expect(cell).toHaveAttribute("aria-label", labels[i++]); } }); it("should focus the first selected date if autoFocus is set", () => { let {getByRole, getAllByLabelText} = render( , ); let cells = getAllByLabelText("selected", {exact: false}); let grid = getByRole("grid"); expect(cells[0].parentElement).toHaveAttribute("role", "gridcell"); expect(cells[0].parentElement).toHaveAttribute("aria-selected", "true"); expect(cells[0]).toHaveFocus(); expect(grid).not.toHaveAttribute("aria-activedescendant"); }); it("should show selected dates across multiple months", async () => { let {getByRole, getByTestId, getAllByLabelText, getAllByRole} = render( , ); let heading = getByRole("heading"); expect(heading).toHaveTextContent("June 2019"); let gridCells = getAllByRole("gridcell").filter( (cell) => cell.getAttribute("aria-disabled") !== "true", ); expect(gridCells.length).toBe(30); let selected = getAllByLabelText("selected", {exact: false}).filter( (cell) => cell.getAttribute("aria-disabled") !== "true", ); expect(selected.length).toBe(11); let juneLabels = [ "Selected Range: Thursday, June 20 to Wednesday, July 10, 2019, Thursday, June 20, 2019 selected", "Friday, June 21, 2019 selected", "Saturday, June 22, 2019 selected", "Sunday, June 23, 2019 selected", "Monday, June 24, 2019 selected", "Tuesday, June 25, 2019 selected", "Wednesday, June 26, 2019 selected", "Thursday, June 27, 2019 selected", "Friday, June 28, 2019 selected", "Saturday, June 29, 2019 selected", "Sunday, June 30, 2019 selected", ]; let i = 0; for (let cell of selected) { expect(cell.parentElement).toHaveAttribute("aria-selected", "true"); expect(cell).toHaveAttribute("aria-label", juneLabels[i++]); } let nextButton = getByTestId("next-button"); await user.click(nextButton); selected = getAllByLabelText("selected", {exact: false}).filter( (cell) => cell.getAttribute("aria-disabled") !== "true", ); expect(selected.length).toBe(10); let julyLabels = [ "Monday, July 1, 2019 selected", "Tuesday, July 2, 2019 selected", "Wednesday, July 3, 2019 selected", "Thursday, July 4, 2019 selected", "Friday, July 5, 2019 selected", "Saturday, July 6, 2019 selected", "Sunday, July 7, 2019 selected", "Monday, July 8, 2019 selected", "Tuesday, July 9, 2019 selected", "Selected Range: Thursday, June 20 to Wednesday, July 10, 2019, Wednesday, July 10, 2019 selected", ]; i = 0; for (let cell of selected) { expect(cell.parentElement).toHaveAttribute("aria-selected", "true"); expect(cell).toHaveAttribute("aria-label", julyLabels[i++]); } expect(heading).toHaveTextContent("July 2019"); gridCells = getAllByRole("gridcell").filter( (cell) => cell.getAttribute("aria-disabled") !== "true", ); expect(gridCells.length).toBe(31); expect(nextButton).toHaveFocus(); let prevButton = getByTestId("prev-button"); await user.click(prevButton); expect(heading).toHaveTextContent("June 2019"); gridCells = getAllByRole("gridcell").filter( (cell) => cell.getAttribute("aria-disabled") !== "true", ); expect(gridCells.length).toBe(30); selected = getAllByLabelText("selected", {exact: false}).filter( (cell) => cell.getAttribute("aria-disabled") !== "true", ); expect(selected.length).toBe(11); i = 0; for (let cell of selected) { expect(cell.parentElement).toHaveAttribute("aria-selected", "true"); expect(cell).toHaveAttribute("aria-label", juneLabels[i++]); } expect(prevButton).toHaveFocus(); }); it("should center the selected range if multiple months are visible", () => { let {getAllByRole, getAllByLabelText} = render( , ); let grids = getAllByRole("grid"); expect(grids).toHaveLength(3); let cells = getAllByLabelText("selected", {exact: false}); expect(cells.every((cell) => grids[1].contains(cell))).toBe(true); }); it("should constrain the visible region depending on the minValue", () => { let {getAllByRole, getAllByLabelText} = render( , ); let grids = getAllByRole("grid"); expect(grids).toHaveLength(3); let cells = getAllByLabelText("selected", {exact: false}); expect(cells.every((cell) => grids[0].contains(cell))).toBe(true); }); it("should start align the selected range if it would go out of view when centered", () => { let {getAllByRole, getAllByLabelText} = render( , ); let grids = getAllByRole("grid"); expect(grids).toHaveLength(3); let cells = getAllByLabelText("selected", {exact: false}); expect(grids[0].contains(cells[0])).toBe(true); }); }); describe("Keyboard interactions", () => { it("should add a range selection prompt to the focused cell", () => { let {getByRole, getByLabelText} = render(); let grid = getByRole("grid"); let cell = getByLabelText("today", {exact: false}); expect(grid).not.toHaveAttribute("aria-activedescendant"); expect(cell).toHaveAttribute("aria-label", `Today, ${cellFormatter.format(new Date())}`); expect(cell).toHaveAttribute("aria-describedby"); const cellDescBy = cell.getAttribute("aria-describedby"); if (cellDescBy) { expect(document.getElementById(cellDescBy)).toHaveTextContent( "Click to start selecting date range", ); } // enter selection mode fireEvent.keyDown(grid, {key: "Enter", keyCode: keyCodes.Enter}); expect(grid).not.toHaveAttribute("aria-activedescendant"); expect(cell.parentElement).toHaveAttribute("aria-selected"); expect(cell).toHaveAttribute( "aria-label", `Today, ${cellFormatter.format(new Date())} selected`, ); expect(cell).toHaveAttribute("aria-describedby"); const cellDescBySelected = cell.getAttribute("aria-describedby"); if (cellDescBySelected) { expect(document.getElementById(cellDescBySelected)).toHaveTextContent( "Click to finish selecting date range", ); } }); it("should select a range with the keyboard (uncontrolled)", () => { let onChange = jest.fn(); let {getAllByLabelText} = render( , ); let selectedDates = getAllByLabelText("Selected", {exact: false}); expect(selectedDates[0].textContent).toBe("5"); expect(selectedDates[selectedDates.length - 1].textContent).toBe("10"); // Select a new date type("ArrowLeft"); // Begin selecting type("Enter"); // Auto advances by one day selectedDates = getAllByLabelText("Selected", {exact: false}); expect(selectedDates[0].textContent).toBe("4"); expect(selectedDates[selectedDates.length - 1].textContent).toBe("5"); expect(onChange).toHaveBeenCalledTimes(0); // Move focus type("ArrowRight"); type("ArrowRight"); type("ArrowRight"); type("ArrowRight"); selectedDates = getAllByLabelText("Selected", {exact: false}); expect(selectedDates[0].textContent).toBe("4"); expect(selectedDates[selectedDates.length - 1].textContent).toBe("9"); expect(onChange).toHaveBeenCalledTimes(0); // End selection type(" "); selectedDates = getAllByLabelText("Selected", {exact: false}); expect(selectedDates[0].textContent).toBe("4"); // uncontrolled expect(selectedDates[selectedDates.length - 1].textContent).toBe("9"); expect(onChange).toHaveBeenCalledTimes(1); let {start, end} = onChange.mock.calls[0][0]; expect(start).toEqual(new CalendarDate(2019, 6, 4)); expect(end).toEqual(new CalendarDate(2019, 6, 9)); }); it("select a range with the keyboard (controlled)", () => { let onChange = jest.fn(); let {getAllByLabelText} = render( , ); let selectedDates = getAllByLabelText("selected", {exact: false}); expect(selectedDates[0].textContent).toBe("5"); expect(selectedDates[selectedDates.length - 1].textContent).toBe("10"); // Select a new date type("ArrowLeft"); // Begin selecting type("Enter"); // Auto advances by one day selectedDates = getAllByLabelText("selected", {exact: false}); expect(selectedDates[0].textContent).toBe("4"); expect(selectedDates[selectedDates.length - 1].textContent).toBe("5"); expect(onChange).toHaveBeenCalledTimes(0); // Move focus type("ArrowRight"); type("ArrowRight"); type("ArrowRight"); type("ArrowRight"); selectedDates = getAllByLabelText("selected", {exact: false}); expect(selectedDates[0].textContent).toBe("4"); expect(selectedDates[selectedDates.length - 1].textContent).toBe("9"); expect(onChange).toHaveBeenCalledTimes(0); // End selection type(" "); selectedDates = getAllByLabelText("selected", {exact: false}); expect(selectedDates[0].textContent).toBe("5"); // controlled expect(selectedDates[selectedDates.length - 1].textContent).toBe("10"); expect(onChange).toHaveBeenCalledTimes(1); let {start, end} = onChange.mock.calls[0][0]; expect(start).toEqual(new CalendarDate(2019, 6, 4)); expect(end).toEqual(new CalendarDate(2019, 6, 9)); }); it("should not enter selection mode with the keyboard if isReadOnly", () => { let {getByRole, getByLabelText} = render(); let grid = getByRole("grid"); let cell = getByLabelText("today", {exact: false}); expect(grid).not.toHaveAttribute("aria-activedescendant"); expect(cell).toHaveAttribute("aria-label", `Today, ${cellFormatter.format(new Date())}`); expect(document.activeElement).toBe(cell); // try to enter selection mode fireEvent.keyDown(grid, {key: "Enter", keyCode: keyCodes.Enter}); expect(grid).not.toHaveAttribute("aria-activedescendant"); expect(cell.parentElement).not.toHaveAttribute("aria-selected"); expect(cell).toHaveAttribute("aria-label", `Today, ${cellFormatter.format(new Date())}`); expect(document.activeElement).toBe(cell); }); it("should select a range with the mouse (uncontrolled)", () => { let onChange = jest.fn(); let {getAllByLabelText, getByText} = render( , ); triggerPress(getByText("17")); let selectedDates = getAllByLabelText("selected", {exact: false}); expect(selectedDates[0].textContent).toBe("17"); expect(selectedDates[selectedDates.length - 1].textContent).toBe("17"); expect(onChange).toHaveBeenCalledTimes(0); // hovering updates the highlighted dates // @ts-ignore fireEvent.pointerEnter(getByText("10").parentElement); selectedDates = getAllByLabelText("selected", {exact: false}); expect(selectedDates[0].textContent).toBe("10"); expect(selectedDates[selectedDates.length - 1].textContent).toBe("17"); expect(onChange).toHaveBeenCalledTimes(0); // @ts-ignore fireEvent.pointerEnter(getByText("7").parentElement); triggerPress(getByText("7")); selectedDates = getAllByLabelText("selected", {exact: false}); expect(selectedDates[0].textContent).toBe("7"); // uncontrolled expect(selectedDates[selectedDates.length - 1].textContent).toBe("17"); expect(onChange).toHaveBeenCalledTimes(1); let {start, end} = onChange.mock.calls[0][0]; expect(start).toEqual(new CalendarDate(2019, 6, 7)); expect(end).toEqual(new CalendarDate(2019, 6, 17)); }); it("should select a range with the mouse (controlled)", () => { let onChange = jest.fn(); let {getAllByLabelText, getByText} = render( , ); triggerPress(getByText("17")); let selectedDates = getAllByLabelText("selected", {exact: false}); expect(selectedDates[0].textContent).toBe("17"); expect(selectedDates[selectedDates.length - 1].textContent).toBe("17"); expect(onChange).toHaveBeenCalledTimes(0); // hovering updates the highlighted dates fireEvent.pointerEnter(getByText("10")); selectedDates = getAllByLabelText("selected", {exact: false}); expect(selectedDates[0].textContent).toBe("10"); expect(selectedDates[selectedDates.length - 1].textContent).toBe("17"); expect(onChange).toHaveBeenCalledTimes(0); fireEvent.pointerEnter(getByText("7")); triggerPress(getByText("7")); selectedDates = getAllByLabelText("selected", {exact: false}); expect(selectedDates[0].textContent).toBe("5"); // controlled expect(selectedDates[selectedDates.length - 1].textContent).toBe("10"); expect(onChange).toHaveBeenCalledTimes(1); let {start, end} = onChange.mock.calls[0][0]; expect(start).toEqual(new CalendarDate(2019, 6, 7)); expect(end).toEqual(new CalendarDate(2019, 6, 17)); }); it("selects by dragging with the mouse", () => { let onChange = jest.fn(); let {getAllByLabelText, getByText} = render( , ); fireEvent.mouseDown(getByText("17"), {detail: 1}); let selectedDates = getAllByLabelText("selected", {exact: false}); expect(selectedDates[0].textContent).toBe("17"); expect(selectedDates[selectedDates.length - 1].textContent).toBe("17"); expect(onChange).toHaveBeenCalledTimes(0); // dragging updates the highlighted dates fireEvent.pointerEnter(getByText("18")); selectedDates = getAllByLabelText("selected", {exact: false}); expect(selectedDates[0].textContent).toBe("17"); expect(selectedDates[selectedDates.length - 1].textContent).toBe("18"); expect(onChange).toHaveBeenCalledTimes(0); fireEvent.pointerEnter(getByText("23")); selectedDates = getAllByLabelText("selected", {exact: false}); expect(selectedDates[0].textContent).toBe("17"); expect(selectedDates[selectedDates.length - 1].textContent).toBe("23"); expect(onChange).toHaveBeenCalledTimes(0); fireEvent.mouseUp(getByText("23"), {detail: 1}); selectedDates = getAllByLabelText("selected", {exact: false}); expect(selectedDates[0].textContent).toBe("17"); expect(selectedDates[selectedDates.length - 1].textContent).toBe("23"); expect(onChange).toHaveBeenCalledTimes(1); let {start, end} = onChange.mock.calls[0][0]; expect(start).toEqual(new CalendarDate(2019, 6, 17)); expect(end).toEqual(new CalendarDate(2019, 6, 23)); }); it("allows dragging the start of the highlighted range to modify it", () => { let onChange = jest.fn(); let {getAllByLabelText, getByText} = render( , ); fireEvent.mouseDown(getByText("10"), {detail: 1}); // mouse down on a range end should not reset it let selectedDates = getAllByLabelText("selected", {exact: false}); expect(selectedDates[0].textContent).toBe("10"); expect(selectedDates[selectedDates.length - 1].textContent).toBe("20"); expect(onChange).toHaveBeenCalledTimes(0); // dragging updates the highlighted dates fireEvent.pointerEnter(getByText("11")); selectedDates = getAllByLabelText("selected", {exact: false}); expect(selectedDates[0].textContent).toBe("11"); expect(selectedDates[selectedDates.length - 1].textContent).toBe("20"); expect(onChange).toHaveBeenCalledTimes(0); fireEvent.pointerEnter(getByText("8")); selectedDates = getAllByLabelText("selected", {exact: false}); expect(selectedDates[0].textContent).toBe("8"); expect(selectedDates[selectedDates.length - 1].textContent).toBe("20"); expect(onChange).toHaveBeenCalledTimes(0); fireEvent.mouseUp(getByText("8"), {detail: 1}); selectedDates = getAllByLabelText("selected", {exact: false}); expect(selectedDates[0].textContent).toBe("8"); expect(selectedDates[selectedDates.length - 1].textContent).toBe("20"); expect(onChange).toHaveBeenCalledTimes(1); let {start, end} = onChange.mock.calls[0][0]; expect(start).toEqual(new CalendarDate(2019, 6, 8)); expect(end).toEqual(new CalendarDate(2019, 6, 20)); }); it("allows dragging the end of the highlighted range to modify it", () => { let onChange = jest.fn(); let {getAllByLabelText, getByText} = render( , ); fireEvent.mouseDown(getByText("20"), {detail: 1}); // mouse down on a range end should not reset it let selectedDates = getAllByLabelText("selected", {exact: false}); expect(selectedDates[0].textContent).toBe("10"); expect(selectedDates[selectedDates.length - 1].textContent).toBe("20"); expect(onChange).toHaveBeenCalledTimes(0); // dragging updates the highlighted dates fireEvent.pointerEnter(getByText("21")); selectedDates = getAllByLabelText("selected", {exact: false}); expect(selectedDates[0].textContent).toBe("10"); expect(selectedDates[selectedDates.length - 1].textContent).toBe("21"); expect(onChange).toHaveBeenCalledTimes(0); fireEvent.pointerEnter(getByText("19")); selectedDates = getAllByLabelText("selected", {exact: false}); expect(selectedDates[0].textContent).toBe("10"); expect(selectedDates[selectedDates.length - 1].textContent).toBe("19"); expect(onChange).toHaveBeenCalledTimes(0); fireEvent.mouseUp(getByText("19"), {detail: 1}); selectedDates = getAllByLabelText("selected", {exact: false}); expect(selectedDates[0].textContent).toBe("10"); expect(selectedDates[selectedDates.length - 1].textContent).toBe("19"); expect(onChange).toHaveBeenCalledTimes(1); let {start, end} = onChange.mock.calls[0][0]; expect(start).toEqual(new CalendarDate(2019, 6, 10)); expect(end).toEqual(new CalendarDate(2019, 6, 19)); }); it("releasing drag outside calendar commits it", () => { let onChange = jest.fn(); let {getAllByLabelText, getByText} = render( , ); fireEvent.mouseDown(getByText("22"), {detail: 1}); let selectedDates = getAllByLabelText("selected", {exact: false}); expect(selectedDates[0].textContent).toBe("22"); expect(selectedDates[selectedDates.length - 1].textContent).toBe("22"); expect(onChange).toHaveBeenCalledTimes(0); // dragging updates the highlighted dates fireEvent.pointerEnter(getByText("25")); selectedDates = getAllByLabelText("selected", {exact: false}); expect(selectedDates[0].textContent).toBe("22"); expect(selectedDates[selectedDates.length - 1].textContent).toBe("25"); expect(onChange).toHaveBeenCalledTimes(0); fireEvent.pointerUp(document.body); selectedDates = getAllByLabelText("selected", {exact: false}); expect(selectedDates[0].textContent).toBe("22"); expect(selectedDates[selectedDates.length - 1].textContent).toBe("25"); expect(onChange).toHaveBeenCalledTimes(1); let {start, end} = onChange.mock.calls[0][0]; expect(start).toEqual(new CalendarDate(2019, 6, 22)); expect(end).toEqual(new CalendarDate(2019, 6, 25)); }); it("releasing drag outside calendar commits it", () => { let onChange = jest.fn(); let {getAllByLabelText, getByText} = render( , ); fireEvent.mouseDown(getByText("22"), {detail: 1}); let selectedDates = getAllByLabelText("selected", {exact: false}); expect(selectedDates[0].textContent).toBe("22"); expect(selectedDates[selectedDates.length - 1].textContent).toBe("22"); expect(onChange).toHaveBeenCalledTimes(0); // dragging updates the highlighted dates fireEvent.pointerEnter(getByText("25")); selectedDates = getAllByLabelText("selected", {exact: false}); expect(selectedDates[0].textContent).toBe("22"); expect(selectedDates[selectedDates.length - 1].textContent).toBe("25"); expect(onChange).toHaveBeenCalledTimes(0); fireEvent.pointerUp(document.body); selectedDates = getAllByLabelText("selected", {exact: false}); expect(selectedDates[0].textContent).toBe("22"); expect(selectedDates[selectedDates.length - 1].textContent).toBe("25"); expect(onChange).toHaveBeenCalledTimes(1); let {start, end} = onChange.mock.calls[0][0]; expect(start).toEqual(new CalendarDate(2019, 6, 22)); expect(end).toEqual(new CalendarDate(2019, 6, 25)); }); }); });