在 Chrome 扩展的 Shadow DOM 中使用 shadui/cn 渲染弹窗组件

本文最后更新于:2024年8月21日 早上

背景

最近使用 WXT 创建 Chrome 扩展 Google Search Console - Bulk Index Cleaner,实现时使用了 shadui/cn 作为 UI 组件库,同时在 Content Script 中使用了 Shadow DOM 以隔离 CSS,因此发现了一些弹窗相关组件的问题。

为什么是 Shadow DOM

正常情况下,使用 Chrome 扩展向网页中注入 UI 时,会受到网页原有 CSS 的影响,这会导致 TailwindCSS 这种 UI 框架用起来也很糟心,可能与原本网页的 CSS 产生冲突。而 Shadow DOM 可以解决这个问题,它可以将组件的 CSS 与网页的 CSS 隔离开。

例如原本的网页结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
<html>
<head>
<style>
/* 网页的 css */
</style>
</head>
<body>
<div class="content">
<!-- 网页的内容 -->
</div>
</body>
</html>

不使用 Shadow DOM 时注入一个 Dialog 组件的示例,Dialog 组件的 CSS 可能与网页原有的 CSS 产生冲突:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<html>
<head>
<style>
/* 网页的 css */
</style>
<style>
/* dialog 组件的 css,可能与网页原有的 css 产生冲突 */
</style>
</head>
<body>
<div class="content">
<!-- 网页的内容 -->
</div>
<div class="dialog-container">
<!-- dialog 组件渲染的位置 -->
</div>
</body>
</html>

使用 Shadow DOM 时注入一个 Dialog 组件的示例,可以看到 HTML/CSS 都被隔离在了 custom-element 之中,而且也不会受到网页原有的 CSS 影响:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<html>
<head>
<style>
/* 网页的 css */
</style>
</head>
<body>
<div class="content">
<!-- 网页的内容 -->
</div>
<!-- 注入的 Shadow DOM -->
<custom-element>
#shadow-root
<style>
/* dialog 组件的 css,与网页原有的 css 隔离 */
</style>
<body>
<div class="dialog-container">
<!-- dialog 组件渲染的位置 -->
</div>
</body>
</custom-element>
</body>
</html>

衍生问题

说完了它解决的问题,再说说它带来的问题。主要还是 shadui/cn 这个组件库中的弹窗组件都使用 Portal 渲染,而 Portal 渲染的位置默认是在 document.body 中,并非是在 custom-element 组件之中,因而 custom-element 组件中的 CSS 无法应用到外层的弹窗上。

例如下面是默认情况下 Dialog 在网页中渲染的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<custom-element>
#shadow-root
<style>
/* 扩展注入的 css 样式 */
.dialog-container {
/* dialog 容器的样式 */
}
</style>
<body>
<!-- 内容 -->
</body>
</custom-element>

<!-- 默认在 custom-element 组件外的 document.body 中渲染了 -->
<div class="dialog-container">
<!-- dialog 组件渲染的位置 -->
</div>

可以看到 Dialog 组件渲染在了 document.body 中,而不是在 custom-element 组件中,导致 Dialog 组件无法应用到 custom-element 组件中的注入的 CSS 样式。

解决

目前在使用 shadui/cn 时,已经发现了两个组件存在这个问题:Dialog 和 Toast,下面将给出解决方法。

Dialog

Dialog 组件比较容易解决,找到 components/ui/dialog 中的 DialogPortal 组件,设置 container 字段指定渲染的容器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal
/* 这里是 custom element */
container={document
.querySelector('custom-element')
?.shadowRoot?.querySelector('body')}
>
<DialogOverlay />
{/* 其他代码... */}
</DialogPortal>
))

之后便可正常使用 Dialog 组件了,它现在会正确渲染到 custom-element 组件中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<custom-element>
#shadow-root
<style>
/* 扩展注入的 css 样式 */
.dialog-container {
/* dialog 容器的样式 */
}
</style>
<body>
<!-- 内容 -->
<!-- 现在会在指定的 Shadow DOM 组件中渲染了 -->
<div class="dialog-container">
<!-- dialog 组件渲染的位置 -->
</div>
</body>
</custom-element>

Toast

Toast 组件没有提供类似 Dialog 的 Portal 组件,需要在根组件中添加 Toaster 组件。

1
2
3
4
5
6
7
8
9
10
<>
<App />
{createPortal(
<Toaster />,
document
/* 这里是 custom element */
.querySelector('custom-element')
?.shadowRoot?.querySelector('body'),
)}
</>

总结

以上就是使用 shadui/cn 时在指定 DOM 中渲染弹窗的方法,在编写 Chrome 扩展时虽然已经有了 WXT 这种非常优秀的开发者工具,但仍然存在与普通网页不同的问题。


在 Chrome 扩展的 Shadow DOM 中使用 shadui/cn 渲染弹窗组件
https://blog.rxliuli.com/p/7e7a1dd99853473ab2af10fdd23dc488/
作者
rxliuli
发布于
2024年8月16日
许可协议