Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom checkbox #292

Merged
merged 20 commits into from
Sep 13, 2024
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions frontend/.eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,16 @@ module.exports = {
},
overrides: [
{
files: "*.e2e.ts",
files: ["*.e2e.ts", "*PgObj.ts"],
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended-type-checked",
"plugin:import/recommended",
"plugin:import/typescript",
"plugin:playwright/recommended",
"prettier",
],
plugins: ["@typescript-eslint"],
plugins: ["@typescript-eslint", "prettier"],
rules: {
"@typescript-eslint/no-floating-promises": "error",
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ export function CandidatesVotesForm({ group }: CandidatesVotesFormProps) {
</BottomBar.Row>
)}
<BottomBar.Row hidden={errors.length > 0 || warnings.length === 0 || hasChanges}>
<Checkbox id={_IGNORE_WARNINGS_ID} defaultChecked={ignoreWarnings}>
<Checkbox id={_IGNORE_WARNINGS_ID} defaultChecked={ignoreWarnings} hasError={warningsWarning}>
Ik heb de aantallen gecontroleerd met het papier en correct overgenomen.
</Checkbox>
</BottomBar.Row>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
}

.icon {
padding-top: 6px;
padding-top: 0.375rem;
padding-right: 1rem;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { userEvent } from "@testing-library/user-event";
import { describe, expect, test, vi } from "vitest";

import { getUrlMethodAndBody, overrideOnce, render, screen, userTypeInputs } from "app/test/unit";
import { getUrlMethodAndBody, overrideOnce, render, screen, userTypeInputs, waitFor } from "app/test/unit";
import { emptyDataEntryRequest } from "app/test/unit/form.ts";

import { FormState, PollingStationFormController, PollingStationValues } from "@kiesraad/api";
Expand Down Expand Up @@ -368,6 +368,17 @@ describe("Test VotersAndVotesForm", () => {
expect(alertText).toHaveTextContent(
/^Je kan alleen verder als je het papieren proces-verbaal hebt gecontroleerd.$/,
);

await user.clear(screen.getByTestId("blank_votes_count"));
await user.type(screen.getByTestId("blank_votes_count"), "100");
await user.tab();
expect(screen.getByTestId("blank_votes_count"), "100").toHaveValue("100");

await waitFor(() => {
const checkbox = screen.getByTestId("voters_and_votes_form_ignore_warnings");
expect(checkbox).toBeInTheDocument();
expect(checkbox).not.toBeVisible();
});
lkleuver marked this conversation as resolved.
Show resolved Hide resolved
});

test("W.201 high number of blank votes", async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@ export function VotersAndVotesForm() {
</BottomBar.Row>
)}
<BottomBar.Row hidden={errors.length > 0 || warnings.length === 0 || hasChanges}>
<Checkbox id={_IGNORE_WARNINGS_ID} defaultChecked={ignoreWarnings}>
<Checkbox id={_IGNORE_WARNINGS_ID} defaultChecked={ignoreWarnings} hasError={warningsWarning}>
Ik heb de aantallen gecontroleerd met het papier en correct overgenomen.
</Checkbox>
</BottomBar.Row>
Expand Down
9 changes: 5 additions & 4 deletions frontend/e2e-tests/dom-to-db-tests/data-entry.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ test.describe("data entry", () => {
await expect(votersVotesPage.warning).toContainText(
"Er is een onverwacht verschil tussen het aantal toegelaten kiezers (A t/m D) en het aantal uitgebrachte stemmen (E t/m H).",
);
await votersVotesPage.acceptWarnings.check();
await votersVotesPage.checkAcceptWarnings();
await votersVotesPage.next.click();

const differencesPage = new DifferencesPage(page);
Expand Down Expand Up @@ -231,7 +231,7 @@ test.describe("data entry", () => {
await expect(votersVotesPage.warning).toContainText(
"Er is een onverwacht verschil tussen het aantal uitgebrachte stemmen (E t/m H) en het herteld aantal toegelaten kiezers (A.2 t/m D.2).",
);
await votersVotesPage.acceptWarnings.check();
await votersVotesPage.checkAcceptWarnings();
await votersVotesPage.next.click();

const differencesPage = new DifferencesPage(page);
Expand Down Expand Up @@ -383,7 +383,7 @@ test.describe("errors and warnings", () => {
await expect(votersVotesPage.error).toBeHidden();

// accept the warning
await votersVotesPage.acceptWarnings.check();
await votersVotesPage.checkAcceptWarnings();
await votersVotesPage.next.click();

const differencesPage = new DifferencesPage(page);
Expand Down Expand Up @@ -622,7 +622,8 @@ test.describe("navigation", () => {
total_votes_cast_count: 100,
};
await votersVotesPage.fillInPageAndClickNext(voters, votes);
await votersVotesPage.acceptWarnings.click();

await votersVotesPage.checkAcceptWarnings();
await votersVotesPage.next.click();

const differencesPage = new DifferencesPage(page);
Expand Down
5 changes: 5 additions & 0 deletions frontend/e2e-tests/page-objects/input/VotersVotesPgObj.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,4 +106,9 @@ export class VotersVotesPage extends InputBasePage {
}
await this.next.click();
}

async checkAcceptWarnings() {
// eslint-disable-next-line playwright/no-force-option -- force option needed to click on hidden element
await this.acceptWarnings.check({ force: true, noWaitAfter: true });
}
}
6 changes: 6 additions & 0 deletions frontend/lib/icon/generated.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,12 @@ export const IconCheckmark = (props: React.SVGAttributes<SVGElement>) => (
</svg>
);

export const IconCheckmarkSmall = (props: React.SVGAttributes<SVGElement>) => (
<svg {...props} xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 14 14">
<path d="M5.2,10.9h0c-.3,0-.5-.1-.7-.3l-2.9-2.9c-.4-.4-.4-1,0-1.4.4-.4,1-.4,1.4,0l2.2,2.2,5.7-5.7c.4-.4,1-.4,1.4,0s.4,1,0,1.4l-6.4,6.4c-.2.2-.4.3-.7.3Z" />
</svg>
);

export const IconChevronRight = (props: React.SVGAttributes<SVGElement>) => (
<svg {...props} viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
Expand Down
3 changes: 3 additions & 0 deletions frontend/lib/icon/svg/checkmarkSmall.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions frontend/lib/ui/BottomBar/BottomBar.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,11 @@
display: flex;
align-items: center;
gap: 2rem;

&:global(.hidden) {
display: none;
visibility: hidden;
opacity: 0;
}
}
}
6 changes: 5 additions & 1 deletion frontend/lib/ui/BottomBar/BottomBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,9 @@ export function BottomBar({ type, children }: BottomBarProps) {
}

BottomBar.Row = function BottomBarRow({ children, hidden }: { children: React.ReactNode; hidden?: boolean }) {
return <section className={cn("row", { hidden: hidden })}>{children}</section>;
return (
<section hidden={hidden} className={cn("row", { hidden: !!hidden })}>
{children}
</section>
);
};
47 changes: 45 additions & 2 deletions frontend/lib/ui/Checkbox/Checkbox.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,60 @@
display: inline-flex;
align-items: center;

/* https://css-tricks.com/inclusively-hidden/ */
input {
clip: rect(0 0 0 0);
clip-path: inset(50%);
height: 1px;
overflow: hidden;
position: absolute;
white-space: nowrap;
width: 1px;
}

> div {
overflow: hidden;
margin-right: 0.5rem;
flex: 0 0 1.25rem;
width: 1.25rem;
height: 1.25rem;
border-radius: 0.375rem;
padding: 0;
border: 1px solid var(--border-color-default);
background-color: #fff;
display: flex;
align-items: center;
justify-content: center;

&:focus {
outline: none;
box-shadow: 0 0 1px 1px rgba(66, 210, 243, 0.4);
border: 1px solid var(--blue-dark-400);
}

&:global(.checked) {
background-color: var(--bg-highlight);
border-color: var(--base-dark-blue);
svg {
opacity: 1;
}
}

svg {
width: 0.875rem;
height: 0.875rem;
fill: var(--base-dark-blue);
opacity: 0;
}
}

&:global(.hidden) {
display: none;
}

&:global(.has-error) {
input {
outline: 1px solid var(--color-error);
> div {
border-color: var(--color-error-darker);
}
}
}
26 changes: 17 additions & 9 deletions frontend/lib/ui/Checkbox/Checkbox.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,23 @@ type Props = {
};

export const DefaultCheckbox: Story<Props> = ({ label }) => (
<Checkbox id="default-checkbox" defaultChecked={false}>
{label}
</Checkbox>
);

export const ErrorCheckbox: Story<Props> = ({ label }) => (
<Checkbox id="default-checkbox" hasError defaultChecked={false}>
{label}
</Checkbox>
<div>
<Checkbox id="default-checkbox" defaultChecked={false}>
{label}
</Checkbox>
<br />
<br />
<Checkbox id="default-checkbox-error" defaultChecked={false} hasError>
{label}
</Checkbox>
<br />
<br />
<div style={{ width: 200 }}>
<Checkbox id="default-checkbox-cramped" defaultChecked={false}>
{label}
</Checkbox>
</div>
</div>
);

export default {
Expand Down
22 changes: 22 additions & 0 deletions frontend/lib/ui/Checkbox/Checkbox.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { render, screen } from "@testing-library/react";
import { userEvent } from "@testing-library/user-event";
import { describe, expect, test } from "vitest";

import { Checkbox } from "./Checkbox";
Expand All @@ -14,6 +15,7 @@ describe("UI component: Checkbox", () => {
expect(screen.getByText("Test label")).toBeInTheDocument();

expect(screen.getByTestId("test-id")).not.toBeChecked();
expect(screen.queryByLabelText("Aangevinkt")).not.toBeInTheDocument();
});

test("The checkbox is checked", () => {
Expand All @@ -24,5 +26,25 @@ describe("UI component: Checkbox", () => {
);

expect(screen.getByTestId("test-id")).toBeChecked();
expect(screen.getByLabelText("Aangevinkt")).toBeInTheDocument();
});
test("The checkbox toggles", async () => {
render(
<Checkbox id="test-id" defaultChecked={false}>
Test label
</Checkbox>,
);

expect(screen.getByTestId("test-id")).not.toBeChecked();

const user = userEvent.setup();

await user.click(screen.getByTestId("checkbox-button-test-id"));

expect(screen.getByTestId("test-id")).toBeChecked();

await user.click(screen.getByTestId("checkbox-button-test-id"));

expect(screen.getByTestId("test-id")).not.toBeChecked();
});
});
29 changes: 26 additions & 3 deletions frontend/lib/ui/Checkbox/Checkbox.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import * as React from "react";

import { IconCheckmarkSmall } from "@kiesraad/icon";
import { cn } from "@kiesraad/util";

import cls from "./Checkbox.module.css";
Expand All @@ -9,12 +12,32 @@ interface CheckboxProps {
defaultChecked?: boolean;
}

//TODO: you can't style a border for a checkbox, currently outline is used as a workaround but the border radius doesnt match

export function Checkbox({ id, children, defaultChecked, hasError }: CheckboxProps) {
const [checked, setChecked] = React.useState(defaultChecked);

React.useEffect(() => {
setChecked(defaultChecked);
}, [defaultChecked]);

const toggleCheckbox = () => {
setChecked((prev) => !prev);
};

const onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setChecked(event.target.checked);
};

return (
<div className={cn(cls.checkbox, { "has-error": !!hasError })} aria-label="input" id={`checkbox-container-${id}`}>
<input type="checkbox" id={id} name={id} defaultChecked={defaultChecked} />
<div
className={checked ? "checked" : "unchecked"}
aria-hidden={true}
onClick={toggleCheckbox}
id={`checkbox-button-${id}`}
>
<IconCheckmarkSmall aria-label={checked ? "Aangevinkt" : "uitgevinkt"} />
</div>
<input type="checkbox" id={id} name={id} checked={checked} onChange={onChange} />
<label htmlFor={id}>{children}</label>
</div>
);
Expand Down