diff --git a/app/components/primer/alpha/dialog.html.erb b/app/components/primer/alpha/dialog.html.erb
index 8ebded10ce..67414761c6 100644
--- a/app/components/primer/alpha/dialog.html.erb
+++ b/app/components/primer/alpha/dialog.html.erb
@@ -1,5 +1,5 @@
<%= show_button %>
-
+
<%= render Primer::BaseComponent.new(**@system_arguments) do %>
<%= header %>
<% if content.present? %>
@@ -9,4 +9,4 @@
<%= footer %>
<% end %>
<% end %>
-
+
diff --git a/app/components/primer/alpha/dialog.rb b/app/components/primer/alpha/dialog.rb
index e5efdc842f..bae650b7a9 100644
--- a/app/components/primer/alpha/dialog.rb
+++ b/app/components/primer/alpha/dialog.rb
@@ -125,8 +125,7 @@ def initialize(
@position_narrow = position_narrow
@visually_hide_title = visually_hide_title
- @system_arguments[:tag] = "modal-dialog"
- @system_arguments[:role] = "dialog"
+ @system_arguments[:tag] = "dialog"
@system_arguments[:id] = @id
@system_arguments[:aria] = { modal: true }
@system_arguments[:aria] = merge_aria(
diff --git a/app/components/primer/alpha/modal_dialog.ts b/app/components/primer/alpha/modal_dialog.ts
deleted file mode 100644
index 840becf0ec..0000000000
--- a/app/components/primer/alpha/modal_dialog.ts
+++ /dev/null
@@ -1,199 +0,0 @@
-import {focusTrap} from '@primer/behaviors'
-import {getFocusableChild} from '@primer/behaviors/utils'
-
-function focusIfNeeded(elem: HTMLElement | undefined | null) {
- if (document.activeElement !== elem) {
- elem?.focus()
- }
-}
-
-const overlayStack: ModalDialogElement[] = []
-
-function clickHandler(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) {
- event.stopPropagation()
- const dialog = document.getElementById(dialogId)
- if (dialog instanceof ModalDialogElement) {
- dialog.openButton = button
- dialog.show()
- // 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()
- return
- }
- }
-
- if (!overlayStack.length) return
-
- dialogId = button.getAttribute('data-close-dialog-id') || button.getAttribute('data-submit-dialog-id')
- if (dialogId) {
- const dialog = document.getElementById(dialogId)
- if (dialog instanceof ModalDialogElement) {
- const dialogIndex = overlayStack.findIndex(ele => ele.id === dialogId)
- overlayStack.splice(dialogIndex, 1)
- dialog.close(button.hasAttribute('data-submit-dialog-id'))
- }
- }
-}
-
-function keydownHandler(event: Event) {
- if (
- !(event instanceof KeyboardEvent) ||
- event.type !== 'keydown' ||
- event.key !== 'Enter' ||
- event.ctrlKey ||
- event.altKey ||
- event.metaKey ||
- event.shiftKey
- )
- return
-
- clickHandler(event)
-}
-
-function mousedownHandler(event: Event) {
- const target = event.target as HTMLElement
- if (target?.closest('button')) return
-
- // Find the top level dialog that is open.
- const topLevelDialog = overlayStack[overlayStack.length - 1]
- if (!topLevelDialog) return
-
- // Check if the mousedown happened outside the boundary of the top level dialog
- const mouseDownOutsideDialog = !target.closest(`#${topLevelDialog.getAttribute('id')}`)
-
- // Only close dialog if it's a click outside the dialog and the dialog has a button?
- if (mouseDownOutsideDialog) {
- target.ownerDocument.addEventListener(
- 'mouseup',
- (upEvent: Event) => {
- if (upEvent.target === target) {
- overlayStack.pop()
- topLevelDialog.close()
- }
- },
- {once: true}
- )
- }
-}
-
-export class ModalDialogElement extends HTMLElement {
- //TODO: Do we remove the abortController from focusTrap?
- #focusAbortController = new AbortController()
- openButton: HTMLButtonElement | null
-
- get open() {
- return this.hasAttribute('open')
- }
- set open(value: boolean) {
- if (value) {
- if (this.open) return
- this.setAttribute('open', '')
- this.setAttribute('aria-disabled', 'false')
- document.body.style.paddingRight = `${window.innerWidth - document.body.clientWidth}px`
- document.body.style.overflow = 'hidden'
- this.#overlayBackdrop?.classList.remove('Overlay--hidden')
- if (this.#focusAbortController.signal.aborted) {
- this.#focusAbortController = new AbortController()
- }
- focusTrap(this, this.querySelector('[autofocus]') as HTMLElement, this.#focusAbortController.signal)
- overlayStack.push(this)
- } else {
- if (!this.open) return
- this.removeAttribute('open')
- this.setAttribute('aria-disabled', 'true')
- this.#overlayBackdrop?.classList.add('Overlay--hidden')
- document.body.style.paddingRight = '0'
- document.body.style.overflow = 'initial'
- this.#focusAbortController.abort()
- // if #openButton is a child of a menu, we need to focus a suitable child of the menu
- // element since it is expected for the menu to close on click
- const menu = this.openButton?.closest('details') || this.openButton?.closest('action-menu')
- if (menu) {
- focusIfNeeded(getFocusableChild(menu))
- } else {
- focusIfNeeded(this.openButton)
- }
- this.openButton = null
- }
- }
-
- get #overlayBackdrop(): HTMLElement | null {
- if (this.parentElement?.hasAttribute('data-modal-dialog-overlay')) {
- return this.parentElement
- }
-
- return null
- }
-
- get showButtons(): NodeList {
- // Dialogs may also be opened from any arbitrary button with a matching show-dialog-id data attribute
- return document.querySelectorAll(`button[data-show-dialog-id='${this.id}']`)
- }
-
- connectedCallback(): void {
- if (!this.hasAttribute('role')) this.setAttribute('role', 'dialog')
-
- document.addEventListener('click', clickHandler)
- document.addEventListener('keydown', keydownHandler)
- document.addEventListener('mousedown', mousedownHandler)
-
- this.addEventListener('keydown', e => this.#keydown(e))
- }
-
- show() {
- this.open = true
- }
-
- close(closedNotCancelled = false) {
- if (this.open === false) return
- const eventType = closedNotCancelled ? 'close' : 'cancel'
- const dialogEvent = new Event(eventType)
- this.dispatchEvent(dialogEvent)
- this.open = false
- }
-
- #keydown(event: Event) {
- if (!(event instanceof KeyboardEvent)) return
- if (event.isComposing) return
- if (!this.open) return
-
- switch (event.key) {
- case 'Escape':
- this.close()
- event.preventDefault()
- event.stopPropagation()
- break
- case 'Enter': {
- const target = event.target as HTMLElement
-
- if (target.getAttribute('data-close-dialog-id') === this.id) {
- event.stopPropagation()
- }
- break
- }
- }
- }
-}
-
-declare global {
- interface Window {
- ModalDialogElement: typeof ModalDialogElement
- }
- interface HTMLElementTagNameMap {
- 'modal-dialog': ModalDialogElement
- }
-}
-
-if (!window.customElements.get('modal-dialog')) {
- window.ModalDialogElement = ModalDialogElement
- window.customElements.define('modal-dialog', ModalDialogElement)
-}
diff --git a/app/components/primer/alpha/overlay.pcss b/app/components/primer/alpha/overlay.pcss
index ec3afead27..d5be828621 100644
--- a/app/components/primer/alpha/overlay.pcss
+++ b/app/components/primer/alpha/overlay.pcss
@@ -9,8 +9,13 @@ anchored-position[popover] {
.Overlay {
display: flex;
+ border-width: 0;
}
-anchored-position.not-anchored::backdrop {
+anchored-position.not-anchored::backdrop, dialog::backdrop {
background-color: var(--overlay-backdrop-bgColor, var(--color-neutral-muted));
}
+
+dialog.Overlay:not([open]) {
+ display: none;
+}
diff --git a/app/components/primer/dialog_helpers.ts b/app/components/primer/dialog_helpers.ts
new file mode 100644
index 0000000000..463a1063c5
--- /dev/null
+++ b/app/components/primer/dialog_helpers.ts
@@ -0,0 +1,77 @@
+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) {
+ event.stopPropagation()
+ const dialog = document.getElementById(dialogId)
+ if (dialog instanceof HTMLDialogElement) {
+ 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()
+ }
+ }
+
+ 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()
+ }
+ }
+}
+
+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)
+ document.addEventListener('click', this)
+ }
+
+ disconnectedCallback() {
+ this.#abortController?.abort()
+ }
+
+ handleEvent(event) {
+ const target = event.target as HTMLElement
+ const dialog = this.dialog
+ if (!dialog.open) return
+ if (target?.closest('dialog') !== dialog) 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.ModalDialogElement = DialogHelperElement
+ window.customElements.define('dialog-helper', DialogHelperElement)
+}
diff --git a/app/components/primer/primer.ts b/app/components/primer/primer.ts
index c234aac2d9..e6a9d4cda2 100644
--- a/app/components/primer/primer.ts
+++ b/app/components/primer/primer.ts
@@ -2,9 +2,9 @@ import '@github/include-fragment-element'
import './alpha/action_bar_element'
import './alpha/dropdown'
import './anchored_position'
+import './dialog_helpers'
import './focus_group'
import './alpha/image_crop'
-import './alpha/modal_dialog'
import './beta/nav_list'
import './alpha/segmented_control'
import './alpha/toggle_switch'