Skip to content

Commit 0e4adf0

Browse files
committed
fix(ui-modal,ui-dom-utils): fix Modal focus trap broken when it has a scrollbar
The issue was that its not recognized by findFocusable as a focusable element. Recently browsers have made scrollable elements focusable, but there is no sure way to know that a scrollbar is displayed, an easy workaround is to add the tabIndex=0 prop. Note that this breaks VoiceOver a bit, its not able to navigate the inside of a scrollable container with SR focus INSTUI-4625
1 parent 78ef567 commit 0e4adf0

File tree

3 files changed

+27
-26
lines changed

3 files changed

+27
-26
lines changed

packages/ui-dom-utils/src/findFocusable.ts

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,10 @@
3333
*
3434
* http://api.jqueryui.com/category/ui-core/
3535
**/
36-
36+
// TODO replace this with https://github.com/focus-trap/tabbable
37+
// or even better use the native <dialog> component.
38+
// tabbable has issues with scrollable containers, e.g.
39+
// https://github.com/focus-trap/tabbable/issues/167
3740
import { getComputedStyle, findDOMNode } from './'
3841
import type { UIElement } from '@instructure/shared-types'
3942

@@ -50,24 +53,27 @@ const focusableSelector = [
5053
'[contenteditable="true"]'
5154
].join(',')
5255

56+
function hasQuerySelectorAll(el?: UIElement): el is Element | Document {
57+
if (
58+
!el ||
59+
typeof (el as Element | Document).querySelectorAll !== 'function'
60+
) {
61+
return false
62+
}
63+
return true
64+
}
65+
5366
function findFocusable(
5467
el?: UIElement,
5568
filter?: (el: Element) => boolean,
5669
shouldSearchRootNode?: boolean
5770
) {
5871
const element = el && findDOMNode(el)
5972

60-
if (
61-
!element ||
62-
typeof (element as Element | Document).querySelectorAll !== 'function'
63-
) {
73+
if (!hasQuerySelectorAll(element)) {
6474
return []
6575
}
66-
67-
let matches = Array.from(
68-
(element as Element | Document).querySelectorAll(focusableSelector)
69-
)
70-
76+
let matches = Array.from(element.querySelectorAll(focusableSelector))
7177
if (shouldSearchRootNode && (element as Element).matches(focusableSelector)) {
7278
matches = [...matches, element as Element]
7379
}

packages/ui-modal/src/Modal/ModalBody/index.tsx

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ import generateStyle from './styles'
3434
import generateComponentTheme from './theme'
3535

3636
import { propTypes, allowedProps } from './props'
37-
import type { ModalBodyProps, ModalBodyState } from './props'
37+
import type { ModalBodyProps } from './props'
3838

3939
/**
4040
---
@@ -44,7 +44,7 @@ id: Modal.Body
4444
**/
4545
@withStyle(generateStyle, generateComponentTheme)
4646
@testable()
47-
class ModalBody extends Component<ModalBodyProps, ModalBodyState> {
47+
class ModalBody extends Component<ModalBodyProps> {
4848
static readonly componentId = 'Modal.Body'
4949

5050
static propTypes = propTypes
@@ -69,10 +69,6 @@ class ModalBody extends Component<ModalBodyProps, ModalBodyState> {
6969

7070
constructor(props: ModalBodyProps) {
7171
super(props)
72-
73-
this.state = {
74-
isFirefox: false
75-
}
7672
}
7773

7874
componentDidMount() {
@@ -117,11 +113,14 @@ class ModalBody extends Component<ModalBodyProps, ModalBodyState> {
117113
as={as}
118114
css={this.props.styles?.modalBody}
119115
padding={padding}
120-
// We have to make an exception in Firefox, because it makes
121-
// the container focusable when it is scrollable.
122-
// This is a feature, not a bug, but it prevents VoiceOver
123-
// to correctly focus inside the body in other browsers.
124-
{...(this.state.isFirefox && { tabIndex: -1 })}
116+
// Setting this to 0 is necessary for findFocusable to find this DOM
117+
// element.
118+
// Note that VoiceOver does not read the contents properly
119+
// if there is a scrollbar, and it has no kb just SR focus.
120+
// To prevent this we could set tabIndex to -1
121+
// but this would make the scrollbar non-focusable, so the modal cannot
122+
// be scrolled with keyboard.
123+
tabIndex={0}
125124
>
126125
{children}
127126
</View>

packages/ui-modal/src/Modal/ModalBody/props.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,6 @@ type ModalBodyProps = ModalBodyOwnProps &
5656

5757
type ModalBodyStyle = ComponentStyle<'modalBody'>
5858

59-
type ModalBodyState = {
60-
isFirefox: boolean
61-
}
62-
6359
const propTypes: PropValidators<PropKeys> = {
6460
children: PropTypes.node,
6561
padding: PropTypes.string,
@@ -81,5 +77,5 @@ const allowedProps: AllowedPropKeys = [
8177
'overflow'
8278
]
8379

84-
export type { ModalBodyProps, ModalBodyState, ModalBodyStyle }
80+
export type { ModalBodyProps, ModalBodyStyle }
8581
export { propTypes, allowedProps }

0 commit comments

Comments
 (0)