Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions e2e/src/react/components/Paragraph.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
import { useNodeViewContext } from '@prosemirror-adapter/react'
import { NodeViewContent, NodeViewRoot, useNodeViewContext } from '@prosemirror-adapter/react'

export function Paragraph() {
const { contentRef, selected } = useNodeViewContext()
return <div style={{ outline: selected ? 'blue solid 1px' : 'none' }} role="presentation" ref={contentRef} />
const shouldMixContentRef =
typeof window !== 'undefined' && new URLSearchParams(window.location.search).has('mixed-content-ref')

return (
<NodeViewRoot
aria-hidden={true}
data-react-node-view-root="true"
role="presentation"
style={{ outline: selected ? 'blue solid 1px' : 'none', width: 10 }}
>
<NodeViewContent className="react-node-view-content" data-react-node-view-content="true" />
{shouldMixContentRef ? <span ref={contentRef} /> : null}
</NodeViewRoot>
)
}
32 changes: 32 additions & 0 deletions e2e/tests/node-view.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,35 @@ testAll(() => {
await expect(h5).toBeVisible()
})
})

testAll(() => {
test('react node view controls root and content DOM attributes', async ({ page }) => {
const root = page.locator('.editor blockquote [data-node-view-root="true"][data-react-node-view-root="true"]')
const content = root.locator('[data-node-view-content="true"][data-react-node-view-content="true"]')
const anchor = root.locator('[data-node-view-content-anchor="true"]')

await expect(root).toBeVisible()
await expect(root).toHaveAttribute('role', 'presentation')
await expect(root).toHaveAttribute('aria-hidden', 'true')
await expect(root).toHaveCSS('width', '10px')
await expect(content).toBeVisible()
await expect(content).toHaveClass(/react-node-view-content/)
await expect(anchor).not.toHaveClass(/react-node-view-content/)
})

test('react node view warns when mixing contentRef and NodeViewContent', async ({ page }) => {
const warnings: string[] = []

page.on('console', (message) => {
if (message.type() === 'warning') {
warnings.push(message.text())
}
})

await page.goto('/stories/react/app/react?mixed-content-ref=true', { waitUntil: 'domcontentloaded' })

await expect
.poll(() => warnings.some((warning) => warning.includes('Do not mix contentRef and NodeViewContent')))
.toBe(true)
})
}, ['react'])
8 changes: 7 additions & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
export { CoreMarkView, type CoreMarkViewSpec, type CoreMarkViewUserOptions, type MarkViewDOMSpec } from './markView'
export { CoreNodeView, type CoreNodeViewSpec, type CoreNodeViewUserOptions, type NodeViewDOMSpec } from './nodeView'
export {
CoreNodeView,
type CoreNodeViewSpec,
type CoreNodeViewUserOptions,
type NodeViewContentMountSource,
type NodeViewDOMSpec,
} from './nodeView'
export {
CorePluginView,
type CorePluginViewSpec,
Expand Down
31 changes: 28 additions & 3 deletions packages/core/src/nodeView/CoreNodeView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ import type { Decoration, DecorationSource, EditorView, NodeView, ViewMutationRe

import { createKey } from '../create-key'

import type { CoreNodeViewSpec, CoreNodeViewUserOptions, NodeViewDOMSpec } from './CoreNodeViewOptions'
import type {
CoreNodeViewSpec,
CoreNodeViewUserOptions,
NodeViewContentMountSource,
NodeViewDOMSpec,
} from './CoreNodeViewOptions'

export class CoreNodeView<ComponentType> implements NodeView {
key: string
Expand All @@ -19,6 +24,8 @@ export class CoreNodeView<ComponentType> implements NodeView {
selected = false
setSelection?: (anchor: number, head: number, root: Document | ShadowRoot) => void
stopEvent?: (event: Event) => boolean
#contentMountSource: NodeViewContentMountSource | null = null
#didWarnContentMountSource = false

#createElement(as?: NodeViewDOMSpec) {
const { node } = this
Expand Down Expand Up @@ -65,12 +72,30 @@ export class CoreNodeView<ComponentType> implements NodeView {
return this.options.component
}

protected contentRef = (element: unknown): void => {
if (element && this.contentDOM && isElementLike(element) && element.firstChild !== this.contentDOM) {
mountContentDOM = (element: HTMLElement | null, source: NodeViewContentMountSource = 'contentRef'): void => {
if (!element || !this.contentDOM) return

if (this.#contentMountSource && this.#contentMountSource !== source && !this.#didWarnContentMountSource) {
this.#didWarnContentMountSource = true
console.warn(
'[prosemirror-adapter] Do not mix contentRef and NodeViewContent in the same node view. ' +
'They both control the same contentDOM mount point.',
)
}

this.#contentMountSource = source

if (element.firstChild !== this.contentDOM) {
element.appendChild(this.contentDOM)
}
}

protected contentRef = (element: unknown): void => {
if (element && isElementLike(element)) {
this.mountContentDOM(element as HTMLElement, 'contentRef')
}
}

selectNode = () => {
this.selected = true
this.options.selectNode?.()
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/nodeView/CoreNodeViewOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import type { Decoration, DecorationSource, EditorView, ViewMutationRecord } fro

export type NodeViewDOMSpec = string | HTMLElement | ((node: Node) => HTMLElement)

export type NodeViewContentMountSource = 'contentRef' | 'component'

export interface CoreNodeViewUserOptions<Component> {
// DOM
as?: NodeViewDOMSpec
Expand Down
7 changes: 6 additions & 1 deletion packages/core/src/nodeView/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
export { CoreNodeView } from './CoreNodeView'
export type { CoreNodeViewSpec, CoreNodeViewUserOptions, NodeViewDOMSpec } from './CoreNodeViewOptions'
export type {
CoreNodeViewSpec,
CoreNodeViewUserOptions,
NodeViewContentMountSource,
NodeViewDOMSpec,
} from './CoreNodeViewOptions'
4 changes: 4 additions & 0 deletions packages/react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,15 @@ export {
nodeViewContext,
ReactNodeView,
AbstractReactNodeView,
NodeViewContent,
NodeViewRoot,
buildReactNodeViewCreator,
useNodeViewContext,
useNodeViewFactory,
type NodeViewContentRef,
type NodeViewContentProps,
type NodeViewContext,
type NodeViewRootProps,
type ReactNodeViewComponent,
type ReactNodeViewSpec,
type ReactNodeViewUserOptions,
Expand Down
Loading
Loading