Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
134 lines
3.3 KiB
TypeScript
134 lines
3.3 KiB
TypeScript
'use client'
|
|
|
|
import { useEffect, useRef } from 'react'
|
|
|
|
/**
|
|
* Closes an overlay when the user presses the browser/Android Back button,
|
|
* without changing the visible URL.
|
|
*
|
|
* How it works
|
|
* - On open: `history.pushState` (same path/search) so Back targets this overlay.
|
|
* - On Back (`popstate`): call `onRequestClose` (do not `history.back` again).
|
|
* - On programmatic close: `history.back()` once and ignore that `popstate`.
|
|
* - Nested overlays share a module stack; Back always dismisses the topmost entry.
|
|
* - When `allowClose` is false, Back is trapped by re-pushing the history entry.
|
|
*/
|
|
|
|
const HISTORY_STATE_KEY = 'consumerModalHistory'
|
|
|
|
interface HistoryBackEntry {
|
|
id: number
|
|
onBack: () => void
|
|
canClose: () => boolean
|
|
}
|
|
|
|
let nextId = 1
|
|
const stack: HistoryBackEntry[] = []
|
|
let ignorePopCount = 0
|
|
let listenerAttached = false
|
|
|
|
function onPopState() {
|
|
if (ignorePopCount > 0) {
|
|
ignorePopCount -= 1
|
|
|
|
return
|
|
}
|
|
|
|
const top = stack[stack.length - 1]
|
|
|
|
if (!top) return
|
|
|
|
if (!top.canClose()) {
|
|
// Browser already consumed the entry — restore so the overlay keeps Back trapped.
|
|
window.history.pushState({ [HISTORY_STATE_KEY]: top.id }, '')
|
|
|
|
return
|
|
}
|
|
|
|
stack.pop()
|
|
top.onBack()
|
|
}
|
|
|
|
function ensureListener() {
|
|
if (listenerAttached || typeof window === 'undefined') return
|
|
window.addEventListener('popstate', onPopState)
|
|
listenerAttached = true
|
|
}
|
|
|
|
function register(entry: HistoryBackEntry) {
|
|
ensureListener()
|
|
stack.push(entry)
|
|
window.history.pushState({ [HISTORY_STATE_KEY]: entry.id }, '')
|
|
}
|
|
|
|
function unregister(entry: HistoryBackEntry) {
|
|
const idx = stack.indexOf(entry)
|
|
|
|
if (idx === -1) {
|
|
// Already removed by user Back
|
|
return
|
|
}
|
|
|
|
// Parent closed while nested children still registered — drop this entry and above.
|
|
const removeCount = stack.length - idx
|
|
|
|
stack.splice(idx, removeCount)
|
|
|
|
if (typeof window === 'undefined' || removeCount === 0) return
|
|
|
|
ignorePopCount += removeCount
|
|
for (let i = 0; i < removeCount; i += 1) {
|
|
window.history.back()
|
|
}
|
|
}
|
|
|
|
/** Clears module stack between tests (jsdom shares the page history). */
|
|
export function resetCloseOnHistoryBackForTests() {
|
|
stack.length = 0
|
|
ignorePopCount = 0
|
|
nextId = 1
|
|
}
|
|
|
|
export interface UseCloseOnHistoryBackOptions {
|
|
isOpen: boolean
|
|
/** When false, no history entry is pushed. Default true. */
|
|
enabled?: boolean
|
|
/**
|
|
* When false while open, Back does not close — the history entry is re-pushed.
|
|
* Use for `!isDismissable` / busy submit states.
|
|
*/
|
|
allowClose?: boolean
|
|
onRequestClose: () => void
|
|
}
|
|
|
|
/**
|
|
* Syncs overlay open state with the session history so Back dismisses the overlay.
|
|
*/
|
|
export function useCloseOnHistoryBack({ isOpen, enabled = true, allowClose = true, onRequestClose }: UseCloseOnHistoryBackOptions) {
|
|
const onRequestCloseRef = useRef(onRequestClose)
|
|
const allowCloseRef = useRef(allowClose)
|
|
|
|
useEffect(() => {
|
|
onRequestCloseRef.current = onRequestClose
|
|
allowCloseRef.current = allowClose
|
|
}, [allowClose, onRequestClose])
|
|
|
|
useEffect(() => {
|
|
if (!isOpen || !enabled || typeof window === 'undefined') return
|
|
|
|
const entry: HistoryBackEntry = {
|
|
id: nextId++,
|
|
onBack: () => {
|
|
onRequestCloseRef.current()
|
|
},
|
|
canClose: () => allowCloseRef.current,
|
|
}
|
|
|
|
register(entry)
|
|
|
|
return () => {
|
|
unregister(entry)
|
|
}
|
|
}, [enabled, isOpen])
|
|
}
|