@tamagui/react-native-web-lite
Version:
React Native for Web
164 lines (142 loc) • 4.72 kB
JavaScript
/**
* Copyright (c) Nicolas Gallagher.
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*
*/
import * as React from 'react'
import { StyleSheet, UIManager, canUseDOM } from '@tamagui/react-native-web-internals'
import createElement from '../createElement/index'
import View from '../View'
/**
* This Component is used to "wrap" the modal we're opening
* so that changing focus via tab will never leave the document.
*
* This allows us to properly trap the focus within a modal
* even if the modal is at the start or end of a document.
*/
var FocusBracket = () => {
return createElement('div', {
accessibilityRole: 'none',
tabIndex: 0,
style: styles.focusBracket,
})
}
function attemptFocus(element) {
if (!canUseDOM) {
return false
}
try {
element.focus()
} catch (e) {
// Do nothing
}
return document.activeElement === element
}
function focusFirstDescendant(element) {
for (var i = 0; i < element.childNodes.length; i++) {
var child = element.childNodes[i]
if (attemptFocus(child) || focusFirstDescendant(child)) {
return true
}
}
return false
}
function focusLastDescendant(element) {
for (var i = element.childNodes.length - 1; i >= 0; i--) {
var child = element.childNodes[i]
if (attemptFocus(child) || focusLastDescendant(child)) {
return true
}
}
return false
}
var ModalFocusTrap = (_ref) => {
var active = _ref.active,
children = _ref.children
var trapElementRef = React.useRef()
var focusRef = React.useRef({
trapFocusInProgress: false,
lastFocusedElement: null,
})
React.useEffect(() => {
if (canUseDOM) {
var trapFocus = () => {
// We should not trap focus if:
// - The modal hasn't fully initialized with an HTMLElement ref
// - Focus is already in the process of being trapped (e.g., we're refocusing)
// - isTrapActive prop being falsey tells us to do nothing
if (
trapElementRef.current == null ||
focusRef.current.trapFocusInProgress ||
!active
) {
return
}
try {
focusRef.current.trapFocusInProgress = true
if (
document.activeElement instanceof Node &&
!trapElementRef.current.contains(document.activeElement)
) {
// To handle keyboard focusing we can make an assumption here.
// If you're tabbing through the focusable elements, the previously
// active element will either be the first or the last.
// If the previously selected element is the "first" descendant
// and we're leaving it - this means that we should be looping
// around to the other side of the modal.
var hasFocused = focusFirstDescendant(trapElementRef.current)
if (focusRef.current.lastFocusedElement === document.activeElement) {
hasFocused = focusLastDescendant(trapElementRef.current)
} // If we couldn't focus a new element then we need to focus onto the trap target
if (!hasFocused && trapElementRef.current != null && document.activeElement) {
UIManager.focus(trapElementRef.current)
}
}
} finally {
focusRef.current.trapFocusInProgress = false
}
focusRef.current.lastFocusedElement = document.activeElement
} // Call the trapFocus callback at least once when this modal has been activated.
trapFocus()
document.addEventListener('focus', trapFocus, true)
return () => document.removeEventListener('focus', trapFocus, true)
}
}, [active]) // To be fully compliant with WCAG we need to refocus element that triggered opening modal
// after closing it
React.useEffect(function () {
if (canUseDOM) {
var lastFocusedElementOutsideTrap = document.activeElement
return function () {
if (
lastFocusedElementOutsideTrap &&
document.contains(lastFocusedElementOutsideTrap)
) {
UIManager.focus(lastFocusedElementOutsideTrap)
}
}
}
}, [])
return /*#__PURE__*/ React.createElement(
React.Fragment,
null,
/*#__PURE__*/ React.createElement(FocusBracket, null),
/*#__PURE__*/ React.createElement(
View,
{
ref: trapElementRef,
},
children
),
/*#__PURE__*/ React.createElement(FocusBracket, null)
)
}
export default ModalFocusTrap
var styles = StyleSheet.create({
focusBracket: {
outlineStyle: 'none',
},
})