-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathfocus-trap.client.ts
105 lines (86 loc) · 2.79 KB
/
focus-trap.client.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
// focus-trap courtesy of Jacob Ebey from the following repo
// https://github.com/jacob-ebey/remix-dashboard-template
// Note that there is no license in this repo and so the origin
// and license for this code should be determined before use.
import { type DOMAttributes } from 'react'
const FOCUSABLE_ELEMENTS_SELECTOR =
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
class FocusTrap extends HTMLElement {
static get observedAttributes() {
return ['trapped']
}
private _returnTo: HTMLElement | null = null
constructor() {
super()
this._getFocusableElements = this._getFocusableElements.bind(this)
this._onKeyDown = this._onKeyDown.bind(this)
}
attributeChangedCallback(name: string, oldValue: string, newValue: string) {
if (name == 'trapped') {
if (newValue) {
this._returnTo = document.activeElement as HTMLElement
setTimeout(() => {
this._getFocusableElements().firstFocusableElement.focus()
}, 1)
} else if (this._returnTo) {
setTimeout(() => {
;(this._returnTo as HTMLElement).focus()
}, 1)
}
return
}
}
connectedCallback() {
if (!this.isConnected) return
this.addEventListener('keydown', this._onKeyDown)
}
disconnectedCallback() {
this.removeEventListener('keydown', this._onKeyDown)
if (this._returnTo) {
setTimeout(() => {
;(this._returnTo as HTMLElement).focus()
}, 1)
}
}
_onKeyDown(event: KeyboardEvent) {
if (this.getAttribute('trapped') != 'true') return
const isTabPressed = event.key === 'Tab'
if (!isTabPressed) return
const { firstFocusableElement, lastFocusableElement } = this._getFocusableElements()
if (event.shiftKey) {
if (document.activeElement === firstFocusableElement) {
lastFocusableElement.focus()
event.preventDefault()
}
} else {
if (document.activeElement === lastFocusableElement) {
firstFocusableElement.focus()
event.preventDefault()
}
}
}
_getFocusableElements() {
const focusableElements = this.querySelectorAll(FOCUSABLE_ELEMENTS_SELECTOR)
const firstFocusableElement = focusableElements[0] as HTMLElement
const lastFocusableElement = focusableElements[focusableElements.length - 1] as HTMLElement
return {
firstFocusableElement,
lastFocusableElement,
}
}
}
let registered = false
export function registerFocusTrap() {
if (!registered) {
registered = true
customElements.define('focus-trap', FocusTrap)
}
}
type CustomElement<T> = Partial<T & DOMAttributes<T> & { children: any; class: string; ref?: any }>
declare global {
namespace JSX {
interface IntrinsicElements {
'focus-trap': CustomElement<FocusTrap & { trapped?: 'true' }>
}
}
}