Shadow DOM Support#3347
Conversation
|
There was a problem hiding this comment.
Pull request overview
This PR introduces experimental Shadow DOM support (gated behind the new 'ShadowDom' experimental feature) by extending DOMHelper with shadow-aware selection APIs and a “append to correct root” helper, then updating core selection/style/announce paths and the demo to work when the editor is hosted inside a ShadowRoot.
Changes:
- Added
'ShadowDom'toExperimentalFeatureand expandedDOMHelperwithgetSelectionRange,setSelectionRange, andappendToRoot. - Updated core selection + DOM selection APIs and lifecycle/announce/style behaviors to use the new
DOMHelpermethods (instead of directly usingdocument/head/ selection APIs). - Updated demo UI to toggle Shadow DOM hosting and added/updated unit tests + shared DOMHelper test mock.
Reviewed changes
Copilot reviewed 27 out of 27 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/roosterjs-content-model-types/lib/parameter/DOMHelper.ts | Extends DOMHelper interface with selection + root-append methods needed for Shadow DOM scenarios. |
| packages/roosterjs-content-model-types/lib/editor/ExperimentalFeature.ts | Adds the 'ShadowDom' experimental flag. |
| packages/roosterjs-content-model-core/test/testUtils/createMockDomHelper.ts | Adds a reusable spy-based DOMHelper mock for tests (including new methods). |
| packages/roosterjs-content-model-core/test/editor/core/DOMHelperImplTest.ts | Adds/updates DOMHelperImpl tests for selection range and shadow-root appending/focus. |
| packages/roosterjs-content-model-core/test/corePlugin/selection/SelectionPluginTest.ts | Updates selection plugin tests to use getDOMHelper().getSelectionRange() path. |
| packages/roosterjs-content-model-core/test/corePlugin/lifecycle/LifecyclePluginTest.ts | Updates lifecycle tests to expect announce container appended via DOMHelper.appendToRoot(). |
| packages/roosterjs-content-model-core/test/corePlugin/copyPaste/CopyPastePluginTest.ts | Removes now-unneeded spying on addRangeToSelection (function deleted). |
| packages/roosterjs-content-model-core/test/coreApi/setEditorStyle/setEditorStyleTest.ts | Updates style API tests to assert appendToRoot() is used for style element insertion. |
| packages/roosterjs-content-model-core/test/coreApi/setEditorStyle/ensureUniqueIdTest.ts | Updates tests to include getRootNode() to support shadow-root-aware uniqueness checks. |
| packages/roosterjs-content-model-core/test/coreApi/setDOMSelection/setDOMSelectionTest.ts | Updates tests to use domHelper.setSelectionRange() instead of addRangeToSelection(). |
| packages/roosterjs-content-model-core/test/coreApi/getDOMSelection/getDOMSelectionTest.ts | Updates tests to use domHelper.getSelectionRange() for selection retrieval. |
| packages/roosterjs-content-model-core/test/coreApi/announce/announceTest.ts | Updates announce tests to append via domHelper.appendToRoot() instead of document.body.appendChild. |
| packages/roosterjs-content-model-core/lib/utils/createAriaLiveElement.ts | Removes side-effect of auto-appending the ARIA live element (now appended by callers via DOMHelper). |
| packages/roosterjs-content-model-core/lib/utils/areSameRanges.ts | Introduces a shared helper for comparing ranges by boundary positions. |
| packages/roosterjs-content-model-core/lib/editor/core/DOMHelperImpl.ts | Implements shadow-root detection + new selection/range/root-append APIs. |
| packages/roosterjs-content-model-core/lib/editor/core/createEditorCore.ts | Wires 'ShadowDom' experimental flag into createDOMHelper(..., { useShadowDom }). |
| packages/roosterjs-content-model-core/lib/corePlugin/selection/SelectionPlugin.ts | Uses domHelper.getSelectionRange() for image-selection detection when selection changes. |
| packages/roosterjs-content-model-core/lib/corePlugin/lifecycle/LifecyclePlugin.ts | Appends announce container via editor.getDOMHelper().appendToRoot(). |
| packages/roosterjs-content-model-core/lib/corePlugin/cache/areSameSelections.ts | Switches to shared areSameRanges helper (removes duplicate implementation). |
| packages/roosterjs-content-model-core/lib/coreApi/setEditorStyle/setEditorStyle.ts | Inserts style elements via core.domHelper.appendToRoot() to support Shadow DOM hosting. |
| packages/roosterjs-content-model-core/lib/coreApi/setEditorStyle/ensureUniqueId.ts | Makes uniqueness checks shadow-root-aware by querying element.getRootNode(). |
| packages/roosterjs-content-model-core/lib/coreApi/setDOMSelection/setDOMSelection.ts | Routes range selection setting through core.domHelper.setSelectionRange(). |
| packages/roosterjs-content-model-core/lib/coreApi/setDOMSelection/addRangeToSelection.ts | Deletes old helper in favor of DOMHelper.setSelectionRange(). |
| packages/roosterjs-content-model-core/lib/coreApi/getDOMSelection/getDOMSelection.ts | Reads selection via core.domHelper.getSelectionRange() and validates it against the editor root. |
| packages/roosterjs-content-model-core/lib/coreApi/announce/announce.ts | Appends announce container via core.domHelper.appendToRoot(). |
| demo/scripts/controlsV2/sidePane/editorOptions/ExperimentalFeatures.tsx | Adds ShadowDom checkbox to the demo experimental features UI. |
| demo/scripts/controlsV2/mainPane/MainPane.tsx | Implements Shadow DOM wrapping/unwrapping of the demo editor when toggled. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| .and.returnValue(null), | ||
| findClosestBlockElement: jasmine | ||
| .createSpy('findClosestBlockElement') | ||
| .and.returnValue(null as any), |
| const doc = core.physicalRoot.ownerDocument; | ||
|
|
||
| styleElement = doc.createElement('style'); | ||
| doc.head.appendChild(styleElement); | ||
| core.domHelper.appendToRoot(styleElement); | ||
|
|
| const image = isSingleImageInSelection(range); | ||
| if (newSelection?.type == 'image' && !image) { | ||
| const range = selection.getRangeAt(0); | ||
| const sel = this.editor.getDocument().defaultView?.getSelection(); |
There was a problem hiding this comment.
Will this work in shadow dom?
There was a problem hiding this comment.
For shadowDom we cannot support reverted selection, since ranges are only returned by the getComposedRanges and this function returns StaticRange. So, we can keep it like this as there is no other way to confirm whether the range is reverted.
…u/bvalverde/shadowDomSupport
Shadow DOM Support as Experimental Feature
This PR adds Shadow DOM support to roosterjs, gated behind the
'ShadowDom'experimental feature flag.Changes
New Experimental Feature:
'ShadowDom'ExperimentalFeaturetype — when enabled,DOMHelperImpldetects and works within a shadow root boundary for selection, focus, and element appending.Core (
roosterjs-content-model-core)DOMHelperImpl— Shadow root detection is now conditional onuseShadowDomoption; addedgetSelectionRange,setSelectionRange,appendToRoot, andhasFocussupport for shadow DOM.createEditorCore— Passes the'ShadowDom'feature flag tocreateDOMHelper.getDOMSelection/setDOMSelection/setEditorStyle/SelectionPlugin/LifecyclePlugin— Updated to work correctly within a shadow boundary.Types (
roosterjs-content-model-types)ExperimentalFeature— Added'ShadowDom'variant.DOMHelper— AddedgetSelectionRange,setSelectionRange, andappendToRootmethods.Demo site
'ShadowDom'feature flag.Tests
DOMHelperImpl, selection, and style APIs covering shadow DOM scenarios.