1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-01-16 13:09:31 +01:00

Add Safari workaround inside shadow DOM. (#5648)

* 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.
This commit is contained in:
Mahmoud Elsayad 2024-05-15 18:17:18 +03:00 committed by GitHub
parent d0d4c63649
commit 0bb7be5496
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 103 additions and 3 deletions

View File

@ -0,0 +1,5 @@
---
'slate-react': minor
---
Fix Safari selection inside Shadow DOM.

View File

@ -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

View File

@ -34,6 +34,7 @@ import {
DOMElement,
DOMRange,
DOMText,
getActiveElement,
getDefaultView,
isDOMElement,
isDOMNode,
@ -50,6 +51,7 @@ import {
IS_WEBKIT,
IS_UC_MOBILE,
IS_WECHATBROWSER,
IS_SAFARI_LEGACY,
} from '../utils/environment'
import Hotkeys from '../utils/hotkeys'
import {
@ -156,6 +158,7 @@ export const Editable = (props: EditableProps) => {
const [placeholderHeight, setPlaceholderHeight] = useState<
number | undefined
>()
const processing = useRef(false)
const { onUserInput, receivedUserInput } = useTrackUserInput()
@ -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)) &&
@ -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 (

View File

@ -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
}

View File

@ -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 =

View File

@ -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!')
})
})