Skip to content

Commit

Permalink
Add Safari workaround inside shadow DOM. (#5648)
Browse files Browse the repository at this point in the history
* Add Safari workaround inside shadow DOM.

* Add E2E test.

* Move browser checks to environment.ts

* Remove leftover @ts-ignore.
Fix linting change.

* Update `getActiveElement`

* Create red-poems-wave.md

* Fix prettier.

* Update E2E test.
  • Loading branch information
MahmoudElsayad committed May 15, 2024
1 parent d0d4c63 commit 0bb7be5
Show file tree
Hide file tree
Showing 6 changed files with 103 additions and 3 deletions.
5 changes: 5 additions & 0 deletions .changeset/red-poems-wave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'slate-react': minor
---

Fix Safari selection inside Shadow DOM.
6 changes: 3 additions & 3 deletions .yarnrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ compressionLevel: mixed
packageExtensions:
eslint-module-utils@*:
dependencies:
eslint-import-resolver-node: "*"
eslint-import-resolver-node: '*'
next@*:
dependencies:
eslint-import-resolver-node: "*"
eslint-import-resolver-node: '*'
react-error-boundary@*:
dependencies:
prop-types: "*"
prop-types: '*'

yarnPath: .yarn/releases/yarn-4.0.2.cjs
55 changes: 55 additions & 0 deletions packages/slate-react/src/components/editable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
DOMElement,
DOMRange,
DOMText,
getActiveElement,
getDefaultView,
isDOMElement,
isDOMNode,
Expand All @@ -50,6 +51,7 @@ import {
IS_WEBKIT,
IS_UC_MOBILE,
IS_WECHATBROWSER,
IS_SAFARI_LEGACY,
} from '../utils/environment'
import Hotkeys from '../utils/hotkeys'
import {
Expand Down Expand Up @@ -156,6 +158,7 @@ export const Editable = (props: EditableProps) => {
const [placeholderHeight, setPlaceholderHeight] = useState<
number | undefined
>()
const processing = useRef(false)

const { onUserInput, receivedUserInput } = useTrackUserInput()

Expand Down Expand Up @@ -202,6 +205,29 @@ export const Editable = (props: EditableProps) => {
const onDOMSelectionChange = useMemo(
() =>
throttle(() => {
const el = ReactEditor.toDOMNode(editor, editor)
const root = el.getRootNode()

if (
IS_SAFARI_LEGACY &&
!processing.current &&
IS_WEBKIT &&
root instanceof ShadowRoot
) {
processing.current = true

const active = getActiveElement()

if (active) {
document.execCommand('indent')
} else {
Transforms.deselect(editor)
}

processing.current = false
return
}

const androidInputManager = androidInputManagerRef.current
if (
(IS_ANDROID || !ReactEditor.isComposing(editor)) &&
Expand Down Expand Up @@ -471,6 +497,35 @@ export const Editable = (props: EditableProps) => {
// https://github.com/facebook/react/issues/11211
const onDOMBeforeInput = useCallback(
(event: InputEvent) => {
const el = ReactEditor.toDOMNode(editor, editor)
const root = el.getRootNode()

if (
IS_SAFARI_LEGACY &&
processing?.current &&
IS_WEBKIT &&
root instanceof ShadowRoot
) {
const ranges = event.getTargetRanges()
const range = ranges[0]

const newRange = new window.Range()

newRange.setStart(range.startContainer, range.startOffset)
newRange.setEnd(range.endContainer, range.endOffset)

// Translate the DOM Range into a Slate Range
const slateRange = ReactEditor.toSlateRange(editor, newRange, {
exactMatch: false,
suppressThrow: false,
})

Transforms.select(editor, slateRange)

event.preventDefault()
event.stopImmediatePropagation()
return
}
onUserInput()

if (
Expand Down
13 changes: 13 additions & 0 deletions packages/slate-react/src/utils/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,3 +314,16 @@ export const isTrackedMutation = (
// Target add/remove is tracked. Track the mutation if we track the parent mutation.
return isTrackedMutation(editor, parentMutation, batch)
}

/**
* Retrieves the deepest active element in the DOM, considering nested shadow DOMs.
*/
export const getActiveElement = () => {
let activeElement = document.activeElement

while (activeElement?.shadowRoot && activeElement.shadowRoot?.activeElement) {
activeElement = activeElement?.shadowRoot?.activeElement
}

return activeElement
}
9 changes: 9 additions & 0 deletions packages/slate-react/src/utils/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,15 @@ export const CAN_USE_DOM = !!(
typeof window.document.createElement !== 'undefined'
)

// Check if the browser is Safari and older than 17
export const IS_SAFARI_LEGACY =
typeof navigator !== 'undefined' &&
/Safari/.test(navigator.userAgent) &&
/Version\/(\d+)/.test(navigator.userAgent) &&
(navigator.userAgent.match(/Version\/(\d+)/)?.[1]
? parseInt(navigator.userAgent.match(/Version\/(\d+)/)?.[1]!, 10) < 17
: false)

// COMPAT: Firefox/Edge Legacy don't support the `beforeinput` event
// Chrome Legacy doesn't support `beforeinput` correctly
export const HAS_BEFORE_INPUT_SUPPORT =
Expand Down
18 changes: 18 additions & 0 deletions playwright/integration/examples/shadow-dom.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,22 @@ test.describe('shadow-dom example', () => {

await expect(innerShadow.getByRole('textbox')).toHaveCount(1)
})

test('renders slate editor inside nested shadow and edits content', async ({
page,
}) => {
const outerShadow = page.locator('[data-cy="outer-shadow-root"]')
const innerShadow = outerShadow.locator('> div')
const textbox = innerShadow.getByRole('textbox')

// Ensure the textbox is present
await expect(textbox).toHaveCount(1)

// Clear any existing text and type new text into the textbox
await textbox.fill('') // Clears the textbox
await textbox.type('Hello, Playwright!')

// Assert that the textbox contains the correct text
await expect(textbox).toHaveValue('Hello, Playwright!')
})
})

0 comments on commit 0bb7be5

Please sign in to comment.