Skip to content

Commit 4c803ca

Browse files
committed
Resolve the keyboard-nav by pressing tab
1 parent f220126 commit 4c803ca

2 files changed

Lines changed: 147 additions & 177 deletions

File tree

src/components/shared/NewResourceModal.tsx

Lines changed: 66 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -7,123 +7,83 @@ import NewAclWizard from "../users/partials/wizard/NewAclWizard";
77
import NewGroupWizard from "../users/partials/wizard/NewGroupWizard";
88
import NewUserWizard from "../users/partials/wizard/NewUserWizard";
99
import { Modal, ModalHandle } from "./modals/Modal";
10-
import { useState } from "react";
1110

1211
/**
1312
* This component renders the modal for adding new resources
1413
*/
15-
export type NewResource = "events" | "series" | "user" | "group" | "acl" | "themes";
14+
export type NewResource =
15+
| "events"
16+
| "series"
17+
| "user"
18+
| "group"
19+
| "acl"
20+
| "themes";
1621

1722
const NewResourceModal = ({
18-
handleClose,
19-
resource,
20-
modalRef,
23+
handleClose,
24+
resource,
25+
modalRef,
2126
}: {
22-
handleClose: () => void,
23-
resource: "events" | "series" | "user" | "group" | "acl" | "themes"
24-
modalRef: React.RefObject<ModalHandle | null>
27+
handleClose: () => void;
28+
resource: "events" | "series" | "user" | "group" | "acl" | "themes";
29+
modalRef: React.RefObject<ModalHandle | null>;
2530
}) => {
26-
const { t } = useTranslation();
27-
const [mode, setMode] = useState<"form" | "confirm">("form");
28-
const [closeRequested, setCloseRequested] = useState(false);
31+
const { t } = useTranslation();
2932

30-
const close = () => {
31-
handleClose();
32-
};
33+
const close = () => {
34+
handleClose();
35+
};
3336

34-
const onAttemptClose = () => {
35-
if (mode === "form" && modalRef.current?.isOpen?.()) {
36-
setCloseRequested(true);
37-
return false; // prevent close, show confirmation modal
38-
}
39-
if (mode === "confirm") {
40-
cancelClose(); // go back to form mode
41-
return true; // make modal close
42-
}
43-
return true; // default allow close
44-
};
37+
const headerText = () => {
38+
switch (resource) {
39+
case "events":
40+
return t("EVENTS.EVENTS.NEW.CAPTION");
41+
case "series":
42+
return t("EVENTS.SERIES.NEW.CAPTION");
43+
case "themes":
44+
return t("CONFIGURATION.THEMES.DETAILS.NEWCAPTION");
45+
case "acl":
46+
return t("USERS.ACLS.NEW.CAPTION");
47+
case "group":
48+
return t("USERS.GROUPS.NEW.CAPTION");
49+
case "user":
50+
return t("USERS.USERS.DETAILS.NEWCAPTION");
51+
}
52+
};
4553

46-
// When closeRequested is set, switch to confirm mode
47-
useEffect(() => {
48-
if (closeRequested) {
49-
setMode("confirm");
50-
setCloseRequested(false);
51-
}
52-
}, [closeRequested]);
53-
54-
const confirmClose = () => {
55-
modalRef.current?.close?.();
56-
};
57-
const cancelClose = () => {
58-
setMode("form");
59-
};
60-
61-
const headerText = () => {
62-
switch (resource) {
63-
case "events": return t("EVENTS.EVENTS.NEW.CAPTION");
64-
case "series": return t("EVENTS.SERIES.NEW.CAPTION");
65-
case "themes": return t("CONFIGURATION.THEMES.DETAILS.NEWCAPTION");
66-
case "acl": return t("USERS.ACLS.NEW.CAPTION");
67-
case "group": return t("USERS.GROUPS.NEW.CAPTION");
68-
case "user": return t("USERS.USERS.DETAILS.NEWCAPTION");
69-
}
70-
};
71-
72-
return (
73-
<Modal
74-
header={mode === "confirm" ? "Confirm Close" : headerText()}
75-
classId="add-event-modal"
76-
// initialFocus={"#firstField"}
77-
ref={modalRef}
78-
closeCallback={onAttemptClose}
79-
>
80-
<div style={{ display: mode === "form" ? "block" : "none" }}>
81-
{resource === "events" && (
82-
// New Event Wizard
83-
<NewEventWizard close={close} />
84-
)}
85-
{resource === "series" && (
86-
// New Series Wizard
87-
<NewSeriesWizard close={close} />
88-
)}
89-
{resource === "themes" && (
90-
// New Theme Wizard
91-
<NewThemeWizard close={close} />
92-
)}
93-
{resource === "acl" && (
94-
// New ACL Wizard
95-
<NewAclWizard close={close} />
96-
)}
97-
{resource === "group" && (
98-
// New Group Wizard
99-
<NewGroupWizard close={close} />
100-
)}
101-
{resource === "user" && (
102-
// New User Wizard
103-
<NewUserWizard close={close} />
104-
)}
105-
</div>
106-
{mode === "confirm" && (
107-
<div style={{ padding: "1rem" }}>
108-
<p>Are you sure you want to close? Your changes will be lost.</p>
109-
<div style={{ display: "flex", justifyContent: "flex-end", gap: "1rem", marginTop: "1rem" }}>
110-
<button
111-
style={{ backgroundColor: "red", color: "white", padding: "0.5rem 1rem" }}
112-
onClick={confirmClose}
113-
>
114-
Yes, Close
115-
</button>
116-
<button
117-
style={{ backgroundColor: "gray", color: "white", padding: "0.5rem 1rem" }}
118-
onClick={cancelClose}
119-
>
120-
No, Go Back
121-
</button>
122-
</div>
123-
</div>
124-
)}
125-
</Modal>
126-
);
54+
return (
55+
<Modal
56+
header={headerText()}
57+
classId="add-event-modal"
58+
// initialFocus={"#firstField"}
59+
ref={modalRef}
60+
>
61+
{resource === "events" && (
62+
// New Event Wizard
63+
<NewEventWizard close={close} />
64+
)}
65+
{resource === "series" && (
66+
// New Series Wizard
67+
<NewSeriesWizard close={close} />
68+
)}
69+
{resource === "themes" && (
70+
// New Theme Wizard
71+
<NewThemeWizard close={close} />
72+
)}
73+
{resource === "acl" && (
74+
// New ACL Wizard
75+
<NewAclWizard close={close} />
76+
)}
77+
{resource === "group" && (
78+
// New Group Wizard
79+
<NewGroupWizard close={close} />
80+
)}
81+
{resource === "user" && (
82+
// New User Wizard
83+
<NewUserWizard close={close} />
84+
)}
85+
</Modal>
86+
);
12787
};
12888

