Skip to content

Instantly share code, notes, and snippets.

@tak-dcxi
Created April 30, 2024 15:06
Show Gist options
  • Save tak-dcxi/2d55345650399db7822626e53b93904a to your computer and use it in GitHub Desktop.
Save tak-dcxi/2d55345650399db7822626e53b93904a to your computer and use it in GitHub Desktop.
initializeModal
const backfaceFixed = (fixed: boolean): void => {
const scrollBarWidth = getScrollBarSize()
const scrollPosition = getScrollPosition(fixed)
document.body.style.borderInlineEnd = fixed ? `${scrollBarWidth}px solid transparent` : ''
applyStyles(scrollPosition, fixed)
if (!fixed) restorePosition(scrollPosition)
}
const isWritingModeVertical = (): boolean => {
const { writingMode } = window.getComputedStyle(document.scrollingElement!)
return writingMode.includes('vertical') || writingMode.includes('sideways')
}
const getScrollBarSize = (): number => {
const scrollBarXSize = window.innerHeight - document.body.clientHeight
const scrollBarYSize = window.innerWidth - document.body.clientWidth
return isWritingModeVertical() ? scrollBarXSize : scrollBarYSize
}
const getScrollPosition = (fixed: boolean): number => {
if (fixed) {
return isWritingModeVertical() ? document.scrollingElement!.scrollLeft : document.scrollingElement!.scrollTop
}
return parseInt(document.body.style.insetBlockStart || '0', 10)
}
type AllowedStyles = 'blockSize' | 'insetInlineStart' | 'position' | 'insetBlockStart' | 'inlineSize'
const applyStyles = (scrollPosition: number, apply: boolean): void => {
const styles: Partial<Record<AllowedStyles, string>> = {
blockSize: '100dvb',
insetInlineStart: '0',
position: 'fixed',
insetBlockStart: isWritingModeVertical() ? `${scrollPosition}px` : `${scrollPosition * -1}px`,
inlineSize: '100dvi',
}
Object.keys(styles).forEach((key) => {
const styleKey = key as AllowedStyles
document.body.style[styleKey] = apply ? styles[styleKey]! : ''
})
}
const restorePosition = (scrollPosition: number): void => {
const options: ScrollToOptions = {
behavior: 'instant',
[isWritingModeVertical() ? 'left' : 'top']: isWritingModeVertical() ? scrollPosition : scrollPosition * -1,
}
window.scrollTo(options)
}
export default backfaceFixed
import backfaceFixed from '@/scripts/utils/backfaceFixed'
const initializeModal = (modal: HTMLDialogElement): void => {
if (!modal) {
console.error('initializeModal: Modal element is not found.')
return
}
const openTriggers = document.querySelectorAll(`[data-modal-open="${modal.id}"]`) as NodeListOf<HTMLButtonElement>
const closeTriggers = modal.querySelectorAll('[data-modal-close]') as NodeListOf<HTMLButtonElement>
if (openTriggers.length === 0 || closeTriggers.length === 0) {
console.error('initializeModal: Elements required for modal trigger are not found.')
return
}
openTriggers.forEach((trigger) => {
trigger.addEventListener('click', (event) => handleOpenTriggerClick(event, modal, trigger), false)
trigger.addEventListener('mousedown', handleTriggerFocus, false)
trigger.addEventListener('keydown', handleTriggerFocus, false)
})
closeTriggers.forEach((trigger) => {
trigger.addEventListener('click', (event) => handleCloseTriggerClick(event, modal), false)
})
}
const waitModalAnimation = (modal: HTMLDialogElement): Promise<PromiseSettledResult<Animation>[]> => {
if (modal.getAnimations().length === 0) {
return Promise.resolve([])
}
return Promise.allSettled([...modal.getAnimations()].map((animation) => animation.finished))
}
let currentOpenTrigger: HTMLButtonElement | null = null
const handleOpenTriggerClick = (event: Event, modal: HTMLDialogElement, trigger: HTMLButtonElement): void => {
event.preventDefault()
currentOpenTrigger = trigger
openModal(modal)
}
const handleCloseTriggerClick = (event: Event, modal: HTMLDialogElement): void => {
event.preventDefault()
closeModal(modal)
}
const handleTriggerFocus = (event: Event): void => {
if (event.type === 'mousedown') {
document.documentElement.setAttribute('data-mousedown', 'true')
}
if (event.type === 'keydown') {
document.documentElement.removeAttribute('data-mousedown')
}
}
const handleBackdropClick = (event: MouseEvent, modal: HTMLDialogElement): void => {
if (event.target === modal) {
closeModal(modal)
}
}
const handleKeyDown = (event: KeyboardEvent, modal: HTMLDialogElement): void => {
document.documentElement.removeAttribute('data-mousedown')
if (event.key === 'Escape') {
event.preventDefault()
closeModal(modal)
}
}
const unsubscribeListeners: Array<() => void> = []
let isAnimating: boolean = false
const openModal = (modal: HTMLDialogElement): void => {
if (isAnimating) return
isAnimating = true
modal.showModal()
backfaceFixed(true)
const backdropClickHandler = (event: MouseEvent) => handleBackdropClick(event, modal)
modal.addEventListener('click', backdropClickHandler, false)
const keyDownHandler = (event: KeyboardEvent) => handleKeyDown(event, modal)
window.addEventListener('keydown', keyDownHandler, false)
unsubscribeListeners.push(() => {
modal.removeEventListener('click', backdropClickHandler)
window.removeEventListener('keydown', keyDownHandler)
})
requestAnimationFrame(async () => {
modal.setAttribute('data-active', 'true')
await waitModalAnimation(modal)
isAnimating = false
})
}
const closeModal = async (modal: HTMLDialogElement): Promise<void> => {
if (isAnimating) return
isAnimating = true
modal.setAttribute('data-active', 'false')
backfaceFixed(false)
unsubscribeListeners.forEach((unsubscribe) => unsubscribe())
await waitModalAnimation(modal)
modal.close()
if (currentOpenTrigger) {
currentOpenTrigger.focus()
currentOpenTrigger = null
}
isAnimating = false
}
export default initializeModal
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment