Skip to content

Commit

Permalink
Restrict tab focus to within modal dialogues when open (fixes #1070 and
Browse files Browse the repository at this point in the history
#1072) (#1163)

* Issues #1070 and #1072 Restrict tab focus to within modal dialogue elements when model-dialogue is open

- Add key listener override on Dialogue.ts which is the base class for most Dialogues
- Added code to do the same thing for the React Dowload Dialogue

* Amendments to Issues #1070 and #1072 Restrict tab focus to within modal dialogue elements when model-dialogue is open

- Changed the way in which the DownloadDialogue.tsx establishes the first element to give focus to on load
- Extracted the focusable elements logic to use for the above
- Removed added whitespace from Dialogue.ts

---------

Co-authored-by: thatton <[email protected]>
  • Loading branch information
thattonBL and attonbomb authored Oct 23, 2024
1 parent 1685b15 commit b024fbf
Show file tree
Hide file tree
Showing 2 changed files with 82 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@ const DownloadDialogue = ({
triggerButton: HTMLElement;
}) => {
const ref = useRef<HTMLDivElement>(null);

const [position, setPosition] = useState({ top: "0px", left: "0px" });
const [arrowPosition, setArrowPosition] = useState("0px 0px");
const [selectedPage, setSelectedPage] = useState<"left" | "right">("left");
Expand Down Expand Up @@ -109,12 +108,59 @@ const DownloadDialogue = ({

setPosition({ top: `${top}px`, left: `${left}px` });
setArrowPosition(`${arrowLeft}px 0px`);

// Focus on the first element when opened
const focusableElements = getFocusableElements();
if (focusableElements && focusableElements.length > 0) {
focusableElements[0]?.focus();
}
}
}, [open]);

if (!open) {
return null;
}
// Method to get focusable elements inside the component
const getFocusableElements = (): NodeListOf<HTMLElement> | null => {
return ref.current?.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
) as NodeListOf<HTMLElement>;
};


// Focus trapping logic
const handleTabKey = (e: KeyboardEvent) => {
if (e.key === "Tab") {
const focusableElements = getFocusableElements();
if (!focusableElements) return;

const firstFocusableElement = focusableElements[0] as HTMLElement;
const lastFocusableElement = focusableElements[focusableElements.length - 1] as HTMLElement;

if (e.shiftKey) {
// If Shift + Tab is pressed and the focus is on the first element, go to the last
if (document.activeElement === firstFocusableElement) {
e.preventDefault();
lastFocusableElement.focus();
}
} else {
// If Tab is pressed and the focus is on the last element, go to the first
if (document.activeElement === lastFocusableElement) {
e.preventDefault();
firstFocusableElement.focus();
}
}
}
};

useEffect(() => {
if (open) {
document.addEventListener("keydown", handleTabKey);
}

return () => {
document.removeEventListener("keydown", handleTabKey);
};
}, [open]);

if (!open) return null;

function getCanvasDimensions(canvas: Canvas): Size | null {
// externalResource may not have loaded yet
Expand Down
33 changes: 32 additions & 1 deletion src/content-handlers/iiif/modules/uv-shared-module/Dialogue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ export class Dialogue<

create(): void {
this.setConfig("dialogue");

super.create();

// events.
Expand Down Expand Up @@ -168,6 +167,9 @@ export class Dialogue<
}
}, 1);

// Add keydown event listener to trap focus within the dialog
this.$element.on("keydown", (e: JQuery.Event) => this.handleKeydown(e));

this.extensionHost.publish(IIIFEvents.SHOW_OVERLAY);

if (this.isUnopened) {
Expand All @@ -186,6 +188,9 @@ export class Dialogue<
this.$element.hide();
this.isActive = false;

// Remove the keydown event listener
this.$element.off("keydown");

this.extensionHost.publish(this.closeCommand);
this.extensionHost.publish(IIIFEvents.HIDE_OVERLAY);
}
Expand All @@ -198,4 +203,30 @@ export class Dialogue<
left: Math.floor(this.extension.width() / 2 - this.$element.width() / 2),
});
}

private handleKeydown(event: JQuery.Event): void {
if (event.key === "Tab") {
const focusableSelectors =
'a[href], button, textarea, input[type="text"], input[type="radio"], input[type="checkbox"], select, [tabindex="0"]';
const focusableElements = this.$element.find(focusableSelectors).filter(':visible');

const firstElement = focusableElements.first()[0];
const lastElement = focusableElements.last()[0];
const activeElement = document.activeElement;

if (event.shiftKey) {
// Shift + Tab (backwards)
if (activeElement === firstElement) {
event.preventDefault();
lastElement.focus();
}
} else {
// Tab (forwards)
if (activeElement === lastElement) {
event.preventDefault();
firstElement.focus();
}
}
}
}
}

0 comments on commit b024fbf

Please sign in to comment.