diff --git a/src/internal/modal.ts b/src/internal/modal.ts index fe463ad23..9c507d902 100644 --- a/src/internal/modal.ts +++ b/src/internal/modal.ts @@ -87,7 +87,7 @@ export default class Modal { if (currentFocusIndex === -1) { this.currentFocus = tabbableElements[0]; - this.currentFocus.focus({ preventScroll: true }); + this.currentFocus?.focus({ preventScroll: true }); return; } diff --git a/src/internal/tabbable.ts b/src/internal/tabbable.ts index fca4ec5c1..d05164127 100644 --- a/src/internal/tabbable.ts +++ b/src/internal/tabbable.ts @@ -1,4 +1,15 @@ -import { offsetParent } from 'composed-offset-position'; +// +// This doesn't technically check visibility, it checks if the element has been rendered and can maybe possibly be tabbed +// to. This is a workaround for shadow roots not having an `offsetParent`. +// +// See https://stackoverflow.com/questions/19669786/check-if-element-is-visible-in-dom +// +// Previously, we used https://www.npmjs.com/package/composed-offset-position, but recursing up an entire node tree took +// up a lot of CPU cycles and made focus traps unusable in Chrome / Edge. +// +function isTakingUpSpace(elem: HTMLElement): boolean { + return Boolean(elem.offsetParent || elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length); +} /** Determines if the specified element is tabbable using heuristics inspired by https://github.com/focus-trap/tabbable */ function isTabbable(el: HTMLElement) { @@ -20,8 +31,7 @@ function isTabbable(el: HTMLElement) { } // Elements that are hidden have no offsetParent and are not tabbable - // offsetParent() is added because otherwise it misses elements in Safari - if (el.offsetParent === null && offsetParent(el) === null) { + if (!isTakingUpSpace(el)) { return false; }