🚧 Keyboard Handling
Markput provides built-in keyboard support for common editing operations and allows you to add custom keyboard shortcuts. This guide covers everything from basic navigation to advanced keyboard interactions.
Built-in Keyboard Support
Section titled “Built-in Keyboard Support”Markput handles common keyboard operations automatically:
| Key | Action | Context |
|---|---|---|
| Arrow Left/Right | Navigate between marks | When mark is focused |
| Backspace | Delete previous mark | At mark boundary |
| Delete | Delete next mark | At mark boundary |
| Enter | Insert line break | In editor |
| Tab | Focus next mark | When mark is focused |
| Esc | Close overlay | When overlay is open |
| Arrow Up/Down | Navigate suggestions | When overlay is open |
These behaviors work out of the box without any configuration.
Basic Keyboard Events
Section titled “Basic Keyboard Events”Listening to Key Presses
Section titled “Listening to Key Presses”Use slotProps.container to listen to keyboard events on the editor:
import {MarkedInput} from 'rc-marked-input'import {useState} from 'react'
function EditorWithKeyboard() { const [value, setValue] = useState('')
const handleKeyDown = (e: React.KeyboardEvent) => { console.log('Key pressed:', e.key) console.log('Modifier keys:', { ctrl: e.ctrlKey, meta: e.metaKey, shift: e.shiftKey, alt: e.altKey, }) }
return ( <MarkedInput value={value} onChange={setValue} Mark={MyMark} slotProps={{ container: { onKeyDown: handleKeyDown, onKeyUp: e => console.log('Key released:', e.key), onKeyPress: e => console.log('Character:', e.key), }, }} /> )}Key Event Properties
Section titled “Key Event Properties”interface KeyboardEvent { key: string // 'Enter', 'a', 'Backspace', etc. code: string // Physical key: 'KeyA', 'Enter', etc. ctrlKey: boolean // Ctrl pressed (Cmd on Mac) metaKey: boolean // Meta/Cmd key shiftKey: boolean // Shift pressed altKey: boolean // Alt/Option pressed repeat: boolean // Key is being held down}Custom Keyboard Shortcuts
Section titled “Custom Keyboard Shortcuts”Save Shortcut (Ctrl/Cmd+S)
Section titled “Save Shortcut (Ctrl/Cmd+S)”function EditorWithSave() { const [value, setValue] = useState('')
const handleKeyDown = (e: React.KeyboardEvent) => { // Ctrl+S (Windows/Linux) or Cmd+S (Mac) if ((e.ctrlKey || e.metaKey) && e.key === 's') { e.preventDefault() console.log('Saving:', value) // Call your save function here saveToServer(value) } }
return ( <MarkedInput value={value} onChange={setValue} Mark={MyMark} slotProps={{ container: {onKeyDown: handleKeyDown}, }} /> )}Multiple Shortcuts
Section titled “Multiple Shortcuts”function EditorWithShortcuts() { const [value, setValue] = useState('')
const handleKeyDown = (e: React.KeyboardEvent) => { const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0 const modKey = isMac ? e.metaKey : e.ctrlKey
if (modKey) { switch (e.key.toLowerCase()) { case 's': e.preventDefault() console.log('Save') break
case 'b': e.preventDefault() console.log('Bold') insertMarkup('**', '**') break
case 'i': e.preventDefault() console.log('Italic') insertMarkup('*', '*') break
case 'k': e.preventDefault() console.log('Insert link') showLinkDialog() break
case 'z': e.preventDefault() if (e.shiftKey) { console.log('Redo') } else { console.log('Undo') } break } } }
return ( <MarkedInput value={value} onChange={setValue} Mark={MyMark} slotProps={{ container: {onKeyDown: handleKeyDown}, }} /> )}Shortcut Helper
Section titled “Shortcut Helper”Create a reusable shortcut matcher:
type Shortcut = { key: string ctrl?: boolean meta?: boolean shift?: boolean alt?: boolean action: () => void}
function useKeyboardShortcuts(shortcuts: Shortcut[]) { return (e: React.KeyboardEvent) => { for (const shortcut of shortcuts) { const keyMatch = e.key.toLowerCase() === shortcut.key.toLowerCase() const ctrlMatch = shortcut.ctrl ? e.ctrlKey : true const metaMatch = shortcut.meta ? e.metaKey : true const shiftMatch = shortcut.shift ? e.shiftKey : !e.shiftKey const altMatch = shortcut.alt ? e.altKey : !e.altKey
if (keyMatch && ctrlMatch && metaMatch && shiftMatch && altMatch) { e.preventDefault() shortcut.action() break } } }}
// Usagefunction Editor() { const [value, setValue] = useState('')
const handleKeyDown = useKeyboardShortcuts([ {key: 's', ctrl: true, action: () => console.log('Save')}, {key: 'b', ctrl: true, action: () => console.log('Bold')}, {key: 'i', ctrl: true, action: () => console.log('Italic')}, {key: 'z', ctrl: true, shift: true, action: () => console.log('Redo')}, {key: 'z', ctrl: true, action: () => console.log('Undo')}, ])
return ( <MarkedInput value={value} onChange={setValue} Mark={MyMark} slotProps={{ container: {onKeyDown: handleKeyDown}, }} /> )}Mark-Specific Keyboard Events
Section titled “Mark-Specific Keyboard Events”Handling Keys Within Marks
Section titled “Handling Keys Within Marks”Use useMark() to handle keyboard events specific to marks:
import {useMark} from 'rc-marked-input'
function KeyboardAwareMark() { const {label, remove, ref} = useMark()
const handleKeyDown = (e: React.KeyboardEvent) => { switch (e.key) { case 'Backspace': case 'Delete': e.preventDefault() remove() break
case 'Enter': e.preventDefault() console.log('Edit mark:', label) break
case 'Escape': e.preventDefault() // Blur the mark e.currentTarget.blur() break } }
return ( <span ref={ref} tabIndex={0} onKeyDown={handleKeyDown} className="mark"> {label} </span> )}Editable Mark with Enter Key
Section titled “Editable Mark with Enter Key”function EditableMark() { const {label, change, ref} = useMark() const [isEditing, setIsEditing] = useState(false) const [editValue, setEditValue] = useState(label)
const handleKeyDown = (e: React.KeyboardEvent) => { if (isEditing) { if (e.key === 'Enter') { e.preventDefault() change({value: editValue}) setIsEditing(false) } else if (e.key === 'Escape') { e.preventDefault() setEditValue(label) setIsEditing(false) } } else { if (e.key === 'Enter') { e.preventDefault() setIsEditing(true) } else if (e.key === 'Backspace' || e.key === 'Delete') { e.preventDefault() remove() } } }
if (isEditing) { return ( <input value={editValue} onChange={e => setEditValue(e.target.value)} onKeyDown={handleKeyDown} onBlur={() => setIsEditing(false)} autoFocus /> ) }
return ( <span ref={ref} tabIndex={0} onKeyDown={handleKeyDown} className="mark"> {label} </span> )}Navigation Between Marks
Section titled “Navigation Between Marks”Arrow Key Navigation
Section titled “Arrow Key Navigation”Built-in arrow key navigation works when marks have ref and tabIndex:
function NavigableMark() { const {label, ref} = useMark()
return ( <span ref={ref} tabIndex={0} className="mark" style={{ outline: 'none', border: '1px solid transparent', }} onFocus={e => { e.currentTarget.style.border = '1px solid blue' }} onBlur={e => { e.currentTarget.style.border = '1px solid transparent' }} > {label} </span> )}Keyboard behavior:
- Arrow Right - Focus next mark
- Arrow Left - Focus previous mark
- Tab - Focus next mark
- Shift+Tab - Focus previous mark
Custom Navigation Logic
Section titled “Custom Navigation Logic”Override default navigation:
function CustomNavigationMark() { const {label, ref} = useMark()
const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'ArrowRight') { e.preventDefault() // Custom logic: jump to end of editor const editor = e.currentTarget.closest('[contenteditable]') if (editor) { const range = document.createRange() const sel = window.getSelection() range.selectNodeContents(editor) range.collapse(false) sel?.removeAllRanges() sel?.addRange(range) } } }
return ( <span ref={ref} tabIndex={0} onKeyDown={handleKeyDown} className="mark"> {label} </span> )}Overlay Keyboard Interactions
Section titled “Overlay Keyboard Interactions”The overlay handles keyboard events automatically:
| Key | Action |
|---|---|
| Arrow Up | Select previous item |
| Arrow Down | Select next item |
| Enter | Insert selected item |
| Esc | Close overlay |
| Tab | Insert selected item (if configured) |
Custom Overlay Keyboard Behavior
Section titled “Custom Overlay Keyboard Behavior”import {useOverlay} from 'rc-marked-input'
function CustomOverlay() { const {select, close, match} = useOverlay() const [selectedIndex, setSelectedIndex] = useState(0)
const items = ['Alice', 'Bob', 'Charlie']
const handleKeyDown = (e: React.KeyboardEvent) => { switch (e.key) { case 'ArrowDown': e.preventDefault() setSelectedIndex(prev => Math.min(prev + 1, items.length - 1)) break
case 'ArrowUp': e.preventDefault() setSelectedIndex(prev => Math.max(prev - 1, 0)) break
case 'Enter': case 'Tab': e.preventDefault() select({value: items[selectedIndex]}) break
case 'Escape': e.preventDefault() close() break
// Custom: Ctrl+Number for quick selection case '1': case '2': case '3': if (e.ctrlKey) { e.preventDefault() const index = parseInt(e.key) - 1 if (items[index]) { select({value: items[index]}) } } break } }
return ( <div onKeyDown={handleKeyDown} tabIndex={-1} style={{outline: 'none'}}> {items.map((item, index) => ( <div key={item} className={index === selectedIndex ? 'selected' : ''} onClick={() => select({value: item})} > {item} {e.ctrlKey && index < 3 && <span> (Ctrl+{index + 1})</span>} </div> ))} </div> )}Preventing Default Behavior
Section titled “Preventing Default Behavior”When to Prevent Default
Section titled “When to Prevent Default”function Editor() { const handleKeyDown = (e: React.KeyboardEvent) => { // Prevent browser shortcuts if (e.ctrlKey || e.metaKey) { switch (e.key.toLowerCase()) { case 's': // Save case 'b': // Bold case 'i': // Italic case 'u': // Underline case 'k': // Link e.preventDefault() // Your custom logic break } }
// Prevent Enter if you want single-line input if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault() // Handle submission }
// Prevent Tab if you want custom behavior if (e.key === 'Tab') { e.preventDefault() // Custom tab handling } }
return ( <MarkedInput Mark={MyMark} slotProps={{ container: {onKeyDown: handleKeyDown}, }} /> )}Allowing Specific Defaults
Section titled “Allowing Specific Defaults”function SelectivePreventDefault() { const handleKeyDown = (e: React.KeyboardEvent) => { // Only prevent on specific conditions if (e.key === 'Enter') { // Allow Shift+Enter for new lines if (e.shiftKey) { return // Let default behavior happen }
// Prevent plain Enter e.preventDefault() handleSubmit() } }
return ( <MarkedInput Mark={MyMark} slotProps={{ container: {onKeyDown: handleKeyDown}, }} /> )}Focus Management
Section titled “Focus Management”Programmatic Focus
Section titled “Programmatic Focus”Focus the editor programmatically:
function EditorWithFocus() { const editorRef = useRef<HTMLDivElement>(null)
const focusEditor = () => { editorRef.current?.focus() }
return ( <div> <button onClick={focusEditor}>Focus Editor</button> <MarkedInput Mark={MyMark} slotProps={{ container: { ref: editorRef, tabIndex: 0, }, }} /> </div> )}Auto-Focus on Mount
Section titled “Auto-Focus on Mount”function AutoFocusEditor() { const editorRef = useRef<HTMLDivElement>(null)
useEffect(() => { editorRef.current?.focus() }, [])
return ( <MarkedInput Mark={MyMark} slotProps={{ container: { ref: editorRef, autoFocus: true, }, }} /> )}Focus Trap
Section titled “Focus Trap”Keep focus within editor:
function FocusTrapEditor() { const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Tab') { e.preventDefault() // Keep focus in editor, don't tab out } }
return ( <MarkedInput Mark={MyMark} slotProps={{ container: {onKeyDown: handleKeyDown}, }} /> )}Complete Examples
Section titled “Complete Examples”Example 1: Vim-Style Navigation
Section titled “Example 1: Vim-Style Navigation”function VimStyleEditor() { const [value, setValue] = useState('') const [mode, setMode] = useState<'normal' | 'insert'>('insert')
const handleKeyDown = (e: React.KeyboardEvent) => { if (mode === 'normal') { switch (e.key) { case 'i': e.preventDefault() setMode('insert') break
case 'h': // Left e.preventDefault() moveCursor(-1) break
case 'l': // Right e.preventDefault() moveCursor(1) break
case 'x': // Delete e.preventDefault() deleteAtCursor() break
case 'u': // Undo e.preventDefault() undo() break } } else if (mode === 'insert') { if (e.key === 'Escape') { e.preventDefault() setMode('normal') } } }
return ( <div> <div>Mode: {mode.toUpperCase()}</div> <MarkedInput value={value} onChange={setValue} Mark={MyMark} slotProps={{ container: { onKeyDown: handleKeyDown, style: { backgroundColor: mode === 'normal' ? '#ffe0e0' : '#fff', }, }, }} /> </div> )}Example 2: Keyboard Shortcuts Legend
Section titled “Example 2: Keyboard Shortcuts Legend”function EditorWithLegend() { const [value, setValue] = useState('') const [showLegend, setShowLegend] = useState(false)
const shortcuts = [ {keys: 'Ctrl/Cmd + S', action: 'Save'}, {keys: 'Ctrl/Cmd + B', action: 'Bold'}, {keys: 'Ctrl/Cmd + I', action: 'Italic'}, {keys: 'Ctrl/Cmd + K', action: 'Insert Link'}, {keys: 'Ctrl/Cmd + Z', action: 'Undo'}, {keys: 'Ctrl/Cmd + Shift + Z', action: 'Redo'}, {keys: 'Esc', action: 'Close Overlay'}, ]
const handleKeyDown = (e: React.KeyboardEvent) => { const mod = e.ctrlKey || e.metaKey
if (e.key === '?' && e.shiftKey) { e.preventDefault() setShowLegend(!showLegend) return }
if (mod) { switch (e.key.toLowerCase()) { case 's': e.preventDefault() save() break // ... other shortcuts } } }
return ( <div> <MarkedInput value={value} onChange={setValue} Mark={MyMark} slotProps={{ container: {onKeyDown: handleKeyDown}, }} />
{showLegend && ( <div className="keyboard-legend"> <h3>Keyboard Shortcuts</h3> {shortcuts.map(shortcut => ( <div key={shortcut.keys}> <kbd>{shortcut.keys}</kbd> - {shortcut.action} </div> ))} <p>Press ? to toggle this legend</p> </div> )} </div> )}Example 3: Single-Line Input with Enter to Submit
Section titled “Example 3: Single-Line Input with Enter to Submit”function SingleLineInput() { const [value, setValue] = useState('')
const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault() handleSubmit(value) setValue('') // Clear after submit } }
const handleSubmit = (text: string) => { console.log('Submitted:', text) // Your submit logic here }
return ( <div> <MarkedInput value={value} onChange={setValue} Mark={MyMark} slotProps={{ container: { onKeyDown: handleKeyDown, style: { minHeight: 'auto', maxHeight: '40px', }, }, }} /> <small>Press Enter to submit</small> </div> )}Best Practices
Section titled “Best Practices”// Use useCallback for stable event handlersconst handleKeyDown = useCallback( (e: React.KeyboardEvent) => { // Handler logic }, [dependencies])
// Check for both Ctrl and Meta for cross-platform supportif (e.ctrlKey || e.metaKey) { // Shortcut logic}
// Prevent default when handling shortcutsif (e.key === 's' && (e.ctrlKey || e.metaKey)) { e.preventDefault() save()}
// Use lowercase for key comparisonif (e.key.toLowerCase() === 'a') { // Handle 'A' or 'a'}
// Add visual feedback for focused marks;<span ref={ref} tabIndex={0} style={{ outline: 'none', }} onFocus={e => e.currentTarget.classList.add('focused')}> {label}</span>❌ Don’t
Section titled “❌ Don’t”// Don't forget preventDefault for custom shortcutsif (e.key === 's' && e.ctrlKey) { save() // Browser will still open save dialog!}
// Don't hardcode Cmd/Ctrlif (e.metaKey) { // Only works on Mac! // Wrong}
// Don't compare key codes (deprecated)if (e.keyCode === 13) { // Use e.key === 'Enter' instead // Wrong}
// Don't block all keyboard eventse.preventDefault() // Prevents typing!e.stopPropagation() // Breaks event bubbling
// Don't forget accessibility<span onClick={selectMark}> // Missing keyboard support! {label}</span>Accessibility Considerations
Section titled “Accessibility Considerations”Keyboard-Only Navigation
Section titled “Keyboard-Only Navigation”Ensure all functionality is accessible via keyboard:
function AccessibleMark() { const {label, remove, ref} = useMark()
return ( <span ref={ref} tabIndex={0} role="button" aria-label={`Mark: ${label}. Press Delete to remove`} onKeyDown={e => { if (e.key === 'Delete' || e.key === 'Backspace') { e.preventDefault() remove() } }} onClick={e => { e.preventDefault() // Visual selection }} > {label} </span> )}Announce Keyboard Shortcuts
Section titled “Announce Keyboard Shortcuts”function AccessibleEditor() { return ( <div> <div role="region" aria-label="Text editor with mention support" aria-describedby="keyboard-help"> <MarkedInput Mark={MyMark} /> </div>
<div id="keyboard-help" className="sr-only"> Type @ to mention someone. Use arrow keys to navigate. Press Enter to select. Press Escape to cancel. </div> </div> )}TypeScript Types
Section titled “TypeScript Types”import type {KeyboardEvent, KeyboardEventHandler} from 'react'
interface ShortcutConfig { key: string ctrl?: boolean meta?: boolean shift?: boolean alt?: boolean action: () => void description?: string}
type KeyHandler = (e: KeyboardEvent<HTMLElement>) => void
const handleKeyDown: KeyboardEventHandler<HTMLDivElement> = e => { // Type-safe event handler}