我给弹窗添加了支持物理返回键 二

背景: 书接上文

上文说到, 如果页面上多个弹窗并存时, popstate 事件会在所有弹窗中触发, 导致一个 popstate 事件关闭掉所有弹窗, 那我们的问题就是, 如何管理 history 栈, 让其逐个触发呢?

实现思路

既然所有的弹窗都在监听 popstate, 那我们维护一个队列, 弹窗出现时往队列放入一个特征值, popstate 事件时, 弹窗根据队列尾部的值, 决定自身是否需要响应 popstate;

image.png

show me your code

首先我们实现一个管理 index 的机制

export class IndexManager {
  private stack: number[] = []

  get latest() {
    // 是最大值 而非 最后一个元素, 因为 stack 不一定是 有序的
    return Math.max(...this.stack) ?? 0
  }

  push(n = this.latest) {
    this.stack.push(n)
  }

  drop(n = this.latest) {
    this.stack = this.stack.filter((k) => k !== n)
  }

  has(n: number) {
    return this.stack.includes(n)
  }
}

然后就是最终实现了:

import { useListen } from './useListen'

import { IndexManager } from '@/utils/IndexManager'

import { useEventCallback } from '@mui/material'
import { useRef } from 'react'
import { useEvent } from 'react-use'

let ignoreIdx = -1
// 这个 manager 既非 stack, 也非 queue,
// 叫 stack 只是随便叫的
const stack = new IndexManager()

/**
 * 该 hook 可用于任何需要物理返回键的地方;
 * 注释中的所有 modal 或 弹窗 都不仅限于 "弹窗",
 * 任何需要的地方都可以用,
 * 如 Dropdown/Drawer/Tooltip 等, 任何有 visible change 的地方都可以用;
 * (其实没有 visible change 的地方也可以用, 毕竟该 hook 只负责管理 history, 不干涉 ui)
 */
export function useInjectHistory(
  open: boolean,
  /**
   * 如果需要在用户物理返回时关闭弹窗, 就在该方法中手动调用 modal.hide();
   * 如果拒绝关闭弹窗, 就别 hide() 并 throw Error;
   */
  onPopState: (e: PopStateEvent) => Promise<void>
) {
  // 弹窗维护各自的 index
  const IndexRef = useRef(-1)

  const finalOnPopState = useEventCallback((e: PopStateEvent) => {
    // 轮到我了吗? 没轮到就返回
    if (IndexRef.current !== stack.latest) {
      return
    }
    // 是上一个弹窗关闭而触发的 popstate 吗? 是就返回;
    // (并且把 ignoreIdx 标志位清掉, 使得下次 popstate 能正常生效)
    if (IndexRef.current === ignoreIdx) {
      ignoreIdx = -1
      return
    }
    stack.drop(IndexRef.current)
    onPopState(e).catch(() => {
      // 抛错时, 恢复 stack
      stack.push(IndexRef.current)
      window.history.pushState(null, '', window.location.href)
    })
  })

  useEvent('popstate', finalOnPopState)

  useListen(!!open, () => {
    if (open) {
      // modal index 初始化
      IndexRef.current = stack.latest + 1
      stack.push(IndexRef.current)
      window.history.pushState(null, '', window.location.href)
    } else {
      if (stack.has(IndexRef.current)) {
        // stack 内有该 modal index, 说明是其他地方手动调用 modal.hide, 而非物理返回所触发
        stack.drop(IndexRef.current)
        // 此时需要先清理 stack, 然后根据新 stack 设置标志位
        ignoreIdx = stack.latest
        // 设置标志位是为了避免下一个 modal 被此处触发的 popstate 关闭
        window.history.back()
      }
      // 不管怎么样, modal close 的时候都需要还原该 modal index
      IndexRef.current = -1
    }
  })
}

usage

function TempA() {
  const modal = useModal()

  useInjectHistory(modal.visible, () => {
    modal.hide()
  })

  return <></>
}

function TempB() {
  const [open, setOpen] = useState(false)

  useInjectHistory(open, () => {
    setOpen(false)
  })

  return <Drawer
    open={open}
    onClose={() => {
      setOpen(false)
    }}
  >
    body
  </Drawer>
}

warning

具体实现代码可能会有更新,请以 github 内容为准。

如非特别声明,本站作品均为原创,遵循【自由转载-保持署名-非商用-非衍生 创意共享 3.0 许可证】。

对于转载作品,如需二次转载,请遵循原作许可。