CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-mantine--tiptap

Rich text editor React component library based on Tiptap with extensive formatting controls and Mantine integration

Pending
Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Pending

The risk profile of this skill

Overview
Eval results
Files

context-hooks.mddocs/

Context and Hooks

React Context and hooks for accessing editor state, configuration, and building custom controls. The context system enables deep integration with the editor while maintaining consistent behavior across all components.

Capabilities

useRichTextEditorContext Hook

Primary hook for accessing the rich text editor's context, including editor instance, styling functions, and configuration options.

/**
 * Hook for accessing rich text editor context
 * Must be used within a RichTextEditor component tree
 * @returns RichTextEditorContext object with editor state and configuration
 * @throws Error if used outside of RichTextEditor provider
 */
function useRichTextEditorContext(): RichTextEditorContext;

interface RichTextEditorContext {
  /** Current Tiptap editor instance */
  editor: Editor | null;
  /** Mantine styling API for consistent component styling */
  getStyles: GetStylesApi<RichTextEditorFactory>;
  /** Merged accessibility labels for all controls */
  labels: RichTextEditorLabels;
  /** Whether code highlighting styles are enabled */
  withCodeHighlightStyles: boolean | undefined;
  /** Whether typography styles are enabled */
  withTypographyStyles: boolean | undefined;
  /** Current component variant */
  variant: string | undefined;
  /** Whether default styles are disabled */
  unstyled: boolean | undefined;
  /** Callback for source code view toggle events */
  onSourceCodeTextSwitch?: (isSourceCodeModeActive: boolean) => void;
}

Usage Example:

import { useRichTextEditorContext } from "@mantine/tiptap";
import { IconCustomFormat } from "@tabler/icons-react";

function CustomControl() {
  const { editor, labels, getStyles } = useRichTextEditorContext();
  
  const isActive = editor?.isActive('bold') || false;
  const canExecute = editor?.can().toggleBold() || false;
  
  return (
    <button
      {...getStyles('control')}
      disabled={!canExecute}
      onClick={() => editor?.chain().focus().toggleBold().run()}
      aria-label={labels.boldControlLabel}
      data-active={isActive}
    >
      <IconCustomFormat size={16} />
    </button>
  );
}

Context Properties

Editor Instance

Access the current Tiptap editor instance for direct manipulation:

function EditorStateDisplay() {
  const { editor } = useRichTextEditorContext();
  
  if (!editor) {
    return <div>Editor not ready</div>;
  }
  
  const wordCount = editor.storage.characterCount?.words() || 0;
  const canUndo = editor.can().undo();
  const canRedo = editor.can().redo();
  
  return (
    <div>
      <p>Words: {wordCount}</p>
      <p>Can Undo: {canUndo ? 'Yes' : 'No'}</p>
      <p>Can Redo: {canRedo ? 'Yes' : 'No'}</p>
    </div>
  );
}

Styling Integration

Use the styling API to maintain consistency with built-in components:

function StyledCustomControl() {
  const { getStyles } = useRichTextEditorContext();
  
  return (
    <div {...getStyles('controlsGroup')}>
      <button {...getStyles('control')}>
        Custom Control
      </button>
    </div>
  );
}

Labels and Internationalization

Access merged labels for consistent accessibility:

function AccessibleControl() {
  const { labels, editor } = useRichTextEditorContext();
  
  return (
    <button
      aria-label={labels.boldControlLabel}
      title={labels.boldControlLabel}
      onClick={() => editor?.chain().focus().toggleBold().run()}
    >
      B
    </button>
  );
}

Building Custom Controls

Basic Custom Control

import { useRichTextEditorContext } from "@mantine/tiptap";

interface CustomControlProps {
  command: string;
  icon: React.ReactNode;
  label: string;
}

function CustomControl({ command, icon, label }: CustomControlProps) {
  const { editor, getStyles } = useRichTextEditorContext();
  
  const isActive = editor?.isActive(command) || false;
  const canExecute = editor?.can()[`toggle${command}`]?.() || false;
  
  const handleClick = () => {
    editor?.chain().focus()[`toggle${command}`]?.().run();
  };
  
  return (
    <button
      {...getStyles('control')}
      onClick={handleClick}
      disabled={!canExecute}
      aria-label={label}
      data-active={isActive}
    >
      {icon}
    </button>
  );
}

// Usage
<CustomControl 
  command="bold" 
  icon={<IconBold />} 
  label="Toggle Bold" 
/>

Advanced Custom Control with State

import { useState } from "react";
import { useRichTextEditorContext } from "@mantine/tiptap";

