forked from primer/view_components
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathdialog_helper.ts
More file actions
169 lines (151 loc) · 6.19 KB
/
Copy pathdialog_helper.ts
File metadata and controls
169 lines (151 loc) · 6.19 KB
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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
function setScrollGutter(doc: Document) {
if (doc.body.style.getPropertyValue('--dialog-scrollgutter')) return
doc.body.style.setProperty('--dialog-scrollgutter', `${window.innerWidth - doc.body.clientWidth}px`)
}
function updateBodyModalClasses(doc: Document) {
doc.body.classList.toggle('has-modal', Boolean(doc.querySelector('dialog[open]')))
doc.body.classList.toggle(
'has-modal-disable-scroll',
Boolean(doc.querySelector('dialog[open].Overlay--disableScroll')),
)
}
function dialogInvokerButtonHandler(event: Event) {
const target = event.target as HTMLElement
const button = target?.closest('button')
if (!button || button.hasAttribute('disabled') || button.getAttribute('aria-disabled') === 'true') return
// If the user is clicking a valid dialog trigger
let dialogId = button?.getAttribute('data-show-dialog-id')
if (dialogId) {
const dialog = document.getElementById(dialogId)
if (dialog instanceof HTMLDialogElement) {
setScrollGutter(dialog.ownerDocument)
dialog.showModal()
// A buttons default behaviour in some browsers it to send a pointer event
// If the behaviour is allowed through the dialog will be shown but then
// quickly hidden- as if it were never shown. This prevents that.
event.preventDefault()
// When a <dialog> is opened with showModal() from inside a popover with popover="auto",
// there are two related issues:
//
// 1. In older browsers (e.g. Chrome 122), the "hide all popovers" algorithm runs when a
// top layer element opens, closing any ancestor popover. We must re-open those popovers.
// See https://github.com/whatwg/html/issues/9998,
// fixed by https://github.com/whatwg/html/pull/10116.
//
// 2. In newer browsers where the popover stays open, the popover="auto" behavior still
// interferes with the native <dialog> focus trap, causing focus to escape the dialog
// when tabbing past the last focusable element. Converting the popover to "manual"
// prevents this interference.
//
// In both cases, the fix is the same: convert ancestor auto popovers to manual.
let node: HTMLElement | null | undefined = button
let fixed = false
while (node) {
node = node.parentElement?.closest('[popover]')
if (node && node.popover === 'auto') {
node.classList.add('dialog-inside-popover-fix')
node.popover = 'manual'
// Changing popover from "auto" to "manual" closes the popover,
// so we need to re-show it regardless of whether it was previously open.
node.showPopover()
fixed = true
}
}
if (fixed) {
// We need to re-open the dialog as modal, and also ensure no close event listeners
// are trying to act on the close
/* eslint-disable-next-line no-restricted-syntax */
dialog.addEventListener('close', e => e.stopImmediatePropagation(), {once: true})
dialog.close()
dialog.showModal()
dialog.addEventListener(
'close',
() => {
for (const el of dialog.ownerDocument.querySelectorAll<HTMLElement>('.dialog-inside-popover-fix')) {
if (el.contains(dialog)) {
el.classList.remove('dialog-inside-popover-fix')
el.popover = 'auto'
el.showPopover()
}
}
},
{once: true},
)
}
updateBodyModalClasses(dialog.ownerDocument)
}
}
dialogId = button.getAttribute('data-close-dialog-id') || button.getAttribute('data-submit-dialog-id')
if (dialogId) {
const dialog = document.getElementById(dialogId)
if (dialog instanceof HTMLDialogElement && dialog.open) {
dialog.close()
updateBodyModalClasses(dialog.ownerDocument)
}
}
}
export class DialogHelperElement extends HTMLElement {
get dialog() {
return this.querySelector('dialog')
}
#abortController: AbortController | null = null
connectedCallback() {
const {signal} = (this.#abortController = new AbortController())
document.addEventListener('click', dialogInvokerButtonHandler, true)
document.addEventListener('click', this, {signal})
this.dialog?.addEventListener('close', () => updateBodyModalClasses(this.dialog!.ownerDocument), {signal})
new MutationObserver(records => {
for (const record of records) {
if (record.target === this.dialog) {
this.#handleDialogOpenAttribute()
}
}
}).observe(this, {subtree: true, attributeFilter: ['open']})
this.#handleDialogOpenAttribute()
}
disconnectedCallback() {
this.#abortController?.abort()
}
#handleDialogOpenAttribute() {
if (!this.dialog) return
const {ownerDocument} = this.dialog
// We don't want to show the Dialog component as non-modal
if (this.dialog.matches('[open]:not(:modal)')) {
// eslint-disable-next-line no-restricted-syntax
this.dialog.addEventListener('close', e => e.stopImmediatePropagation(), {once: true})
this.dialog.close()
setScrollGutter(ownerDocument)
this.dialog.showModal()
}
updateBodyModalClasses(ownerDocument)
}
handleEvent(event: MouseEvent) {
const target = event.target as HTMLElement
const dialog = this.dialog
// The click target _must_ be the dialog element itself, and not elements underneath or inside.
if (target !== dialog || !dialog?.open) return
// If the dialog contains a form, do not close the dialog when clicking outside of the dialog
if (dialog.querySelector('form')) return
const rect = dialog.getBoundingClientRect()
const clickWasInsideDialog =
rect.top <= event.clientY &&
event.clientY <= rect.top + rect.height &&
rect.left <= event.clientX &&
event.clientX <= rect.left + rect.width
if (!clickWasInsideDialog) {
dialog.close()
}
}
}
declare global {
interface Window {
DialogHelperElement: typeof DialogHelperElement
}
interface HTMLElementTagNameMap {
'dialog-helper': DialogHelperElement
}
}
if (!window.customElements.get('dialog-helper')) {
window.DialogHelperElement = DialogHelperElement
window.customElements.define('dialog-helper', DialogHelperElement)
}