From 94c505a30548900704cf223d2a43aad486db53ad Mon Sep 17 00:00:00 2001 From: jquense Date: Mon, 24 Apr 2023 10:40:40 -0400 Subject: [PATCH 1/2] fix: prevent scrolling when focus trap cycles back into the modal also fixes a small issue with the `alertdialog` role --- src/Modal.tsx | 21 ++++++++++++++++----- src/ModalManager.ts | 26 ++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/src/Modal.tsx b/src/Modal.tsx index 1555c04..00df7ec 100644 --- a/src/Modal.tsx +++ b/src/Modal.tsx @@ -4,6 +4,10 @@ import activeElement from 'dom-helpers/activeElement'; import contains from 'dom-helpers/contains'; import canUseDOM from 'dom-helpers/canUseDOM'; import listen from 'dom-helpers/listen'; + +import getScrollTop from 'dom-helpers/scrollTop'; +import getScrollLeft from 'dom-helpers/scrollLeft'; + import { useState, useRef, @@ -275,6 +279,7 @@ const Modal: React.ForwardRefExoticComponent< const prevShow = usePrevious(show); const [exited, setExited] = useState(!show); const lastFocusRef = useRef(null); + const scrollPositionRef = useRef(null); useImperativeHandle(ref, () => modal, [modal]); @@ -290,6 +295,11 @@ const Modal: React.ForwardRefExoticComponent< const handleShow = useEventCallback(() => { modal.add(); + scrollPositionRef.current = { + x: getScrollLeft(container!), + y: getScrollTop(container!), + }; + removeKeydownListenerRef.current = listen( document as any, 'keydown', @@ -299,9 +309,7 @@ const Modal: React.ForwardRefExoticComponent< removeFocusListenerRef.current = listen( document as any, 'focus', - // the timeout is necessary b/c this will run before the new modal is mounted - // and so steals focus from it - () => setTimeout(handleEnforceFocus), + handleEnforceFocus, true, ); @@ -363,7 +371,7 @@ const Modal: React.ForwardRefExoticComponent< // -------------------------------- - const handleEnforceFocus = useEventCallback(() => { + const handleEnforceFocus = useEventCallback((event: FocusEvent) => { if (!enforceFocus || !isMounted() || !modal.isTopModal()) { return; } @@ -375,7 +383,9 @@ const Modal: React.ForwardRefExoticComponent< currentActiveElement && !contains(modal.dialog, currentActiveElement) ) { + event.preventDefault(); modal.dialog.focus(); + manager.maybeResetScrollPosition(); } }); @@ -417,7 +427,8 @@ const Modal: React.ForwardRefExoticComponent< role, ref: modal.setDialogRef, // apparently only works on the dialog role element - 'aria-modal': role === 'dialog' ? true : undefined, + 'aria-modal': + role === 'dialog' || role === 'alertdialog' ? true : undefined, ...rest, style, className, diff --git a/src/ModalManager.ts b/src/ModalManager.ts index fbab930..3b83819 100644 --- a/src/ModalManager.ts +++ b/src/ModalManager.ts @@ -1,4 +1,8 @@ import css from 'dom-helpers/css'; +import getSetScrollTop from 'dom-helpers/scrollTop'; +import getSetScrollLeft from 'dom-helpers/scrollLeft'; +import getScrollParent from 'dom-helpers/scrollParent'; +import isDocument from 'dom-helpers/isDocument'; import { dataAttr } from './DataKey'; import getBodyScrollbarWidth from './getScrollbarWidth'; @@ -51,6 +55,11 @@ class ModalManager { return getBodyScrollbarWidth(this.ownerDocument); } + protected getScollingElement() { + const element = getScrollParent(this.getElement()); + return isDocument(element) ? element.defaultView! : element; + } + getElement() { return (this.ownerDocument || document).body; } @@ -113,8 +122,12 @@ class ModalManager { return modalIdx; } + const scrollElement = this.getScollingElement() as Element; + this.state = { scrollBarWidth: this.getScrollbarWidth(), + scrollTop: getSetScrollTop(scrollElement), + scrollLeft: getSetScrollLeft(scrollElement), style: {}, }; @@ -125,6 +138,19 @@ class ModalManager { return modalIdx; } + maybeResetScrollPosition() { + const scrollElement = this.getScollingElement() as Element; + + const { scrollTop, scrollLeft } = this.state; + + if (getSetScrollTop(scrollElement) !== scrollTop) { + getSetScrollTop(scrollElement, scrollTop); + } + if (getSetScrollLeft(scrollElement) !== scrollLeft) { + getSetScrollLeft(scrollElement, scrollLeft); + } + } + remove(modal: ModalInstance) { const modalIdx = this.modals.indexOf(modal); From f2315fa43070796ef204302f3acd18ab394bb085 Mon Sep 17 00:00:00 2001 From: jquense Date: Mon, 24 Apr 2023 10:44:09 -0400 Subject: [PATCH 2/2] Update Modal.tsx --- src/Modal.tsx | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/Modal.tsx b/src/Modal.tsx index 00df7ec..5500003 100644 --- a/src/Modal.tsx +++ b/src/Modal.tsx @@ -5,9 +5,6 @@ import contains from 'dom-helpers/contains'; import canUseDOM from 'dom-helpers/canUseDOM'; import listen from 'dom-helpers/listen'; -import getScrollTop from 'dom-helpers/scrollTop'; -import getScrollLeft from 'dom-helpers/scrollLeft'; - import { useState, useRef, @@ -279,7 +276,6 @@ const Modal: React.ForwardRefExoticComponent< const prevShow = usePrevious(show); const [exited, setExited] = useState(!show); const lastFocusRef = useRef(null); - const scrollPositionRef = useRef(null); useImperativeHandle(ref, () => modal, [modal]); @@ -295,11 +291,6 @@ const Modal: React.ForwardRefExoticComponent< const handleShow = useEventCallback(() => { modal.add(); - scrollPositionRef.current = { - x: getScrollLeft(container!), - y: getScrollTop(container!), - }; - removeKeydownListenerRef.current = listen( document as any, 'keydown',