function FontSizeControl() {
  const { editor, getStyles } = useRichTextEditorContext();
  const [isOpen, setIsOpen] = useState(false);
  
  const currentSize = editor?.getAttributes('textStyle').fontSize || '16px';
  
  const sizes = ['12px', '14px', '16px', '18px', '20px', '24px'];
  
  const applySize = (size: string) => {
    editor?.chain().focus().setFontSize(size).run();
    setIsOpen(false);
  };
  
  return (
    <div style={{ position: 'relative' }}>
      <button
        {...getStyles('control')}
        onClick={() => setIsOpen(!isOpen)}
        aria-label="Font Size"
      >
        {currentSize}
      </button>
      
      {isOpen && (
        <div style={{ position: 'absolute', top: '100%', background: 'white', border: '1px solid #ccc' }}>
          {sizes.map(size => (
            <button
              key={size}
              onClick={() => applySize(size)}
              style={{ display: 'block', width: '100%', fontSize: size }}
            >
              {size}
            </button>
          ))}
        </div>
      )}
    </div>
  );
}

Building Complex Custom Controls

import { useRichTextEditorContext } from "@mantine/tiptap";
import { IconCustomIcon } from "@tabler/icons-react";

// Build a complex custom control with multiple features
function CustomFormatControl() {
  const { editor, labels, getStyles } = useRichTextEditorContext();
  
  const isActive = editor?.isActive('customFormat') || false;
  const canExecute = editor?.can().toggleCustomFormat?.() || false;
  
  const handleClick = () => {
    editor?.chain().focus().toggleCustomFormat().run();
  };
  
  return (
    <button
      {...getStyles('control')}
      onClick={handleClick}
      disabled={!canExecute}
      aria-label="Apply Custom Format"
      data-active={isActive}
    >
      <IconCustomIcon size={16} />
    </button>
  );
}

// Usage
<RichTextEditor editor={editor}>
  <RichTextEditor.Toolbar>
    <RichTextEditor.ControlsGroup>
      <CustomFormatControl />
    </RichTextEditor.ControlsGroup>
  </RichTextEditor.Toolbar>
  <RichTextEditor.Content />
</RichTextEditor>

Context Provider

The context is automatically provided by the RichTextEditor component:

// Context is provided automatically
<RichTextEditor editor={editor}>
  {/* All child components have access to context */}
  <RichTextEditor.Toolbar>
    <CustomControl /> {/* Can use useRichTextEditorContext */}
    <RichTextEditor.Bold /> {/* Uses context internally */}
  </RichTextEditor.Toolbar>
  <RichTextEditor.Content />
</RichTextEditor>

Error Handling

The context hook includes built-in error handling:

function SafeCustomControl() {
  try {
    const context = useRichTextEditorContext();
    // Use context safely
    return <button>Custom Control</button>;
  } catch (error) {
    // Handle context not found error
    console.error('RichTextEditor context not found:', error);
    return null;
  }
}

TypeScript Integration

Full TypeScript support with proper type inference:

import type { Editor } from "@tiptap/react";
import type { RichTextEditorContext } from "@mantine/tiptap";

function TypedCustomControl() {
  const context: RichTextEditorContext = useRichTextEditorContext();
  const editor: Editor | null = context.editor;
  
  // TypeScript knows the exact shape of context
  const labels = context.labels; // RichTextEditorLabels
  const getStyles = context.getStyles; // GetStylesApi<RichTextEditorFactory>
  
  return (
    <button onClick={() => editor?.chain().focus().toggleBold().run()}>
      Bold
    </button>
  );
}

Best Practices

Performance Optimization

import { useCallback } from "react";

function OptimizedControl() {
  const { editor } = useRichTextEditorContext();
  
  // Memoize event handlers
  const handleClick = useCallback(() => {
    editor?.chain().focus().toggleBold().run();
  }, [editor]);
  
  return <button onClick={handleClick}>Bold</button>;
}

Conditional Rendering

function ConditionalControl({ showAdvanced }: { showAdvanced: boolean }) {
  const { editor } = useRichTextEditorContext();
  
  if (!editor || !showAdvanced) {
    return null;
  }
  
  return (
    <button onClick={() => editor.chain().focus().toggleCode().run()}>
      Code
    </button>
  );
}

State Synchronization

import { useEffect, useState } from "react";

function EditorStateSync() {
  const { editor } = useRichTextEditorContext();
  const [isBold, setIsBold] = useState(false);
  
  useEffect(() => {
    if (!editor) return;
    
    const updateState = () => {
      setIsBold(editor.isActive('bold'));
    };
    
    // Listen to editor updates
    editor.on('selectionUpdate', updateState);
    editor.on('transaction', updateState);
    
    return () => {
      editor.off('selectionUpdate', updateState);
      editor.off('transaction', updateState);
    };
  }, [editor]);
  
  return <div>Text is {isBold ? 'bold' : 'normal'}</div>;
}

docs

advanced-controls.md

context-hooks.md

core-components.md

extensions.md

index.md

labels-i18n.md

structure-controls.md

text-formatting.md

tile.json