mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-12 10:14:02 +02: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:
5
.changeset/red-poems-wave.md
Normal file
5
.changeset/red-poems-wave.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'slate-react': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix Safari selection inside Shadow DOM.
|
@@ -3,12 +3,12 @@ compressionLevel: mixed
|
|||||||
packageExtensions:
|
packageExtensions:
|
||||||
eslint-module-utils@*:
|
eslint-module-utils@*:
|
||||||
dependencies:
|
dependencies:
|
||||||
eslint-import-resolver-node: "*"
|
eslint-import-resolver-node: '*'
|
||||||
next@*:
|
next@*:
|
||||||
dependencies:
|
dependencies:
|
||||||
eslint-import-resolver-node: "*"
|
eslint-import-resolver-node: '*'
|
||||||
react-error-boundary@*:
|
react-error-boundary@*:
|
||||||
dependencies:
|
dependencies:
|
||||||
prop-types: "*"
|
prop-types: '*'
|
||||||
|
|
||||||
yarnPath: .yarn/releases/yarn-4.0.2.cjs
|
yarnPath: .yarn/releases/yarn-4.0.2.cjs
|
||||||
|
@@ -34,6 +34,7 @@ import {
|
|||||||
DOMElement,
|
DOMElement,
|
||||||
DOMRange,
|
DOMRange,
|
||||||
DOMText,
|
DOMText,
|
||||||
|
getActiveElement,
|
||||||
getDefaultView,
|
getDefaultView,
|
||||||
isDOMElement,
|
isDOMElement,
|
||||||
isDOMNode,
|
isDOMNode,
|
||||||
@@ -50,6 +51,7 @@ import {
|
|||||||
IS_WEBKIT,
|
IS_WEBKIT,
|
||||||
IS_UC_MOBILE,
|
IS_UC_MOBILE,
|
||||||
IS_WECHATBROWSER,
|
IS_WECHATBROWSER,
|
||||||
|
IS_SAFARI_LEGACY,
|
||||||
} from '../utils/environment'
|
} from '../utils/environment'
|
||||||
import Hotkeys from '../utils/hotkeys'
|
import Hotkeys from '../utils/hotkeys'
|
||||||
import {
|
import {
|
||||||
@@ -156,6 +158,7 @@ export const Editable = (props: EditableProps) => {
|
|||||||
const [placeholderHeight, setPlaceholderHeight] = useState<
|
const [placeholderHeight, setPlaceholderHeight] = useState<
|
||||||
number | undefined
|
number | undefined
|
||||||
>()
|
>()
|
||||||
|
const processing = useRef(false)
|
||||||
|
|
||||||
const { onUserInput, receivedUserInput } = useTrackUserInput()
|
const { onUserInput, receivedUserInput } = useTrackUserInput()
|
||||||
|
|
||||||
@@ -202,6 +205,29 @@ export const Editable = (props: EditableProps) => {
|
|||||||
const onDOMSelectionChange = useMemo(
|
const onDOMSelectionChange = useMemo(
|
||||||
() =>
|
() =>
|
||||||
throttle(() => {
|
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
|
const androidInputManager = androidInputManagerRef.current
|
||||||
if (
|
if (
|
||||||
(IS_ANDROID || !ReactEditor.isComposing(editor)) &&
|
(IS_ANDROID || !ReactEditor.isComposing(editor)) &&
|
||||||
@@ -471,6 +497,35 @@ export const Editable = (props: EditableProps) => {
|
|||||||
// https://github.com/facebook/react/issues/11211
|
// https://github.com/facebook/react/issues/11211
|
||||||
const onDOMBeforeInput = useCallback(
|
const onDOMBeforeInput = useCallback(
|
||||||
(event: InputEvent) => {
|
(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()
|
onUserInput()
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
@@ -314,3 +314,16 @@ export const isTrackedMutation = (
|
|||||||
// Target add/remove is tracked. Track the mutation if we track the parent mutation.
|
// Target add/remove is tracked. Track the mutation if we track the parent mutation.
|
||||||
return isTrackedMutation(editor, parentMutation, batch)
|
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
|
||||||
|
}
|
||||||
|
@@ -66,6 +66,15 @@ export const CAN_USE_DOM = !!(
|
|||||||
typeof window.document.createElement !== 'undefined'
|
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
|
// COMPAT: Firefox/Edge Legacy don't support the `beforeinput` event
|
||||||
// Chrome Legacy doesn't support `beforeinput` correctly
|
// Chrome Legacy doesn't support `beforeinput` correctly
|
||||||
export const HAS_BEFORE_INPUT_SUPPORT =
|
export const HAS_BEFORE_INPUT_SUPPORT =
|
||||||
|
@@ -12,4 +12,22 @@ test.describe('shadow-dom example', () => {
|
|||||||
|
|
||||||
await expect(innerShadow.getByRole('textbox')).toHaveCount(1)
|
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!')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
Reference in New Issue
Block a user