12989
export default NewResourceModal;
Lines changed: 81 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import { forwardRef, PropsWithChildren, useCallback, useImperativeHandle, useState } from "react";
1+
import {
2+
forwardRef,
3+
PropsWithChildren,
4+
useCallback,
5+
useImperativeHandle,
6+
useState,
7+
} from "react";
28
import ReactDOM from "react-dom";
39
import { useHotkeys } from "react-hotkeys-hook";
410
import { availableHotkeys } from "../../../configs/hotkeysConfig";
@@ -7,83 +13,87 @@ import ButtonLikeAnchor from "../ButtonLikeAnchor";
713
import { FocusTrap } from "focus-trap-react";
814

915
export type ModalProps = {
10-
open?: boolean
11-
// Having this return false will prevent the modal from closing
12-
closeCallback?: () => boolean
13-
/** If true, the first element in the modal automatically be focused. If false,
14-
* no element is initially focused */
15-
initialFocus?: false | string
16-
header: string
17-
classId: string
18-
className?: string
16+
open?: boolean;
17+
// Having this return false will prevent the modal from closing
18+
closeCallback?: () => boolean;
19+
/** If true, the first element in the modal automatically be focused. If false,
20+
* no element is initially focused */
21+
initialFocus?: false | string;
22+
header: string;
23+
classId: string;
24+
className?: string;
1925
};
2026

