指南:用层级叠加模态框

真实应用会在模态框中再打开模态框,比如需要确认的设置模态框,或打开选择器的表单。本指南说明 React Snappy Modal 如何排列叠加的模态框,以及如何关闭你想关闭的那个。

三个不同的设置

叠加模态框时涉及三个容易混淆的设置。

设置含义用途
layer数字选项,默认 0。已打开的模态框按 layer 排序,层级越高越晚渲染,显示在上方。排列模态框分组,并通过 close(value, layer) 指定分组。
modalId每次调用 show() 生成的唯一 ID,可通过 useCurrentModal() 获取。关闭某一个特定的模态框。
zIndex模态框区域的 CSS z-index,默认 1000。让模态框显示在 sticky 头部等页面元素之上。

常见错误:按层级关闭

如果不传其他值,所有模态框都在层级 0 打开。当一个模态框打开第二个模态框,而第二个调用 SnappyModal.close() 时,库会查找层级 0 上的第一个模态框,也就是父模态框。结果父模态框被关闭,子模态框仍留在屏幕上。

tsx
1function Child() {
2  // Looks for the first modal on layer 0 — that is the Parent
3  return <button onClick={() => SnappyModal.close('child done')}>Done</button>;
4}
5
6function Parent() {
7  return <button onClick={() => SnappyModal.show(<Child />)}>Open child</button>;
8}
9
10SnappyModal.show(<Parent />); // layer 0
11// Clicking "Done" closes Parent; Child stays on screen

方案 1:用 useCurrentModal 在内部关闭

resolveModal() 按 ID 关闭渲染它的那个模态框,无论同一层级有多少模态框都能准确关闭。把它作为所有模态框组件的默认方式。父模态框也可以 await 子模态框,在子模态框回答后再关闭自己。

tsx
1function Child() {
2  const { resolveModal } = useCurrentModal();
3  return <button onClick={() => resolveModal('child done')}>Done</button>;
4}
5
6function Parent() {
7  const { resolveModal } = useCurrentModal();
8
9  const openChild = async () => {
10    const answer = await SnappyModal.show(<Child />);
11    if (answer) resolveModal(answer); // close Parent only after Child answered
12  };
13
14  return <button onClick={openChild}>Open child</button>;
15}

方案 2:给嵌套模态框指定独立层级

如果需要在模态框外部关闭(例如计时器或全局快捷键),请为每一级使用独立层级打开,并把该层级传给 close()。把层级名称集中在一个常量中,顺序更易读。

tsx
1const LAYER = { page: 0, picker: 1, alert: 2 } as const;
2
3SnappyModal.show(<ColorPicker />, { layer: LAYER.picker });
4
5// Later, from anywhere (a timer, a keyboard shortcut, ...)
6SnappyModal.close(undefined, LAYER.picker);

方案 3:按 ID 关闭特定模态框

useCurrentModal() 还会返回 modalId。让模态框在 effect 中报告它,等拿到后再传给 SnappyModal.close(value, modalId),即使同一层级有其他模态框,也只会关闭这一个。不要在拿到 ID 之前关闭:空 ID 会被解析为层级 0,从而关闭该层级的第一个模态框。

tsx
1function UploadProgress({ onReady }: { onReady: (id: string) => void }) {
2  const { modalId } = useCurrentModal();
3  useEffect(() => onReady(modalId), [modalId, onReady]);
4  return <p>Uploading…</p>;
5}
6
7const opened = Promise.withResolvers<string>();
8SnappyModal.show(<UploadProgress onReady={opened.resolve} />, { allowOutsideClick: false });
9
10// Wait for the ID too: closing with an empty ID would target layer 0 instead
11const [modalId] = await Promise.all([opened.promise, upload(file)]);
12SnappyModal.close('uploaded', modalId);

多个模态框下的外部点击与滚动锁定

外部点击会按 ID 关闭被点击区域所属的模态框,并以 undefined resolve 其 Promise。下方的模态框保持打开。

只要任一已打开的模态框设置了 allowScroll: false,页面就保持锁定。所有这类模态框关闭后,才会解除锁定并恢复滚动位置。

响应栈的变化

useSnappyModalState() 在栈变化时重新渲染。可用于在模态框打开期间暂停全局快捷键,或显示叠加的模态框数量。

tsx
1function useEscapeToGoBack() {
2  const { isShow } = useSnappyModalState();
3
4  useEffect(() => {
5    if (isShow) return; // let the open modal handle the keyboard
6    const onKey = (event: KeyboardEvent) => {
7      if (event.key === 'Escape') history.back();
8    };
9    window.addEventListener('keydown', onKey);
10    return () => window.removeEventListener('keydown', onKey);
11  }, [isShow]);
12}

检查清单

如果一个页面可能同时打开多个模态框,请检查以下各项。

  • 模态框组件通过 useCurrentModal() 关闭自己。
  • 无参数的 SnappyModal.close() 只在最多只有一个模态框打开时使用。
  • 从外部关闭的模态框拥有独立的 layer,或按 modalId 关闭。
  • zIndex 只用于让模态框位于非模态框页面元素之上,而不是用于排列模态框之间的顺序。