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:
parent
d0d4c63649
commit
0bb7be5496
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:
|
||||
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
|
||||
|
@ -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 (
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 =
|
||||
|
@ -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!')
|
||||
})
|
||||
})
|
||||
|
Loading…
x
Reference in New Issue
Block a user