2127
export type ModalHandle = {
22-
open: () => void;
23-
close?: () => void;
24-
isOpen?: () => boolean;
28+
open: () => void;
29+
close?: () => void;
30+
isOpen?: () => boolean;
2531
};
2632

27-
export const Modal = forwardRef<ModalHandle, PropsWithChildren<ModalProps>>(({
28-
open = false,
29-
closeCallback,
30-
header,
31-
classId,
32-
className,
33-
children,
34-
}, ref) => {
33+
export const Modal = forwardRef<ModalHandle, PropsWithChildren<ModalProps>>(
34+
(
35+
{ open = false, closeCallback, header, classId, className, children },
36+
ref,
37+
) => {
38+
const { t } = useTranslation();
3539

36-
const { t } = useTranslation();
40+
const [isOpen, setOpen] = useState(open);
41+
const close = useCallback(() => {
42+
if (closeCallback !== undefined && !closeCallback()) {
43+
// Don't close modal
44+
return;
45+
}
46+
setOpen(false);
47+
}, [closeCallback]);
3748

38-
const [isOpen, setOpen] = useState(open);
39-
const close = useCallback(() => {
40-
if (closeCallback !== undefined && !closeCallback()) {
41-
// Don't close modal
42-
return;
43-
}
44-
setOpen(false);
45-
}, [closeCallback]);
49+
useImperativeHandle(
50+
ref,
51+
() => ({
52+
isOpen: () => isOpen,
53+
open: () => setOpen(true),
54+
close,
55+
}),
56+
[close, isOpen],
57+
);
4658

47-
useImperativeHandle(ref, () => ({
48-
isOpen: () => isOpen,
49-
open: () => setOpen(true),
50-
close,
51-
}), [close, isOpen]);
59+
useHotkeys(
60+
availableHotkeys.general.CLOSE_MODAL.sequence,
61+
close,
62+
{
63+
description:
64+
t(availableHotkeys.general.CLOSE_MODAL.description) ?? undefined,
65+
},
66+
[ref],
67+
);
5268

53-
useHotkeys(
54-
availableHotkeys.general.CLOSE_MODAL.sequence,
55-
close,
56-
{ description: t(availableHotkeys.general.CLOSE_MODAL.description) ?? undefined },
57-
[ref],
58-
);
69+
return ReactDOM.createPortal(
70+
isOpen && (
71+
<FocusTrap
72+
focusTrapOptions={{
73+
escapeDeactivates: false,
74+
}} // Prevent escape from deactivating the trap
75+
>
76+
<div>
77+
<div className="modal-animation modal-overlay" />
78+
<section
79+
id={classId}
80+
className={className ? className : "modal wizard modal-animation"}
81+
>
82+
<header>
83+
<ButtonLikeAnchor
84+
extraClassName="fa fa-times close-modal"
85+
onClick={close}
86+
tabIndex={0}
87+
/>
88+
<h2>{header}</h2>
89+
</header>
5990

60-
return ReactDOM.createPortal(
61-
isOpen &&
62-
<FocusTrap focusTrapOptions={{
63-
escapeDeactivates: false }} // Prevent escape from deactivating the trap
64-
>
65-
<div>
66-
<div className="modal-animation modal-overlay" />
67-
<section
68-
id={classId}
69-
className={className ? className : "modal wizard modal-animation"}
70-
>
71-
<header>
72-
<ButtonLikeAnchor
73-
extraClassName="fa fa-times close-modal"
74-
onClick={close}
75-
tabIndex={0}
76-
/>
77-
<h2>
78-
{header}
79-
</h2>
80-
</header>
81-
82-
{children}
83-
</section>
84-
</div>
85-
</FocusTrap>,
86-
document.body,
87-
);
88-
89-
});
91+
{children}
92+
</section>
93+
</div>
94+
</FocusTrap>
95+
),
96+
document.body,
97+
);
98+
},
99+
);

0 commit comments

Comments
 (0)