-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
Allow Modals to optionally ignore the Close
button when determining where to place focus
#54296
Changes from 25 commits
075e405
4e13c67
79d1ad1
ac8c8d4
12d1e33
3d51a34
ae180f3
784ea80
c23d284
fe83a2e
559cc79
e83fa17
23234d6
22e1d8c
cbbed8d
fe0f2f1
ad5ec44
7844085
254d71e
65d39bb
bf58309
9fec0cf
e56f4a3
f52d193
47f461e
3cf68b5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -39,6 +39,34 @@ import type { ModalProps } from './types'; | |
// Used to count the number of open modals. | ||
let openModalCount = 0; | ||
|
||
/** | ||
* When `firstElement` is passed to `focusOnMount`, this function is optimized to | ||
* avoid focusing on the `Close` button (or other "header" elements of the Modal | ||
* and instead focus within the Modal's contents. | ||
* However, if no tabbable elements are found within the Modal's contents, the | ||
* first tabbable element (likely the `Close` button) will be focused instead. | ||
* This ensures that at least one element is focused whilst still optimizing | ||
* for the best a11y experience. | ||
* | ||
* See: https://github.com/WordPress/gutenberg/issues/54106. | ||
* @param tabbables Element[] an array of tabbable elements. | ||
* @return Element the first tabbable element in the Modal contents (or any tabbable element if none are found in content). | ||
*/ | ||
function getFirstTabbableElement( tabbables: Element[] ) { | ||
// Attempt to locate tabbable outside of the header portion of the Modal. | ||
const firstContentTabbable = tabbables.find( ( tabbable ) => { | ||
return tabbable.closest( '.components-modal__header' ) === null; | ||
} ); | ||
|
||
if ( firstContentTabbable ) { | ||
return firstContentTabbable; | ||
} | ||
|
||
// Fallback to the first tabbable element anywhere within the Modal. | ||
// Likely the `Close` button. | ||
return tabbables[ 0 ]; | ||
getdave marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
function UnforwardedModal( | ||
props: ModalProps, | ||
forwardedRef: ForwardedRef< HTMLDivElement > | ||
|
@@ -75,7 +103,13 @@ function UnforwardedModal( | |
const headingId = title | ||
? `components-modal-header-${ instanceId }` | ||
: aria.labelledby; | ||
const focusOnMountRef = useFocusOnMount( focusOnMount ); | ||
|
||
// If focusOnMount is `firstElement`, Modals should ignore the `Close` button which is the first focusable element. | ||
// Remap `true` to select the next focusable element instead. | ||
const focusOnMountRef = useFocusOnMount( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Forgive me if it's already mentioned above but it's a long PR and I'm not on my laptop 😅 . Could we instead just assign the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That would also change the focus behaviour when the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I mean only change the ref callback to be assigned to the children container div if and only if There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I wouldn't know off the top of my head, but I guess we could try the approach keeping the current set of unit tests and see how it goes. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would this give us the flexibility to find the first focusable node outside of the content wrapper if there's nothing appropriate within it? Feels like we still need the "global" context, and moving the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. True! FWIW, I would prefer to add a new type like That said, I don't really have a strong preference here. I'm just sharing a suggestion, and I'm okay with whatever solution we end up with! ❤️ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
That could also be an option, I wouldn't be opposed to that. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using I knew this wouldn't be easy 😅 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You all are doing a great job! Happy to help in any way when I have the capacity! 💪 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @kevin940726 Alternative now available in #54590 |
||
focusOnMount === 'firstElement' ? getFirstTabbableElement : focusOnMount | ||
); | ||
Comment on lines
+107
to
+109
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note with this change consumers of |
||
|
||
const constrainedTabbingRef = useConstrainedTabbing(); | ||
const focusReturnRef = useFocusReturn(); | ||
const focusOutsideProps = useFocusOutside( onRequestClose ); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm actually not sure I would call this a breaking change, maybe more of an enhancement?