-
-
Notifications
You must be signed in to change notification settings - Fork 2.7k
feat(react): introduce <Tiptap /> component for easier integration
#6917
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
Open
bdbch
wants to merge
4
commits into
develop
Choose a base branch
from
refactor/react-setup
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
2419d74
feat(react): introduce `<Tiptap />` component for improved editor int…
bdbch c6717ea
Update packages/react/src/Tiptap.tsx
bdbch 2abe064
feat(react): enhance `<Tiptap />` component with additional context a…
bdbch e17bf4b
Merge branch 'develop' into refactor/react-setup
bdbch File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| --- | ||
| "@tiptap/react": minor | ||
| --- | ||
|
|
||
| Introduce a new, optional React integration that provides a declarative `<Tiptap />` component for setting up editors in React apps. | ||
|
|
||
| Summary | ||
| - Add a new, ergonomic way to initialize and use Tiptap editors in React via `<Tiptap />` components. This is an additive change and does not remove or change existing APIs. | ||
|
|
||
| Why this change | ||
| - Improves ergonomics for React users by offering a component-first API that pairs well with React patterns (hooks, JSX composition and props-driven configuration). | ||
|
|
||
| Migration and usage | ||
| - The old programmatic setup remains supported for this major version — nothing breaks. We encourage consumers to try the new `<Tiptap />` component and migrate when convenient. | ||
|
|
||
| Example | ||
|
|
||
| ```tsx | ||
| import { Tiptap, useEditor } from '@tiptap/react' | ||
|
|
||
| function MyEditor() { | ||
| const editor = useEditor({ extensions: [StarterKit], content: '<h1>Hello from Tiptap</h1>' }) | ||
|
|
||
| return ( | ||
| <Tiptap instance={editor}> | ||
| <Tiptap.Content /> | ||
| <Tiptap.BubbleMenu>My Bubble Menu</Tiptap.BubbleMenu> | ||
| <Tiptap.FloatingMenu>My Floating Menu</Tiptap.FloatingMenu> | ||
| <MenuBar /> {/* MenuBar can use the new `useTiptap` hook to read the editor instance from context */} | ||
| </Tiptap> | ||
| ) | ||
| } | ||
| ``` | ||
|
|
||
| Deprecation plan | ||
| - The old imperative setup will remain fully backward-compatible for this major release. We plan to deprecate (and remove) the legacy setup in the next major version — a deprecation notice and migration guide will be published ahead of that change. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,277 @@ | ||
| import type { ReactNode } from 'react' | ||
| import { createContext, useContext, useEffect, useState } from 'react' | ||
|
|
||
| import type { Editor, EditorContentProps, EditorStateSnapshot } from './index.js' | ||
| import { EditorContent, useEditorState } from './index.js' | ||
| import { type BubbleMenuProps, BubbleMenu } from './menus/BubbleMenu.js' | ||
| import { type FloatingMenuProps, FloatingMenu } from './menus/FloatingMenu.js' | ||
|
|
||
| /** | ||
| * The shape of the React context used by the `<Tiptap />` components. | ||
| * | ||
| * This object exposes the editor instance and a simple readiness flag. | ||
| */ | ||
| export type TiptapContextType = { | ||
| /** The Tiptap editor instance. May be null during SSR or before initialization. */ | ||
| editor: Editor | ||
|
|
||
| /** True when the editor has finished initializing and is ready for user interaction. */ | ||
| isReady: boolean | ||
bdbch marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| /** | ||
| * React context that stores the current editor instance and readiness flag. | ||
| * | ||
| * Use `useTiptap()` to read from this context in child components. | ||
| */ | ||
| export const TiptapContext = createContext<TiptapContextType>({ | ||
| editor: null as unknown as Editor, // placeholder for typing | ||
| isReady: false, | ||
| }) | ||
|
|
||
| /** | ||
| * Hook to read the Tiptap context (`editor` + `isReady`). | ||
| * | ||
| * This is a small convenience wrapper around `useContext(TiptapContext)`. | ||
| * | ||
| * @returns The current `TiptapContextType` value from the provider. | ||
| * | ||
| * @example | ||
| * ```tsx | ||
| * import { useTiptap } from '@tiptap/react' | ||
| * | ||
| * function Status() { | ||
| * const { isReady } = useTiptap() | ||
| * return <div>{isReady ? 'Editor ready' : 'Loading editor...'}</div> | ||
| * } | ||
| * ``` | ||
| */ | ||
| export const useTiptap = () => useContext(TiptapContext) | ||
|
|
||
| /** | ||
| * Select a slice of the editor state using the context-provided editor. | ||
| * | ||
| * This is a thin wrapper around `useEditorState` that reads the `editor` | ||
| * instance from `useTiptap()` so callers don't have to pass it manually. | ||
| * | ||
| * @typeParam TSelectorResult - The type returned by the selector. | ||
| * @param selector - Function that receives the editor state snapshot and | ||
| * returns the piece of state you want to subscribe to. | ||
| * @param equalityFn - Optional function to compare previous/next selected | ||
| * values and avoid unnecessary updates. | ||
| * @returns The selected slice of the editor state. | ||
| * | ||
| * @example | ||
| * ```tsx | ||
| * // Subscribe to the current selection and re-render only when it changes | ||
| * const selection = useTiptapState(state => state.selection) | ||
| * ``` | ||
| */ | ||
| export function useTiptapState<TSelectorResult>( | ||
| selector: (context: EditorStateSnapshot<Editor>) => TSelectorResult, | ||
| equalityFn?: (a: TSelectorResult, b: TSelectorResult | null) => boolean, | ||
| ) { | ||
| const { editor } = useTiptap() | ||
| return useEditorState({ | ||
| editor, | ||
| selector, | ||
| equalityFn, | ||
| }) | ||
| } | ||
|
|
||
| /** | ||
| * Props for the `Tiptap` root/provider component. | ||
| */ | ||
| export type TiptapWrapperProps = { | ||
| /** The editor instance to provide to child components. */ | ||
| instance: Editor | ||
| children: ReactNode | ||
| } | ||
|
|
||
| /** | ||
| * Top-level provider component that makes the editor instance available via | ||
| * React context and tracks when the editor becomes ready. | ||
| * | ||
| * The component listens to the editor's `create` event and flips the | ||
| * `isReady` flag once initialization completes. | ||
| * | ||
| * @param props - Component props. | ||
| * @returns A context provider element wrapping `children`. | ||
| * | ||
| * @example | ||
| * ```tsx | ||
| * import { Tiptap } from '@tiptap/react' | ||
| * | ||
| * function App({ editor }) { | ||
| * return ( | ||
| * <Tiptap instance={editor}> | ||
| * <Toolbar /> | ||
| * <Tiptap.Content /> | ||
| * </Tiptap> | ||
| * ) | ||
| * } | ||
| * ``` | ||
| */ | ||
| export function TiptapWrapper({ instance, children }: TiptapWrapperProps) { | ||
| const [isReady, setIsReady] = useState(instance.isInitialized) | ||
|
|
||
| useEffect(() => { | ||
| const handleCreate = () => { | ||
| setIsReady(true) | ||
| } | ||
| instance.on('create', handleCreate) | ||
|
|
||
| return () => { | ||
| instance.off('create', handleCreate) | ||
| } | ||
| }, [instance]) | ||
|
|
||
| return <TiptapContext.Provider value={{ editor: instance, isReady }}>{children}</TiptapContext.Provider> | ||
| } | ||
|
|
||
| /** | ||
| * Convenience component that renders `EditorContent` using the context-provided | ||
| * editor instance. Use this instead of manually passing the `editor` prop. | ||
| * | ||
| * @param props - All `EditorContent` props except `editor` and `ref`. | ||
| * @returns An `EditorContent` element bound to the context editor. | ||
| * | ||
| * @example | ||
| * ```tsx | ||
| * // inside a Tiptap provider | ||
| * <Tiptap.Content className="editor" /> | ||
| * ``` | ||
| */ | ||
| export function TiptapContent({ ...rest }: Omit<EditorContentProps, 'editor' | 'ref'>) { | ||
| const { editor } = useTiptap() | ||
|
|
||
| return <EditorContent editor={editor} {...rest} /> | ||
| } | ||
|
|
||
| export type TiptapLoadingProps = { | ||
| children: ReactNode | ||
| } | ||
|
|
||
| /** | ||
| * Component that renders its children only when the editor is not ready. | ||
| * @param props The props for the TiptapLoading component. | ||
| * @returns The TiptapLoading component or null if the editor is ready. | ||
| */ | ||
| export function TiptapLoading({ children }: TiptapLoadingProps) { | ||
| const { isReady } = useTiptap() | ||
|
|
||
| if (isReady) { | ||
| return null | ||
| } | ||
|
|
||
| return children | ||
| } | ||
|
|
||
| /** | ||
| * A wrapper around the library `BubbleMenu` that injects the editor from | ||
| * context so callers don't need to pass the `editor` prop. | ||
| * | ||
| * Returns `null` when the editor is not available (for example during SSR). | ||
| * | ||
| * @param props - Props for the underlying `BubbleMenu` (except `editor`). | ||
| * @returns A `BubbleMenu` bound to the context editor, or `null`. | ||
| * | ||
| * @example | ||
| * ```tsx | ||
| * <Tiptap.BubbleMenu tippyOptions={{ duration: 100 }}> | ||
| * <button onClick={() => editor.chain().focus().toggleBold().run()}>Bold</button> | ||
| * </Tiptap.BubbleMenu> | ||
| * ``` | ||
| */ | ||
| export function TiptapBubbleMenu({ children, ...rest }: { children: ReactNode } & Omit<BubbleMenuProps, 'editor'>) { | ||
| const { editor } = useTiptap() | ||
|
|
||
| if (!editor) { | ||
| return null | ||
| } | ||
|
|
||
| return ( | ||
| <BubbleMenu editor={editor} {...rest}> | ||
| {children} | ||
| </BubbleMenu> | ||
| ) | ||
| } | ||
|
|
||
| /** | ||
| * A wrapper around the library `FloatingMenu` that injects the editor from | ||
| * context so callers don't need to pass the `editor` prop. | ||
| * | ||
| * Returns `null` when the editor is not available. | ||
| * | ||
| * @param props - Props for the underlying `FloatingMenu` (except `editor`). | ||
| * @returns A `FloatingMenu` bound to the context editor, or `null`. | ||
| * | ||
| * @example | ||
| * ```tsx | ||
| * <Tiptap.FloatingMenu placement="top"> | ||
| * <button onClick={() => editor.chain().focus().toggleItalic().run()}>Italic</button> | ||
| * </Tiptap.FloatingMenu> | ||
| * ``` | ||
| */ | ||
| export function TiptapFloatingMenu({ children, ...rest }: { children: ReactNode } & Omit<FloatingMenuProps, 'editor'>) { | ||
| const { editor } = useTiptap() | ||
|
|
||
| if (!editor) { | ||
| return null | ||
| } | ||
|
|
||
| return ( | ||
| <FloatingMenu {...rest} editor={editor}> | ||
| {children} | ||
| </FloatingMenu> | ||
| ) | ||
| } | ||
|
|
||
| /** | ||
| * Root `Tiptap` component. Use it as the provider for all child components. | ||
| * | ||
| * The exported object includes several helper subcomponents for common use | ||
| * cases: `Content`, `Loading`, `BubbleMenu`, and `FloatingMenu`. | ||
| * | ||
| * Example | ||
| * ```tsx | ||
| * const editor = useEditor({ extensions: [...] }) | ||
| * | ||
| * return ( | ||
| * <Tiptap instance={editor}> | ||
| * <Tiptap.Loading>Initializing editor…</Tiptap.Loading> | ||
| * <Tiptap.Content /> | ||
| * <Tiptap.BubbleMenu> | ||
| * <button onClick={() => editor.chain().focus().toggleBold().run()}>Bold</button> | ||
| * </Tiptap.BubbleMenu> | ||
| * </Tiptap> | ||
| * ) | ||
| * ``` | ||
| */ | ||
| export const Tiptap = Object.assign(TiptapWrapper, { | ||
| /** | ||
| * The Tiptap Content component that renders the EditorContent with the editor instance from the context. | ||
| * @see TiptapContent | ||
| */ | ||
| Content: TiptapContent, | ||
|
|
||
| /** | ||
| * The Tiptap Loading component that renders its children only when the editor is not ready. | ||
| * @see TiptapLoading | ||
| */ | ||
| Loading: TiptapLoading, | ||
|
|
||
| /** | ||
| * The Tiptap BubbleMenu component that wraps the BubbleMenu from Tiptap and provides the editor instance from the context. | ||
| * @see TiptapBubbleMenu | ||
| */ | ||
| BubbleMenu: TiptapBubbleMenu, | ||
|
|
||
| /** | ||
| * The Tiptap FloatingMenu component that wraps the FloatingMenu from Tiptap and provides the editor instance from the context. | ||
| * @see TiptapFloatingMenu | ||
| */ | ||
| FloatingMenu: TiptapFloatingMenu, | ||
| }) | ||
|
|
||
| export default Tiptap | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.