Samuel Kendrick
Posted on February 1, 2023
Song I listened to while starting: https://www.youtube.com/watch?v=N2yg63_vnqg&ab_channel=TheChemicalBrothers-Topic
Hello Rubber Ducky,
Today I'm making a general purpose draggable menu which will be used in many different applications. A concrete example is to emulate something like this big panel:
Or this little panel:
I was given some specifications in a GitHub issue: https://github.com/VEuPathDB/web-components/issues/420.
I read them and tried to come up with some behavior specifications to better define what this component is and what it does. Here's what I came up with:
- Panels can be opened and closed.
- Panels live somewhere initially.
- Users can change where panels live using their cursor/finger.
- This, of course, implies that users can identify the container's handle.
- We can put anything into a draggable panel.
Some public-facing API specs for the draggable container:
- It should have a
defaultPosition
. - It should emit a
position
via anonStop
. That's how I'm interpreting "an externally state-controlledposition
." - It should surface an
onClose
. - It should also have a
setOpen
. - It "should be standardised with the EDA's
GlobalFiltersDialog
(which has a title and close icon, but no handle)."- Of note: I'm assuming the thing being standardized is the appearance.
I added some testing tools in: https://github.com/VEuPathDB/CoreUI/pull/138. With these I can take a stab at converting a specification in to functional code.
describe("Draggable Panel", () => {
test("panels live somewhere initially.", () => {});
test("dragging a panel changes where it lives.", () => {});
test("when there are two draggables stacked on each other, only the panel the user drags will move.", () => {});
test("users can close draggable panels.", () => {});
test("users can open draggable panels.", () => {});
});
In the end
Here are the tests I came up with:
import { fireEvent, render, screen } from "@testing-library/react";
import { useState } from "react";
import { DraggablePanel, DraggablePanelCoordinatePair } from "./DraggablePanel";
describe("Draggable Panels", () => {
test("dragging a panel changes where it lives.", () => {
const defaultPosition: DraggablePanelCoordinatePair = { x: 0, y: 0 };
const panelTitleForAccessibilityOnly = "Study Filters Panel";
const handleOnDragComplete = jest.fn();
render(
<DraggablePanel
defaultPosition={defaultPosition}
isOpen
onDragComplete={handleOnDragComplete}
onPanelDismiss={() => {}}
panelTitle={panelTitleForAccessibilityOnly}
showPanelTitle
>
<p>Panel contents</p>
</DraggablePanel>
);
const panelDragHandle = screen.getByText(
`Close ${panelTitleForAccessibilityOnly}`
);
const destinationCoordinates = { clientX: 73, clientY: 22 };
drag(panelDragHandle, destinationCoordinates);
/**
* I really don't like assert on implementation details. If we change React dragging librbaries,
* this assertion could break and raise a false positive. That said, jsdom doesn't render layouts
* like a legit browser so we're left with this and data-testids. The data-testid is nice because
* at least we're in control of that so we can make sure that doesn't change if we swap dragging
* providers. See conversations like: https://softwareengineering.stackexchange.com/questions/234024/unit-testing-behaviours-without-coupling-to-implementation-details
*/
const panelFromDataTestId = screen.getByTestId(
`${panelTitleForAccessibilityOnly} dragged`
);
expect(panelFromDataTestId.style.transform).toEqual(
`translate(${destinationCoordinates.clientX}px,${destinationCoordinates.clientY}px)`
);
expect(panelFromDataTestId).toBeTruthy();
expect(handleOnDragComplete).toHaveBeenCalled();
});
test("you can open and close panels", async () => {
const defaultPosition = { x: 50, y: 50 };
function ToggleButtonAndDraggablePanel() {
const [panelIsOpen, setPanelIsOpen] = useState(true);
return (
<>
<button onClick={() => setPanelIsOpen((isOpen) => !isOpen)}>
Toggle Filters Panel
</button>
<DraggablePanel
defaultPosition={defaultPosition}
isOpen={panelIsOpen}
panelTitle="My Filters"
onDragComplete={() => {}}
onPanelDismiss={() => setPanelIsOpen(false)}
showPanelTitle
>
<p>I might be here or I might be gone</p>
</DraggablePanel>
</>
);
}
render(
<>
<ToggleButtonAndDraggablePanel />
<DraggablePanel
defaultPosition={defaultPosition}
isOpen
panelTitle="My Extra Ordinary Data"
onDragComplete={() => {}}
onPanelDismiss={() => {}}
showPanelTitle
>
<p>I will be with you forever.</p>
</DraggablePanel>
</>
);
expect(
screen.getByText("I might be here or I might be gone")
).toBeVisible();
const closePanel = screen.getByText("Close My Filters");
fireEvent.click(closePanel);
expect(
screen.queryByText("I might be here or I might be gone")
).not.toBeVisible();
expect(screen.queryByText("I will be with you forever.")).toBeVisible();
fireEvent.click(screen.getByText("Toggle Filters Panel"));
expect(
screen.getByText("I might be here or I might be gone")
).toBeVisible();
});
});
/**
* So we're pretty limited as regards js-dom and dragging. Here's what I would like to do:
* 1. Simulate dragging events on the draggable element.
* 2. Find the element, getBoundingClientRect for the element
* 3. Assert that the coordinates moved predictably.
*
* Here's the reality: jsdom doesn't do any rendering, so getBoundingClientRect() always
* returns 0,0,0,0. That won't change (even foreseeable long-term).
* You can try to mock the function to emulate the results you'd expect.
* https://github.com/jsdom/jsdom/issues/1590#issuecomment-243228840
*
* @param element
* @param destinationCoordinates
*/
function drag(
element: HTMLElement,
destinationCoordinates: { clientX: number; clientY: number }
): void {
fireEvent.mouseDown(element);
fireEvent.mouseMove(element, destinationCoordinates);
fireEvent.mouseUp(element);
}
Posted on February 1, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.