Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Safari workaround inside shadow DOM. #5648

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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!')
})
})