指南:用层级叠加模态框
真实应用会在模态框中再打开模态框,比如需要确认的设置模态框,或打开选择器的表单。本指南说明 React Snappy Modal 如何排列叠加的模态框,以及如何关闭你想关闭的那个。
三个不同的设置
叠加模态框时涉及三个容易混淆的设置。
| 设置 | 含义 | 用途 |
|---|---|---|
layer | 数字选项,默认 0。已打开的模态框按 layer 排序,层级越高越晚渲染,显示在上方。 | 排列模态框分组,并通过 close(value, layer) 指定分组。 |
modalId | 每次调用 show() 生成的唯一 ID,可通过 useCurrentModal() 获取。 | 关闭某一个特定的模态框。 |
zIndex | 模态框区域的 CSS z-index,默认 1000。 | 让模态框显示在 sticky 头部等页面元素之上。 |
常见错误:按层级关闭
如果不传其他值,所有模态框都在层级 0 打开。当一个模态框打开第二个模态框,而第二个调用 SnappyModal.close() 时,库会查找层级 0 上的第一个模态框,也就是父模态框。结果父模态框被关闭,子模态框仍留在屏幕上。
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 子模态框,在子模态框回答后再关闭自己。
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()。把层级名称集中在一个常量中,顺序更易读。
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,从而关闭该层级的第一个模态框。
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() 在栈变化时重新渲染。可用于在模态框打开期间暂停全局快捷键,或显示叠加的模态框数量。
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只用于让模态框位于非模态框页面元素之上,而不是用于排列模态框之间的顺序。