如何在Chrome扩展中配合React使用react-shadow实现Grammarly式文本框悬浮元素
实现方案说明
你之前的实现核心问题在于:
input/textarea属于原生可替换元素,本身不支持直接挂载 Shadow DOM,直接调用attachShadow会静默失败,也会影响输入框本身的原生行为。
Grammarly的核心实现逻辑可以拆解为以下几点:
- 不修改原页面的输入框元素,而是单独创建一个自定义DOM节点作为Shadow DOM的宿主,挂载到
document.body下,完全和原页面DOM隔离,避免被原页面样式影响 - 实时监听当前激活的输入框状态,通过
getBoundingClientRect()获取输入框的视口坐标,动态计算悬浮元素的定位值 - 监听页面滚动、窗口缩放、输入框尺寸变化、焦点切换等事件,实时更新悬浮元素的位置,保证始终对齐在输入框的指定位置
原生JS基础实现示例
// 1. 创建Shadow宿主节点,挂载到body const shadowHost = document.createElement('div'); // 宿主本身设置fixed定位,层级拉满,不影响原页面布局 shadowHost.style.cssText = ` position: fixed; z-index: 99999; pointer-events: none; `; document.body.appendChild(shadowHost); // 2. 挂载Shadow DOM const shadowRoot = shadowHost.attachShadow({ mode: 'open' }); // 3. 创建悬浮元素,写入样式和内容 const floatingBtn = document.createElement('div'); floatingBtn.className = 'grammarly-btn'; floatingBtn.textContent = '悬浮按钮'; floatingBtn.style.cssText = ` position: absolute; width: 32px; height: 32px; background: #16a34a; border-radius: 50%; pointer-events: all; cursor: pointer; `; // Shadow内部样式隔离 const style = document.createElement('style'); style.textContent = ` .grammarly-btn:hover { background: #15803d; } `; shadowRoot.appendChild(style); shadowRoot.appendChild(floatingBtn); // 4. 定位更新函数 function updatePosition(targetInput) { if (!targetInput || !['INPUT', 'TEXTAREA'].includes(targetInput.tagName)) { shadowHost.style.display = 'none'; return; } shadowHost.style.display = 'block'; const rect = targetInput.getBoundingClientRect(); // 对齐到输入框右下角,预留4px边距 const left = rect.left + rect.width - 32 - 4; const top = rect.top + rect.height - 32 - 4; shadowHost.style.left = `${left}px`; shadowHost.style.top = `${top}px`; } // 5. 绑定事件监听 document.addEventListener('focusin', (e) => updatePosition(e.target)); document.addEventListener('focusout', () => shadowHost.style.display = 'none'); window.addEventListener('scroll', () => updatePosition(document.activeElement), true); window.addEventListener('resize', () => updatePosition(document.activeElement)); // 监听输入框内容变化导致的尺寸变化 document.addEventListener('input', (e) => { if (e.target.tagName === 'TEXTAREA') updatePosition(e.target); });
React适配方案
不需要使用第三方react-shadow包,直接用React原生的createPortal就能把组件渲染到Shadow DOM内部:
import { createPortal, useEffect, useState } from 'react'; // 悬浮按钮组件 function FloatingGrammarlyBtn() { const [shadowRoot, setShadowRoot] = useState(null); useEffect(() => { // 初始化创建Shadow宿主 const host = document.createElement('div'); host.style.cssText = ` position: fixed; z-index: 99999; pointer-events: none; `; document.body.appendChild(host); const root = host.attachShadow({ mode: 'open' }); setShadowRoot(root); // 事件监听逻辑和上面原生实现一致 return () => host.remove(); }, []); if (!shadowRoot) return null; return createPortal( <> <style> {` .floating-btn { width: 32px; height: 32px; background: #16a34a; border-radius: 50%; pointer-events: all; cursor: pointer; } .floating-btn:hover { background: #15803d; } `} </style> <div className="floating-btn">悬浮</div> </>, shadowRoot ); }
内容的提问来源于stack exchange,提问作者user17085802
相关产品推荐
相关产品推荐

