Skip to content
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

[Version 8] Handle Shadow DOM subtrees when checking for active element #458

Conversation

mxmason
Copy link
Contributor

@mxmason mxmason commented Jan 22, 2023

Summary

This PR adds further support for Shadow DOM and custom elements, following work in #397. Note: it depends on changes in #455, so that should merge first.

Without these changes, our trapTabKey function doesn't understand when the user's focus is in a shadow subtree, so the user could escape the modal when we don't want them to.

Why?

document.activeElement is not aware of any shadow roots in the document. We have to check for shadow roots on the active element, then get the active element within that root. See getDeepActiveElement().

Comment on lines +310 to +313
const firstFocusableChild = focusableChildren[0]
const lastFocusableChild = focusableChildren[focusableChildren.length - 1]

const activeElement = getDeepActiveElement()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 We don't need to look through our collection of focusableChildren to get an index; we can compare our activeElement to the first and last nodes to check if the user is at the beginning or end of the modal.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That’s a good point. I’m not sure this was ever a real performance issue but potentially this could be when there are hundreds or thousands of focusable elements. Checking just the first and last is nicer. Well done!

// Get the active element, accounting for Shadow DOM subtrees.
// Credit to Cory LaViska for this inmplementation
// @see: https://www.abeautifulsite.net/posts/finding-the-active-element-in-a-shadow-root/
function getDeepActiveElement(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function checks for shadow subtrees and returns the active element within, if it exists.

@@ -52,7 +52,7 @@ export default class A11yDialog {

// Keep a reference to the currently focused element to be able to restore
// it later
this.previouslyFocused = document.activeElement as HTMLElement
this.previouslyFocused = getDeepActiveElement() as HTMLElement
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 We probably want previouslyFocused to be shadow-aware, to account for possible custom elements in useerland.

@mxmason mxmason changed the title Handle Shadow DOM subtrees when checking for active element [Version 8] Handle Shadow DOM subtrees when checking for active element Jan 22, 2023
@KittyGiraudel KittyGiraudel deleted the branch KittyGiraudel:event-delegation-fix January 22, 2023 09:57
@KittyGiraudel
Copy link
Owner

Aaaand I made a mistake. Merging #455 caused the branch to be deleted. Could you reopen this? Sorryyyy…

@@ -52,7 +52,7 @@ export default class A11yDialog {

// Keep a reference to the currently focused element to be able to restore
// it later
this.previouslyFocused = document.activeElement as HTMLElement
this.previouslyFocused = getDeepActiveElement() as HTMLElement
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we call it getActiveElement()? The fact that it’s deep is an implementation detail and does not need to be mentioned in the name I think.

Comment on lines +310 to +313
const firstFocusableChild = focusableChildren[0]
const lastFocusableChild = focusableChildren[focusableChildren.length - 1]

const activeElement = getDeepActiveElement()
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That’s a good point. I’m not sure this was ever a real performance issue but potentially this could be when there are hundreds or thousands of focusable elements. Checking just the first and last is nicer. Well done!

document.activeElement as HTMLElement
)
const firstFocusableChild = focusableChildren[0]
const lastFocusableChild = focusableChildren[focusableChildren.length - 1]
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Side note, now that we no longer supper Internet Explorer, we could use .at(-1) here to shorten this line.

https://caniuse.com/mdn-javascript_builtins_array_at

event.preventDefault()
}
}

// Get the active element, accounting for Shadow DOM subtrees.
// Credit to Cory LaViska for this inmplementation
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

implementation*

// Get the active element, accounting for Shadow DOM subtrees.
// Credit to Cory LaViska for this inmplementation
// @see: https://www.abeautifulsite.net/posts/finding-the-active-element-in-a-shadow-root/
function getDeepActiveElement(
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice little function. Short and sweet, I like it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants