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

fix(a11y): always enable continue button on FileUpload and DrawBoundary upload page #3137

Merged
merged 1 commit into from
May 10, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,13 @@ test("shows the file upload option by default and requires user data to continue
// Navigate to upload a file screen
await user.click(screen.getByTestId("upload-file-button"));
expect(screen.getByText("Upload a file")).toBeInTheDocument();
expect(screen.getByTestId("continue-button")).toBeDisabled();

// Continue is enabled by default, but requires data to proceed
expect(screen.getByTestId("continue-button")).toBeEnabled();
await user.click(screen.getByTestId("continue-button"));
expect(
screen.getByTestId("error-message-upload-location-plan"),
).toBeInTheDocument();
});

test("hides the upload option and allows user to continue without drawing if editor specifies", async () => {
Expand Down
137 changes: 93 additions & 44 deletions editor.planx.uk/src/@planx/components/DrawBoundary/Public/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@ import { PrivateFileUpload } from "@planx/components/shared/PrivateFileUpload/Pr
import { squareMetresToHectares } from "@planx/components/shared/utils";
import type { PublicProps } from "@planx/components/ui";
import buffer from "@turf/buffer";
import { type Feature, point } from "@turf/helpers";
import { type Feature,point } from "@turf/helpers";
import { Store, useStore } from "pages/FlowEditor/lib/store";
import React, { useEffect, useRef, useState } from "react";
import { FONT_WEIGHT_SEMI_BOLD } from "theme";
import FullWidthWrapper from "ui/public/FullWidthWrapper";
import ErrorWrapper from "ui/shared/ErrorWrapper";
import { array } from "yup";

import {
DrawBoundary,
Expand All @@ -31,6 +33,21 @@ export type Props = PublicProps<DrawBoundary>;

export type Boundary = Feature | undefined;

const slotsSchema = array()
.required()
.test({
name: "nonUploading",
message: "Upload a location plan.",
test: (slots?: Array<FileUploadSlot>) => {
return Boolean(
slots &&
slots.length === 1 &&
!slots.some((slot) => slot.status === "uploading") &&
slots.every((slot) => slot.url && slot.status === "success"),
);
},
});

export default function Component(props: Props) {
const isMounted = useRef(false);
const passport = useStore((state) => state.computePassport());
Expand All @@ -52,7 +69,9 @@ export default function Component(props: Props) {
props.previouslySubmittedData?.data?.[PASSPORT_UPLOAD_KEY];
const startPage = previousFile ? "upload" : "draw";
const [page, setPage] = useState<"draw" | "upload">(startPage);

const [slots, setSlots] = useState<FileUploadSlot[]>(previousFile ?? []);
const [fileValidationError, setFileValidationError] = useState<string>();

const addressPoint =
passport?.data?._address?.longitude &&
Expand Down Expand Up @@ -94,44 +113,69 @@ export default function Component(props: Props) {
};
}, [page, setArea, setBoundary, setSlots]);

return (
<Card
handleSubmit={() => {
const newPassportData: Store.userData["data"] = {};
/**
* Declare a ref to hold a mutable copy the up-to-date validation error.
* The intention is to prevent frequent unnecessary update loops that clears the
* validation error state if it is already empty.
*/
const validationErrorRef = useRef(fileValidationError);
useEffect(() => {
validationErrorRef.current = fileValidationError;
}, [fileValidationError]);

useEffect(() => {
if (validationErrorRef.current) {
setFileValidationError(undefined);
}
}, [slots]);

// Used the map
if (page === "draw" && boundary && props.dataFieldBoundary) {
newPassportData[props.dataFieldBoundary] = boundary;
newPassportData[`${props.dataFieldBoundary}.buffered`] = buffer(
boundary,
bufferInMeters,
{ units: "meters" },
);
const validateAndSubmit = () => {
const newPassportData: Store.userData["data"] = {};

if (area && props.dataFieldArea) {
newPassportData[props.dataFieldArea] = area;
newPassportData[`${props.dataFieldArea}.hectares`] =
squareMetresToHectares(area);
}
// Used the map
if (page === "draw") {
if (boundary && props.dataFieldBoundary) {
newPassportData[props.dataFieldBoundary] = boundary;
newPassportData[`${props.dataFieldBoundary}.buffered`] = buffer(
boundary,
bufferInMeters,
{ units: "meters" },
);

// Track the type of map interaction
if (
boundary?.geometry ===
passport.data?.["property.boundary.title"]?.geometry
) {
newPassportData[PASSPORT_COMPONENT_ACTION_KEY] =
DrawBoundaryUserAction.Accept;
} else if (boundary?.properties?.dataset === "title-boundary") {
newPassportData[PASSPORT_COMPONENT_ACTION_KEY] =
DrawBoundaryUserAction.Amend;
} else {
newPassportData[PASSPORT_COMPONENT_ACTION_KEY] =
DrawBoundaryUserAction.Draw;
}
if (area && props.dataFieldArea) {
newPassportData[props.dataFieldArea] = area;
newPassportData[`${props.dataFieldArea}.hectares`] =
squareMetresToHectares(area);
}

// Track the type of map interaction
if (
boundary?.geometry ===
passport.data?.["property.boundary.title"]?.geometry
) {
newPassportData[PASSPORT_COMPONENT_ACTION_KEY] =
DrawBoundaryUserAction.Accept;
} else if (boundary?.properties?.dataset === "title-boundary") {
newPassportData[PASSPORT_COMPONENT_ACTION_KEY] =
DrawBoundaryUserAction.Amend;
} else {
newPassportData[PASSPORT_COMPONENT_ACTION_KEY] =
DrawBoundaryUserAction.Draw;
}

// Uploaded a file
if (page === "upload" && slots.length) {
props.handleSubmit?.({ data: { ...newPassportData } });
}

if (props.hideFileUpload && !boundary) {
props.handleSubmit?.({ data: { ...newPassportData } });
}
}

// Uploaded a file
if (page === "upload") {
slotsSchema
.validate(slots, { context: { slots } })
.then(() => {
newPassportData[PASSPORT_UPLOAD_KEY] = slots;
newPassportData[PASSPORT_COMPONENT_ACTION_KEY] =
DrawBoundaryUserAction.Upload;
Expand All @@ -146,24 +190,27 @@ export default function Component(props: Props) {
recommended,
optional,
};
}

props.handleSubmit?.({ data: { ...newPassportData } });
}}
props.handleSubmit?.({ data: { ...newPassportData } });
})
.catch((err) => setFileValidationError(err?.message));
}
};

return (
<Card
handleSubmit={validateAndSubmit}
isValid={
props.hideFileUpload
? true
: Boolean(
(page === "draw" && boundary) ||
(page === "upload" && slots[0]?.url),
)
: Boolean((page === "draw" && boundary) || page === "upload")
}
>
{getBody(bufferInMeters)}
{getBody(bufferInMeters, fileValidationError)}
</Card>
);

function getBody(bufferInMeters: number) {
function getBody(bufferInMeters: number, fileValidationError?: string) {
if (page === "draw") {
return (
<>
Expand Down Expand Up @@ -257,7 +304,9 @@ export default function Component(props: Props) {
howMeasured={props.howMeasured}
definitionImg={props.definitionImg}
/>
<PrivateFileUpload slots={slots} setSlots={setSlots} maxFiles={1} />
<ErrorWrapper error={fileValidationError} id="upload-location-plan">
<PrivateFileUpload slots={slots} setSlots={setSlots} maxFiles={1} />
</ErrorWrapper>
<Box sx={{ textAlign: "right" }}>
<Link
component="button"
Expand Down
18 changes: 16 additions & 2 deletions editor.planx.uk/src/@planx/components/FileUpload/Public.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,30 @@ import { axe, setup } from "testUtils";
import { PASSPORT_REQUESTED_FILES_KEY } from "../FileUploadAndLabel/model";
import FileUpload from "./Public";

test("renders correctly and blocks submit if there are no files added", async () => {
test("renders correctly", async () => {
const handleSubmit = jest.fn();

setup(<FileUpload fn="someKey" handleSubmit={handleSubmit} />);

expect(screen.getByRole("button", { name: "Continue" })).toBeDisabled();
expect(screen.getByRole("button", { name: "Continue" })).toBeEnabled();

expect(handleSubmit).toHaveBeenCalledTimes(0);
});

test("shows error if user tries to continue before adding files", async () => {
const handleSubmit = jest.fn();

const { user } = setup(
<FileUpload fn="elevations" id="elevations" handleSubmit={handleSubmit} />,
);

await user.click(screen.getByTestId("continue-button"));
expect(screen.getByText("Upload at least one file.")).toBeInTheDocument();

// Blocked by validation error
expect(handleSubmit).toHaveBeenCalledTimes(0);
});

test("recovers previously submitted files when clicking the back button", async () => {
const handleSubmit = jest.fn();
const componentId = uniqueId();
Expand Down
8 changes: 1 addition & 7 deletions editor.planx.uk/src/@planx/components/FileUpload/Public.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,13 +115,7 @@ const FileUpload: React.FC<Props> = (props) => {
}, [slots]);

return (
<Card
isValid={
slots.length > 0 &&
slots.every((slot) => slot.url && slot.status === "success")
}
handleSubmit={handleSubmit}
>
<Card isValid={true} handleSubmit={handleSubmit}>
<QuestionHeader
title={props.title}
description={props.description}
Expand Down
Loading