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

Modal focus trapping #172

Merged
merged 46 commits into from
Feb 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
af25cc4
Modal: remove unnecessary comments
susulone Feb 15, 2024
f714f26
Modal: remove dialog from the tab index
susulone Feb 15, 2024
6e69ae9
Modal: give all focusable children a tab index and make them loop
susulone Feb 15, 2024
f2b0a5e
DeleteModal: Remove dialog from the tab index. Give all focusable chi…
susulone Feb 15, 2024
22bd3de
DeleteModal: add aria label to close modal button
susulone Feb 15, 2024
6999140
SubModal: Remove dialog from the tab index. Give all focusable childr…
susulone Feb 15, 2024
d687b7b
SubModal: add aria labels for icon buttons
susulone Feb 15, 2024
a7544da
SubModal: wrap closeAllModals with use callback hook
susulone Feb 15, 2024
0440a06
Modal: remove aria modal
susulone Feb 15, 2024
44901bd
CreateLabelModal: change import to import type
susulone Feb 16, 2024
110c246
changed modal to close on mouse dow
TTyni Feb 16, 2024
952b9cc
Frontend: Kanban column polish (#167)
bkruotsalainen Feb 15, 2024
06a1581
Add missing tabIndex and onKeyDown to TaskMemberItem (#168)
bkruotsalainen Feb 15, 2024
eef6b3c
Password update validation (#166)
TTyni Feb 15, 2024
9528984
KanbanTask: Remove dialog from the tab index. Give all focusable chil…
susulone Feb 16, 2024
a626b61
CreateLabelModal: add autofocus for the input field
susulone Feb 16, 2024
1c3c904
EditLabelModal: add autofocus for the input field
susulone Feb 16, 2024
3b33eb5
KanbanTask: change closing the task modal to onMouseDown
susulone Feb 16, 2024
09f3afc
SubModal: change closing the modal to onMouseDown
susulone Feb 16, 2024
9286417
IconButton: fix sizing issues when only icon is displayed
susulone Feb 16, 2024
ce7280d
LabelModal: make checkbox focusable and selectable with keyboard
susulone Feb 16, 2024
7cae87a
LabelModal: refactor code to be able to select the task by clicking t…
susulone Feb 16, 2024
b4e6bf6
SubModal: change min-w-[400px] to only apply with larger screens
susulone Feb 16, 2024
0003f0f
LabelModal: give label modal a max height and give overflow a scroll
susulone Feb 16, 2024
74dfa4f
TaskMembersModal: give modal a max height and set overflow to scroll
susulone Feb 16, 2024
9a6a9bd
Modal: remove unnecessary styling from the dialog and change overflow…
susulone Feb 16, 2024
59ca4d1
ProjectMembersModal: give div a max height and set overflow to auto
susulone Feb 16, 2024
3eada3a
SubModal: remove unnecessary styling from the dialog
susulone Feb 16, 2024
462e414
DeleteEventModal: refactor button to enable better styling
susulone Feb 16, 2024
940f3c8
CalendarEventModal: Remove dialog from the tab index. Give all focusa…
susulone Feb 16, 2024
8d6eff3
make suggested fixes to LabelModal and TaskMemberItem
susulone Feb 19, 2024
dc0a833
fix overflow issues in horizontal mobile screens
susulone Feb 19, 2024
7eb768f
fix overflow issues in horizontal mobile screens
susulone Feb 19, 2024
dd1727f
DeleteModal: fix issues on vertical mobile screen
susulone Feb 19, 2024
971a9cb
fix overflow issues in horizontal mobile screens (Modal)
susulone Feb 19, 2024
76bcace
frontend: pass only required events to eventmodal
Moiman Feb 16, 2024
0aaaed1
frontend: calendar change from y.array to y.map
Moiman Feb 16, 2024
c564bb6
frontend: increase event maxlength (#170)
Moiman Feb 16, 2024
7ffcbc0
frontend: invalidate projects cache on userUpdate
Moiman Feb 16, 2024
d2a3529
frontend: update backend url
Moiman Feb 19, 2024
b78c750
Frontend: Polish kanban task (#173)
bkruotsalainen Feb 19, 2024
c580bdc
KanbanTask: Remove dialog from the tab index. Give all focusable chil…
susulone Feb 16, 2024
c1b6003
KanbanTask: fix deadline label styling
susulone Feb 19, 2024
6c026ad
Modal: change overflow-y-hidden to auto
susulone Feb 19, 2024
5c1587f
ProjectMembersModal: change max height
susulone Feb 19, 2024
de54364
Merge branch 'main' into modal-focus-trapping
susulone Feb 19, 2024
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
58 changes: 51 additions & 7 deletions frontend/src/components/DeleteModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,36 +18,80 @@ export const DeleteModal: React.FunctionComponent<propTypes> = ({
deleteModalText,
}) => {
const screenDimensions = useScreenDimensions();
const modalRef = React.useRef(null as HTMLDialogElement | null);

React.useEffect(() => {
if (confirmDeleteEdit) {
const modalElement = modalRef.current;
const focusableElements = modalElement!.querySelectorAll(
"button, [tabindex]:not([tabindex=\"-1\"])"
);

const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];

const handleTabKeyPress = (event: KeyboardEvent) => {
if (event.key === "Tab") {
if (event.shiftKey && document.activeElement === firstElement) {
event.preventDefault();
(lastElement as HTMLDialogElement).focus();
} else if (
!event.shiftKey &&
document.activeElement === lastElement
) {
event.preventDefault();
(firstElement as HTMLDialogElement).focus();
}
}
};

const closeOnEscapePressed = (e: KeyboardEvent) => {
if (e.key === "Escape") {
setConfirmDeleteEdit(false);
}
};
document.addEventListener("keydown", closeOnEscapePressed);
modalElement?.addEventListener("keydown", handleTabKeyPress);
return () => {

document.removeEventListener("keydown", closeOnEscapePressed);
modalElement?.removeEventListener("keydown", handleTabKeyPress);
};
}
}, [confirmDeleteEdit, setConfirmDeleteEdit]);

return (
<div
className={`fixed z-50 flex inset-0 justify-center items-center transition-colors rounded ${
confirmDeleteEdit ? "visible bg-dark-blue-100/40" : "invisible"}`}
onClick={() => setConfirmDeleteEdit(!confirmDeleteEdit)}
onMouseDown={() => setConfirmDeleteEdit(!confirmDeleteEdit)}
>
<div
onClick={(e) => e.stopPropagation()}
<dialog
tabIndex={-1}
ref={modalRef}
onMouseDown={(e) => e.stopPropagation()}
className={`p-2 pb-4 flex flex-col inset-0 sm:justify-start items-left overflow-x-hidden overflow-y-auto outline-none rounded focus:outline-none shadow transition-all bg-grayscale-100 ${
confirmDeleteEdit ? "scale-100 opacity-100" : "scale-110 opacity-0"}
${screenDimensions.height < 400 ? "min-h-screen max-h-screen w-full" : "w-full h-full sm:h-fit sm:w-fit sm:max-w-2xl"}
}`}
>
<div className="w-full flex flex-col place-items-end">
<button
aria-label="Close modal"
onClick={() => setConfirmDeleteEdit(!confirmDeleteEdit)}
className="p-1 text-dark-font bg-grayscale-0 hover:bg-grayscale-0"
>
<X size={20} />
</button>
</div>

<main className="w-min mx-auto px-2">
<main className="w-full sm:w-min mx-auto px-2">
{deleteModalTitle &&
<p className="w-max px-2 mx-auto mb-2 heading-sm text-center">
<p className="w-full sm:w-max px-2 mx-auto mb-2 heading-sm text-center">
{deleteModalTitle}
</p>
}
<p className="w-11/12 mx-auto mb-6 body-text-md text-center">{deleteModalText}</p>
<p className="sm:w-11/12 mx-auto mb-6 body-text-md text-center">{deleteModalText}</p>

<section className="w-full min-w-max mx-auto grid grid-cols-2 gap-6">
<button
Expand All @@ -68,7 +112,7 @@ export const DeleteModal: React.FunctionComponent<propTypes> = ({
</section>
</main>

</div>
</dialog>
</div>
);
};
61 changes: 46 additions & 15 deletions frontend/src/components/Modal.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type ReactElement, useState, createContext, useEffect } from "react";
import { type ReactElement, useState, createContext, useEffect, useRef } from "react";
import { X } from "react-feather";
import useScreenDimensions from "../utils/screenDimensions";

Expand Down Expand Up @@ -27,6 +27,7 @@ export const Modal = ({
}: ModalProps) => {
const [isModalOpen, setIsModalOpen] = useState(false);
const screenDimensions = useScreenDimensions();
const modalRef = useRef(null as HTMLDialogElement | null);

const openModal = () => {
setIsModalOpen(true);
Expand All @@ -37,15 +38,44 @@ export const Modal = ({
};

useEffect(() => {
const closeOnEscapePressed = (e: KeyboardEvent) => {
if (e.key === "Escape") {
closeModal();
}
};
document.addEventListener("keydown", closeOnEscapePressed);
return () =>
document.removeEventListener("keydown", closeOnEscapePressed);
}, []);
if (isModalOpen) {
const modalElement = modalRef.current;
const focusableElements = modalElement!.querySelectorAll(
"button, input, select, textarea, [tabindex]:not([tabindex=\"-1\"])"
);

const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];

const handleTabKeyPress = (event: KeyboardEvent) => {
if (event.key === "Tab") {
if (event.shiftKey && document.activeElement === firstElement) {
event.preventDefault();
(lastElement as HTMLDialogElement).focus();
} else if (
!event.shiftKey &&
document.activeElement === lastElement
) {
event.preventDefault();
(firstElement as HTMLDialogElement).focus();
}
}
};

const closeOnEscapePressed = (e: KeyboardEvent) => {
if (e.key === "Escape") {
closeModal();
}
};
document.addEventListener("keydown", closeOnEscapePressed);
modalElement?.addEventListener("keydown", handleTabKeyPress);
return () => {

document.removeEventListener("keydown", closeOnEscapePressed);
modalElement?.removeEventListener("keydown", handleTabKeyPress);
};
}
}, [isModalOpen]);

return (
<>
Expand All @@ -61,18 +91,19 @@ export const Modal = ({

{isModalOpen &&
<div
onClick={closeModal}
onMouseDown={closeModal}
className="fixed flex justify-center inset-0 z-30 items-center transition-colors bg-dark-blue-100/40"
>
<dialog
aria-modal="true"
onClick={(e) => e.stopPropagation()}
// The sizing of the modal (w, min-w and max-w) might need to be modified
className={`fixed p-2 pb-4 flex flex-col inset-0 z-30 max-h-screen sm:justify-start items-left overflow-x-hidden overflow-y-auto outline-none sm:rounded focus:outline-none shadow transition-all
tabIndex={-1}
ref={modalRef}
onMouseDown={(e) => e.stopPropagation()}
className={`fixed p-2 pb-4 flex flex-col inset-0 z-30 max-h-screen sm:justify-start items-left overflow-x-hidden overflow-y-auto sm:rounded shadow transition-all
${screenDimensions.height < 500 ? "min-h-screen w-full" : "w-full h-full sm:h-fit sm:w-fit sm:max-w-2xl"}`}>
<header className="w-full flex flex-col mb-2 place-items-end">
<button
onClick={closeModal}
aria-label="Close modal"
className="p-1 text-dark-font bg-grayscale-0 hover:bg-grayscale-0">
<X size={20}/>
</button>
Expand Down
57 changes: 45 additions & 12 deletions frontend/src/features/calendar/CalendarEventModal.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState, useEffect, useMemo } from "react";
import { useState, useEffect, useMemo, useRef } from "react";
import { useParams } from "react-router-dom";
import { nanoid } from "@reduxjs/toolkit";
import {
Expand Down Expand Up @@ -30,6 +30,7 @@ const CalendarEventModal = ({ events, currentMonth, day, yevents }: Props) => {
const [newDate, setNewDate] = useState(day);
const [newDateOnCreate, setNewDateOnCreate] = useState(day);
const [activeEdit, setActiveEdit] = useState<string>("");
const calendarModalRef = useRef(null as HTMLDialogElement | null);

const projectId = parseInt(useParams().projectId!);
const { data: project } = useGetProjectQuery(projectId);
Expand Down Expand Up @@ -114,15 +115,44 @@ const CalendarEventModal = ({ events, currentMonth, day, yevents }: Props) => {
};

useEffect(() => {
const closeOnEscapePressed = (e: KeyboardEvent) => {
if (e.key === "Escape") {
closeModal();
}
};
document.addEventListener("keydown", closeOnEscapePressed);
return () =>
document.removeEventListener("keydown", closeOnEscapePressed);
}, []);
if (isModalOpen) {
const calendarModalElement = calendarModalRef.current;
const focusableElements = calendarModalElement!.querySelectorAll(
"button, input, select, textarea, [tabindex]:not([tabindex=\"-1\"])"
);

const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];

const handleTabKeyPress = (event: KeyboardEvent) => {
if (event.key === "Tab") {
if (event.shiftKey && document.activeElement === firstElement) {
event.preventDefault();
(lastElement as HTMLDialogElement).focus();
} else if (
!event.shiftKey &&
document.activeElement === lastElement
) {
event.preventDefault();
(firstElement as HTMLDialogElement).focus();
}
}
};

const closeOnEscapePressed = (e: KeyboardEvent) => {
if (e.key === "Escape") {
closeModal();
}
};
document.addEventListener("keydown", closeOnEscapePressed);
calendarModalElement?.addEventListener("keydown", handleTabKeyPress);
return () => {

document.removeEventListener("keydown", closeOnEscapePressed);
calendarModalElement?.removeEventListener("keydown", handleTabKeyPress);
};
}
}, [isModalOpen]);

return (
<>
Expand Down Expand Up @@ -165,16 +195,19 @@ const CalendarEventModal = ({ events, currentMonth, day, yevents }: Props) => {
</section>
{isModalOpen &&
<div
onClick={closeModal}
onMouseDown={closeModal}
className="fixed flex justify-center inset-0 z-30 items-center transition-colors bg-dark-blue-100/40">
<dialog
onClick={(e) => e.stopPropagation()}
tabIndex={-1}
ref={calendarModalRef}
onMouseDown={(e) => e.stopPropagation()}
className={`fixed p-2 pb-4 flex flex-col inset-0 z-30 max-h-screen sm:justify-start items-left overflow-x-hidden overflow-y-auto
outline-none sm:rounded focus:outline-none shadow transition-all
${screenDimensions.height < 500 ? "min-h-screen w-full" : "w-full h-full sm:h-fit sm:w-fit sm:max-w-2xl"}`}>
<header className="flex flex-col mb-2 place-items-end">
<button
onClick={closeModal}
aria-label="Close modal"
className="p-1 text-dark-font bg-grayscale-0 hover:bg-grayscale-0">
<X size={20} />
</button>
Expand Down
9 changes: 5 additions & 4 deletions frontend/src/features/calendar/DeleteEventModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,16 @@ export const DeleteEventModal = ({ deleteEvent, eventId }: Props) => {

return (
<>
<Trash2
className="grid cursor-pointer text-caution-100 focus:outline-none focus:ring focus:ring-dark-blue-50 rounded"
<button
className="grid p-1 cursor-pointer text-caution-100 bg-grayscale-0 hover:bg-grayscale-0 focus:outline-none focus:ring focus:ring-dark-blue-50 rounded"
onClick={() => openModal()}
tabIndex={0}
onKeyDown={(e) => {
if (e.key !== "Enter") return;
openModal();
}}
/>
>
<Trash2 />
</button>

{isDeleteModalOpen && (
<DeleteModal
Expand Down
7 changes: 4 additions & 3 deletions frontend/src/features/kanban/CreateLabelModal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useContext, useState } from "react";
import { ColorModal } from "./ColorModal";
import { Labels } from "./Kanban";
import type { Labels } from "./Kanban";
import { FieldErrors, useForm } from "react-hook-form";
import { createLabelSchema } from "./labelValidation";
import { yupResolver } from "@hookform/resolvers/yup";
Expand Down Expand Up @@ -76,6 +76,7 @@ export const CreateLabelModal = ({
<input
type="text"
{...register("name")}
autoFocus
placeholder="Title for label"
className="block w-full body-text-sm py-1 px-2 mt-1.5 border-grayscale-300"
/>
Expand All @@ -94,8 +95,8 @@ export const CreateLabelModal = ({
</label>
<div className="grid grid-cols-3 gap-2 mt-1.5">
{labelColors.map((label) => (
<div key={label.id}
className={label.color === selectedColor ? "outline outline-grayscale-400 rounded" : ""}
<div key={label.id}
className={label.color === selectedColor ? "outline outline-grayscale-400 rounded" : ""}
onClick={() => setSelectedColor(label.color)}>
<ColorModal
setValue={setValue}
Expand Down
1 change: 1 addition & 0 deletions frontend/src/features/kanban/EditLabelModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export const EditLabelModal = ({
<input
type="text"
{...register("name")}
autoFocus
placeholder={label.name}
className="block w-full body-text-sm py-1 px-2 mt-1.5 border-grayscale-300"
/>
Expand Down
12 changes: 7 additions & 5 deletions frontend/src/features/kanban/IconButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ interface IconButtonProps {
handleOnClick: () => void;
}

export const IconButton = ({iconName, btnText, btnType = "button", handleOnClick}: IconButtonProps) => {
export const IconButton = ({iconName, btnText = "", btnType = "button", handleOnClick}: IconButtonProps) => {
const getIconFromName = (iconName: IconType) => {
switch (iconName) {
case "Edit":
Expand Down Expand Up @@ -55,17 +55,19 @@ export const IconButton = ({iconName, btnText, btnType = "button", handleOnClick
type={btnType}
onClick={handleOnClick}
aria-label={getAriaLabelText(iconName)}
className={`w-full px-4 pt-2 pb-1 btn-text-xs ${
className={`btn-text-xs ${
iconName === "Save" && "bg-success-100 hover:bg-success-200"
} ${iconName === "Delete" && "bg-caution-100 hover:bg-caution-200"} ${
iconName === "none" ? "sm:text-center pb-1.5" : "sm:text-left"
} ${iconName === "Edit" && "bg-grayscale-0 hover:bg-grayscale-0"}`}
} ${iconName === "Edit" && "bg-grayscale-0 hover:bg-grayscale-0"} ${
btnText === "" ? "w-fit h-fit p-1 pb-0 place-items-center" : "w-full px-4 pt-2 pb-1"}`}
>
<span className="inline-flex">
{getIconFromName(iconName)}
<p
className={`ms-[6px] ${
iconName === "none" ? "visible" : "hidden"
className={`${
iconName === "none" ? "visible" : "hidden"} ${
btnText === "" ? "hidden" : "ms-[6px] visible"
} sm:inline-block`}
>
{btnText}
Expand Down
Loading