Skip to content

Commit 586ff7a

Browse files
committed
Merge branch 'fix/modal-keyboard-nav-950' of EnsiyehE/opencast-admin-interface into develop
Pull request #1387 Fix/modal keyboard nav 950
2 parents 745e3ac + dab3f71 commit 586ff7a

6 files changed

Lines changed: 150 additions & 99 deletions

File tree

src/components/events/partials/ModalTabsAndPages/NewMetadataPage.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ const NewMetadataPage = ({
1818
header?: ParseKeys
1919
}) => {
2020
const { t } = useTranslation();
21-
2221
return (
2322
<ModalContentTable>
2423
{

src/components/shared/DropDown.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ const DropDown = <T, >({
209209
<AsyncSelect
210210
ref={selectRef}
211211
{...commonProps}
212+
openMenuOnFocus={false}
212213
noOptionsMessage={() => t("SELECT_NO_MATCHING_RESULTS")}
213214
cacheOptions
214215
defaultOptions={formatOptions(

src/components/shared/NewResourceModal.tsx

Lines changed: 66 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -11,67 +11,79 @@ import { Modal, ModalHandle } from "./modals/Modal";
1111
/**
1212
* This component renders the modal for adding new resources
1313
*/
14-
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";
1521

1622
const NewResourceModal = ({
17-
handleClose,
18-
resource,
19-
modalRef,
23+
handleClose,
24+
resource,
25+
modalRef,
2026
}: {
21-
handleClose: () => void,
22-
resource: "events" | "series" | "user" | "group" | "acl" | "themes"
23-
modalRef: React.RefObject<ModalHandle | null>
27+
handleClose: () => void;
28+
resource: "events" | "series" | "user" | "group" | "acl" | "themes";
29+
modalRef: React.RefObject<ModalHandle | null>;
2430
}) => {
25-
const { t } = useTranslation();
31+
const { t } = useTranslation();
2632

27-
const close = () => {
28-
handleClose();
29-
};
33+
const close = () => {
34+
handleClose();
35+
};
3036

31-
const headerText = () => {
32-
switch (resource) {
33-
case "events": return t("EVENTS.EVENTS.NEW.CAPTION");
34-
case "series": return t("EVENTS.SERIES.NEW.CAPTION");
35-
case "themes": return t("CONFIGURATION.THEMES.DETAILS.NEWCAPTION");
36-
case "acl": return t("USERS.ACLS.NEW.CAPTION");
37-
case "group": return t("USERS.GROUPS.NEW.CAPTION");
38-
case "user": return t("USERS.USERS.DETAILS.NEWCAPTION");
39-
}
40-
};
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+
};
4153

42-
return (
43-
<Modal
44-
header={headerText()}
45-
classId="add-event-modal"
46-
// initialFocus={"#firstField"}
47-
ref={modalRef}
48-
>
49-
{resource === "events" && (
50-
// New Event Wizard
51-
<NewEventWizard close={close} />
52-
)}
53-
{resource === "series" && (
54-
// New Series Wizard
55-
<NewSeriesWizard close={close} />
56-
)}
57-
{resource === "themes" && (
58-
// New Theme Wizard
59-
<NewThemeWizard close={close} />
60-
)}
61-
{resource === "acl" && (
62-
// New ACL Wizard
63-
<NewAclWizard close={close} />
64-
)}
65-
{resource === "group" && (
66-
// New Group Wizard
67-
<NewGroupWizard close={close} />
68-
)}
69-
{resource === "user" && (
70-
// New User Wizard
71-
<NewUserWizard close={close} />
72-
)}
73-
</Modal>
74-
);
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+
);
7587
};
7688

7789
export default NewResourceModal;

src/components/shared/modals/Modal.tsx

Lines changed: 51 additions & 42 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";
@@ -8,55 +14,58 @@ import { FocusTrap } from "focus-trap-react";
814
import { LuX } from "react-icons/lu";
915

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

2228
export type ModalHandle = {
23-
open: () => void;
24-
close?: () => void;
25-
isOpen?: () => boolean;
29+
open: () => void;
30+
close?: () => void;
31+
isOpen?: () => boolean;
2632
};
2733

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

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

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

48-
useImperativeHandle(ref, () => ({
49-
isOpen: () => isOpen,
50-
open: () => setOpen(true),
51-
close,
52-
}), [close, isOpen]);
53-
54-
useHotkeys(
55-
availableHotkeys.general.CLOSE_MODAL.sequence,
56-
close,
57-
{ description: t(availableHotkeys.general.CLOSE_MODAL.description) ?? undefined },
58-
[ref],
59-
);
60+
useHotkeys(
61+
availableHotkeys.general.CLOSE_MODAL.sequence,
62+
close,
63+
{
64+
description:
65+
t(availableHotkeys.general.CLOSE_MODAL.description) ?? undefined,
66+
},
67+
[ref],
68+
);
6069

6170
return ReactDOM.createPortal(
6271
isOpen &&

src/components/shared/wizard/RenderMultiField.tsx

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,10 @@ const RenderMultiField = ({
117117
field={field}
118118
form={form}
119119
showCheck={showCheck}
120+
onBlur = {() => {
121+
submitValue();
122+
setEditMode(false);
123+
}}
120124
/>
121125
)
122126
);
@@ -200,14 +204,29 @@ const ShowValue = ({
200204
form: { initialValues },
201205
field,
202206
showCheck,
207+
onBlur,
203208
}: {
204-
setEditMode: (e: boolean) => void
209+
setEditMode: (e: boolean) => void
205210
form: FieldProps["form"]
206211
field: FieldProps["field"]
207212
showCheck: boolean,
213+
onBlur: () => void
208214
}) => {
209215
return (
210-
<div onClick={() => setEditMode(true)} className="show-edit">
216+
<div
217+
tabIndex={0}
218+
onClick={() => setEditMode(true)}
219+
onFocus={() => setEditMode(true)} // <-- activate edit mode on focus
220+
onKeyDown={e => {
221+
if (e.key === "Enter" || e.key === " ") {
222+
setEditMode(true);
223+
e.preventDefault();
224+
}
225+
}}
226+
227+
onBlur={onBlur}
228+
className="show-edit"
229+
>
211230
{field.value instanceof Array && field.value.length !== 0 ? (
212231
<ul>
213232
{field.value.map((item, key) => (

src/hooks/wizardHooks.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,17 @@ export const useClickOutsideField = (
118118
childRef.current.focus();
119119
}
120120

121+
const handleBlur = (e: FocusEvent) => {
122+
// Check if blur moves to an element outside childRef
123+
if (childRef.current && !childRef.current.contains(e.relatedTarget as Node)) {
124+
setEditMode(false);
125+
}
126+
};
127+
128+
if (childRef.current) {
129+
childRef.current.addEventListener("blur", handleBlur, true); // capture phase
130+
}
131+
121132
// Adding event listener for detecting click outside
122133
window.addEventListener("mousedown", handleClickOutside);
123134

0 commit comments

Comments
 (0)