# react-example-virtual-selection-repro

> Reproduces pointer refocus in an editable nested node view.

A shadcn/ui block from the [@prosekit](https://prosekit.dev) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/prosekit/react-example-virtual-selection-repro).

- Page, with a live preview: https://shadcn.exchange/r/prosekit/react-example-virtual-selection-repro
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/prosekit/react-example-virtual-selection-repro-light-64774dec52fcf507.png
- Registry item: https://prosekit.dev/r/react-example-virtual-selection-repro.json
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://prosekit.dev/r/react-example-virtual-selection-repro.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `prosekit`

## Files

### `registry/src/react/examples/virtual-selection-repro/editor.tsx`

```tsx
'use client'

import 'prosekit/basic/style.css'
import 'prosekit/basic/typography.css'
import './style.css'

import { createEditor, type NodeJSON } from 'prosekit/core'
import { ProseKit } from 'prosekit/react'
import { useMemo, useState } from 'react'

import { defineExtension } from './extension.ts'

const defaultContent: NodeJSON = {
  type: 'doc',
  content: [
    {
      type: 'paragraph',
      content: [{ type: 'text', text: 'Select the first paragraph below, then blur the editor.' }],
    },
    {
      type: 'nestedEditable',
      content: [
        {
          type: 'paragraph',
          content: [{ type: 'text', text: 'Selected text inside the nested editable NodeView.' }],
        },
        {
          type: 'paragraph',
          content: [{ type: 'text', text: 'Click this target paragraph after blurring.' }],
        },
      ],
    },
  ],
}

export default function Editor() {
  const [legacy, setLegacy] = useState(false)
  const editor = useMemo(() => {
    return createEditor({ extension: defineExtension(legacy), defaultContent })
  }, [legacy])

  return (
    <ProseKit editor={editor}>
      <div className="box-border h-full w-full min-h-36 overflow-y-hidden overflow-x-hidden rounded-md border border-solid border-gray-200 dark:border-gray-700 shadow-sm flex flex-col bg-[canvas] text-black dark:text-white">
        <p>
          {legacy
            ? 'Legacy mode removes the decoration only on focus. Its failure is browser and DOM-shape dependent.'
            : 'Fixed mode removes the decoration on pointerdown before focus.'}
        </p>
        <button onClick={() => setLegacy(value => !value)} type="button">
          Switch to {legacy ? 'fixed' : 'legacy'} mode
        </button>
        <p>Select the first nested paragraph, click “Blur editor”, then click the target paragraph once.</p>
        <button data-testid="blur-editor" type="button">Blur editor</button>
        <div className="relative w-full flex-1 box-border overflow-y-auto">
          <div ref={editor.mount} className="ProseMirror box-border min-h-full px-[max(4rem,calc(50%-20rem))] py-8 outline-hidden outline-0 [&_span[data-mention=user]]:text-blue-500 [&_span[data-mention=tag]]:text-violet-500"></div>
        </div>
      </div>
    </ProseKit>
  )
}
```

### `registry/src/react/examples/virtual-selection-repro/extension.ts`

```ts
import { defineBasicExtension } from 'prosekit/basic'
import { defineNodeSpec, definePlugin, union } from 'prosekit/core'
import { PluginKey, ProseMirrorPlugin, type Transaction } from 'prosekit/pm/state'
import { Decoration, DecorationSet } from 'prosekit/pm/view'
import { defineReactNodeView } from 'prosekit/react'

import { NestedEditableView } from './nested-editable-view.tsx'

const legacyVirtualSelectionKey = new PluginKey<boolean>('virtual-selection-repro-legacy')

function getLegacyFocusMeta(tr: Transaction): boolean | undefined {
  return tr.getMeta(legacyVirtualSelectionKey) as boolean | undefined
}

function defineLegacyVirtualSelection() {
  return definePlugin(
    new ProseMirrorPlugin<boolean>({
      key: legacyVirtualSelectionKey,
      state: {
        init: () => false,
        apply: (tr, value) => getLegacyFocusMeta(tr) ?? value,
      },
      props: {
        handleDOMEvents: {
          focus: (view) => {
            view.dispatch(view.state.tr.setMeta(legacyVirtualSelectionKey, false))
          },
          blur: (view) => {
            const { dom, root } = view
            const domSelection = 'getSelection' in root ? root.getSelection() : window.getSelection()
            if (domSelection?.anchorNode && dom.contains(domSelection.anchorNode)) {
              domSelection.removeAllRanges()
            }
            view.dispatch(view.state.tr.setMeta(legacyVirtualSelectionKey, true))
          },
        },
        decorations: (state) => {
          const { selection } = state
          if (!legacyVirtualSelectionKey.getState(state) || selection.empty || !selection.visible) {
            return null
          }
          return DecorationSet.create(state.doc, [
            Decoration.inline(selection.from, selection.to, {
              class: 'prosekit-virtual-selection-repro-legacy',
            }),
          ])
        },
      },
    }),
  )
}

export function defineExtension(legacy: boolean) {
  return union(
    defineBasicExtension(),
    ...(legacy ? [defineLegacyVirtualSelection()] : []),
    defineNodeSpec({
      name: 'nestedEditable',
      content: 'block+',
      group: 'block',
      toDOM: () => ['div', 0],
    }),
    defineReactNodeView({
      name: 'nestedEditable',
      component: NestedEditableView,
    }),
  )
}

export type EditorExtension = ReturnType<typeof defineExtension>
```

### `registry/src/react/examples/virtual-selection-repro/index.ts`

```ts
export { default as ExampleEditor } from './editor.tsx'
```

### `registry/src/react/examples/virtual-selection-repro/nested-editable-view.tsx`

```tsx
'use client'

import type { ReactNodeViewProps } from 'prosekit/react'

export function NestedEditableView(props: ReactNodeViewProps) {
  return (
    <div data-testid="nested-editable" style={{ border: '1px solid currentColor', padding: '0.75rem' }}>
      <div contentEditable={false}>
        <button type="button">Non-editable NodeView control</button>
      </div>
      <div ref={props.contentRef}></div>
    </div>
  )
}
```

### `registry/src/react/examples/virtual-selection-repro/style.css`

```css
.prosekit-virtual-selection-repro-legacy {
  background: color-mix(in srgb, red 35%, transparent);
}
```
