From 28809be5334893bc7ada9cbbc221eca7c7289266 Mon Sep 17 00:00:00 2001 From: Mikey Stengel Date: Tue, 3 Dec 2024 18:21:14 +0100 Subject: [PATCH] refactor(editor-packages): Throw out all mentions of shadow dom * Change docs * Restore old code * Simplify editor-web-component API by removing attribute/property --- packages/editor-web-component/README.md | 9 +++-- .../src/editor-web-component.tsx | 22 +--------- packages/editor/demo/lit/index.js | 13 +----- .../core/hooks/use-blur-on-outside-click.ts | 9 ++--- .../editor/src/core/hooks/use-shadow-root.ts | 40 ------------------- .../editor/src/editor-ui/editor-modal.tsx | 21 ++-------- .../editor/src/editor-ui/slate-overlay.tsx | 33 ++------------- packages/editor/src/math/editor.tsx | 5 +-- .../plugins/text/hooks/use-editor-change.tsx | 15 +------ 9 files changed, 21 insertions(+), 146 deletions(-) delete mode 100644 packages/editor/src/core/hooks/use-shadow-root.ts diff --git a/packages/editor-web-component/README.md b/packages/editor-web-component/README.md index 2d921f65bc..8177388b37 100644 --- a/packages/editor-web-component/README.md +++ b/packages/editor-web-component/README.md @@ -103,9 +103,12 @@ The plugins attribute/property accepts an array of plugin types. You can referen ## Shadow DOM vs. normal DOM -We give you the option whether you want to render the web-component within the Shadow DOM or not. Both have their pros and cons. Outside of the Shadow DOM, it's easier to run into style collisions. However, the Serlo Editor within the Shadow DOM is still buggy in a few places, especially when it comes to focus management. -As we'll be deprecating the Shadow DOM soon, we highly recommend using the normal DOM! -By default we are rendering the Serlo Editor within the normal DOM. If you want to render it within the Shadow DOM, you can pass `true` to the `use-shadow-dom` argument. Bug reports and fixes in form of a PR for the use-shadow-dom mode are very welcome! +Version 0.10.3 was the last stable version where you can render the Serlo Editor within the Shadow DOM. All future versions will only work in the regular DOM! + +### For versions < 0.10.3 + +We give you the option whether you want to render the web-component within the Shadow DOM or not. Both have their pros and cons. Outside of the Shadow DOM, it's easier to run into style collisions. However, the Serlo Editor within the Shadow DOM is buggy in a lot of places, especially when it comes to focus management. +By default we are rendering the Serlo Editor within the normal DOM. If you want to render it within the Shadow DOM, you can pass `true` to the `use-shadow-dom` argument. ```html diff --git a/packages/editor-web-component/src/editor-web-component.tsx b/packages/editor-web-component/src/editor-web-component.tsx index 4320d3acd6..95c8ab0756 100644 --- a/packages/editor-web-component/src/editor-web-component.tsx +++ b/packages/editor-web-component/src/editor-web-component.tsx @@ -43,15 +43,10 @@ export class EditorWebComponent extends HTMLElement { private _isProductionEnvironment: boolean = false - // By default, we are NOT attaching it to the shadow DOM - private _useShadowDOM: boolean = false - constructor() { super() this.container = document.createElement('div') - - // Shadow DOM will be attached in connectedCallback if needed } static get observedAttributes() { @@ -59,7 +54,6 @@ export class EditorWebComponent extends HTMLElement { 'initial-state', 'mode', 'testing-secret', - 'use-shadow-dom', 'editor-variant', 'plugins', 'is-production-environment', @@ -75,8 +69,6 @@ export class EditorWebComponent extends HTMLElement { (newValue === 'read' || newValue === 'write') ) { this.mode = newValue - } else if (name === 'use-shadow-dom') { - this._useShadowDOM = newValue === 'true' } else if (name === 'editor-variant' && oldValue !== newValue) { this.editorVariant = newValue as EditorVariant } else if (name === 'plugins' && oldValue !== newValue) { @@ -169,13 +161,7 @@ export class EditorWebComponent extends HTMLElement { } connectedCallback() { - if (this._useShadowDOM && !this.shadowRoot) { - this.attachShadow({ mode: 'open' }) - this.shadowRoot!.appendChild(this.container) - } else if (!this._useShadowDOM) { - this.appendChild(this.container) - } - + this.appendChild(this.container) this.loadAndApplyStyles() if (!this.reactRoot) { @@ -188,11 +174,7 @@ export class EditorWebComponent extends HTMLElement { loadAndApplyStyles() { const styleEl = document.createElement('style') styleEl.textContent = styles - if (this._useShadowDOM) { - this.shadowRoot!.appendChild(styleEl) - } else { - this.appendChild(styleEl) - } + this.appendChild(styleEl) } broadcastNewState(newState: unknown): void { diff --git a/packages/editor/demo/lit/index.js b/packages/editor/demo/lit/index.js index 0e15cbcde2..9fa296c15d 100644 --- a/packages/editor/demo/lit/index.js +++ b/packages/editor/demo/lit/index.js @@ -27,7 +27,6 @@ const initialExampleState = { class SerloEditorDemo extends LitElement { static properties = { editing: { type: Boolean }, - isRenderedInShadowRoot: false, editorState: { type: Object }, selectedPlugin: { type: String }, } @@ -39,17 +38,8 @@ class SerloEditorDemo extends LitElement { this.selectedPlugin = Plugin.Text } - // Render in light mode. Override this if you want to render the lit component - // and the editor within the Shadow DOM. - createRenderRoot() { - this.isRenderedInShadowRoot = false - return this - } - getEditor() { - return this.isRenderedInShadowRoot - ? this.shadowRoot.querySelector('serlo-editor') - : this.querySelector('serlo-editor') + return this.querySelector('serlo-editor') } writeCurrentEditorState() { @@ -117,7 +107,6 @@ class SerloEditorDemo extends LitElement {
) { const dispatch = useAppDispatch() - const shadowRoot = useShadowRoot(editorWrapperRef) useEffect(() => { - const root = shadowRoot || document.body + const root = document.body function handleClickOutside(event: Event) { const mouseEvent = event as MouseEvent @@ -38,12 +35,12 @@ export function useBlurOnOutsideClick( } } - const rootListener = shadowRoot || document + const rootListener = document // Bind the event listener rootListener.addEventListener('mousedown', handleClickOutside) return () => { // Unbind the event listener on clean up rootListener.removeEventListener('mousedown', handleClickOutside) } - }, [editorWrapperRef, dispatch, shadowRoot]) + }, [editorWrapperRef, dispatch]) } diff --git a/packages/editor/src/core/hooks/use-shadow-root.ts b/packages/editor/src/core/hooks/use-shadow-root.ts deleted file mode 100644 index 0219f64713..0000000000 --- a/packages/editor/src/core/hooks/use-shadow-root.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { useEffect, useState } from 'react' - -export function useShadowRoot(containerRef: React.RefObject) { - const [shadowRoot, setShadowRoot] = useState(null) - - useEffect(() => { - // Should be fixed in React 19, hopefully. If not for the timeout, - // getRootNode() will not consistently return the shadow root... - const timeout = setTimeout(() => { - // containerRef.current.shadowRoot is always null which is why we - // use getRootNode. - const possibleShadowRoot = containerRef.current?.getRootNode() - if (possibleShadowRoot instanceof ShadowRoot) { - setShadowRoot(possibleShadowRoot) - } - }, 1) - - return () => { - clearTimeout(timeout) - } - }, [containerRef]) - - return shadowRoot -} - -export function isShadowRoot( - shadowRoot: ShadowRoot | null | Node | undefined -): shadowRoot is ShadowRoot { - return shadowRoot instanceof ShadowRoot -} - -export function getFirstElementOrUndefined( - shadowRoot: ShadowRoot | null | undefined -): HTMLElement | undefined { - if (!shadowRoot) { - return undefined - } - - return shadowRoot?.firstElementChild as HTMLElement -} diff --git a/packages/editor/src/editor-ui/editor-modal.tsx b/packages/editor/src/editor-ui/editor-modal.tsx index 118ece1fde..5cf8747c96 100644 --- a/packages/editor/src/editor-ui/editor-modal.tsx +++ b/packages/editor/src/editor-ui/editor-modal.tsx @@ -1,7 +1,3 @@ -import { - getFirstElementOrUndefined, - useShadowRoot, -} from '@editor/core/hooks/use-shadow-root' import { cn } from '@editor/utils/cn' import { faXmark } from '@fortawesome/free-solid-svg-icons' import * as Dialog from '@radix-ui/react-dialog' @@ -34,12 +30,8 @@ export function EditorModal({ onEscapeKeyDown, onKeyDown, }: EditorModalProps) { - const shadowRootRef = useRef(null) - const shadowRoot = useShadowRoot(shadowRootRef) const previouslyFocusedElementRef = useRef(null) - const appElement = getFirstElementOrUndefined(shadowRoot) - const onOpenChange = useCallback( (open: boolean) => { if (open !== false) { @@ -67,20 +59,13 @@ export function EditorModal({ return } - if (shadowRoot) { - previouslyFocusedElementRef.current = - shadowRoot.activeElement as HTMLElement - } else { - previouslyFocusedElementRef.current = - document.activeElement as HTMLElement - } - }, [isOpen, shadowRoot]) + previouslyFocusedElementRef.current = document.activeElement as HTMLElement + }, [isOpen]) return ( <> -
- + diff --git a/packages/editor/src/editor-ui/slate-overlay.tsx b/packages/editor/src/editor-ui/slate-overlay.tsx index c895b3219e..582b781d74 100644 --- a/packages/editor/src/editor-ui/slate-overlay.tsx +++ b/packages/editor/src/editor-ui/slate-overlay.tsx @@ -20,7 +20,7 @@ export function SlateOverlay(props: SlateOverlayProps) { // select the correct anchor const timeout = setTimeout(() => { if (!wrapper.current) return - const anchorRect = getAnchorRect(editor, anchor, wrapper.current) + const anchorRect = getAnchorRect(editor, anchor) const parentRect = wrapper.current .closest('.rows-editor-renderer-container') ?.getBoundingClientRect() @@ -57,13 +57,11 @@ export function SlateOverlay(props: SlateOverlayProps) { ) } -// If provided an anchor element, returns its size and position (DOMRect). Also -// checks the Shadow DOM, otherwise retrieves the native DOM selection, and -// yields a DOMRect based on it. +// If provided an anchor element, returns its size and position (DOMRect). Otherwise +// retrieves the native DOM selection, and yields a DOMRect based on it. function getAnchorRect( editor: ReactEditor, - anchor: CustomElement | undefined, - wrapper: HTMLDivElement + anchor: CustomElement | undefined ): DOMRect | null { if (anchor) { return ( @@ -71,9 +69,6 @@ function getAnchorRect( ) } - const shadowRect = getRectWithinShadowDom(wrapper) - if (shadowRect) return shadowRect - const nativeDomSelection = window.getSelection() if (nativeDomSelection && nativeDomSelection.rangeCount > 0) { return nativeDomSelection.getRangeAt(0).getBoundingClientRect() @@ -81,23 +76,3 @@ function getAnchorRect( return null } - -function getRectWithinShadowDom(wrapper: HTMLDivElement): DOMRect | null { - const rootNode = wrapper.getRootNode() as ShadowRoot | Document - - if (!(rootNode instanceof ShadowRoot)) return null - - const activeElement = rootNode.activeElement as HTMLElement - if (activeElement) { - const rect = activeElement.getBoundingClientRect() - return new DOMRect(rect.left, rect.top, rect.width, rect.height) - } - - const shadowHostRect = rootNode.host.getBoundingClientRect() - return new DOMRect( - shadowHostRect.left, - shadowHostRect.top, - shadowHostRect.width, - shadowHostRect.height - ) -} diff --git a/packages/editor/src/math/editor.tsx b/packages/editor/src/math/editor.tsx index 83220b6b6b..ae52a44e7b 100644 --- a/packages/editor/src/math/editor.tsx +++ b/packages/editor/src/math/editor.tsx @@ -1,4 +1,3 @@ -import { isShadowRoot } from '@editor/core/hooks/use-shadow-root' import { FaIcon } from '@editor/editor-ui/fa-icon' import { ToolbarSelect } from '@editor/editor-ui/plugin-toolbar' import { useEditStrings } from '@editor/i18n/edit-strings-provider' @@ -158,11 +157,9 @@ export function MathEditor(props: MathEditorProps) { function renderControlsPortal(children: JSX.Element) { const root = containerRef.current?.getRootNode() - const isDocument = root instanceof Document - const isShadowRootNode = isShadowRoot(root) const target = - (isShadowRootNode || isDocument + (isDocument ? root.querySelector( '.plugin-text .toolbar-controls-target' ) diff --git a/packages/editor/src/plugins/text/hooks/use-editor-change.tsx b/packages/editor/src/plugins/text/hooks/use-editor-change.tsx index 5f1fb20fb5..0f17eb49f7 100644 --- a/packages/editor/src/plugins/text/hooks/use-editor-change.tsx +++ b/packages/editor/src/plugins/text/hooks/use-editor-change.tsx @@ -95,20 +95,7 @@ function isEditorInDOM(editor: Editor) { try { // Get DOMNode of the whole editor const domNode = ReactEditor.toDOMNode(editor, editor) - if (document.body.contains(domNode)) { - return true - } - - // Fallback to checking if it's in the Shadow DOM - let rootNode = domNode.getRootNode() as ShadowRoot | Document - while (rootNode instanceof ShadowRoot) { - if (rootNode.host.contains(domNode)) { - return true - } - rootNode = rootNode.host.getRootNode() as ShadowRoot | Document - } - - return false + return document.body.contains(domNode) } catch (error) { // eslint-disable-next-line no-console console.warn('Error checking if editor is in DOM. Not mounted!', error)