fix(mobile): use selectionchange event for immediate text selection detection

Replace mouseup/touchend handlers with selectionchange event listener
for more reliable mobile text selection detection. This fixes the issue
where the highlight button required an extra tap to become active on
mobile devices.

- Extract selection checking logic into shared checkSelection function
- Use selectionchange event with requestAnimationFrame for immediate detection
- Remove onMouseUp and onTouchEnd props from VideoEmbedProcessor
- Simplify code by eliminating separate mouse/touch event handlers
This commit is contained in:
Gigi
2025-11-05 23:04:38 +01:00
parent 54cba2beed
commit 5389397e9b
3 changed files with 31 additions and 30 deletions

View File

@@ -133,7 +133,7 @@ const ContentPanel: React.FC<ContentPanelProps> = ({
return selectedUrl || `${title || ''}:${(markdown || html || '').length}`
}, [selectedUrl, title, markdown, html])
const { contentRef, handleSelectionEnd } = useHighlightInteractions({
const { contentRef } = useHighlightInteractions({
onHighlightClick,
selectedHighlightId,
onTextSelection,
@@ -815,8 +815,6 @@ const ContentPanel: React.FC<ContentPanelProps> = ({
html={finalHtml}
renderVideoLinksAsEmbeds={settings?.renderVideoLinksAsEmbeds === true}
className="reader-markdown"
onMouseUp={handleSelectionEnd}
onTouchEnd={handleSelectionEnd}
/>
) : (
<div className="reader-markdown">
@@ -830,8 +828,6 @@ const ContentPanel: React.FC<ContentPanelProps> = ({
html={finalHtml || html || ''}
renderVideoLinksAsEmbeds={settings?.renderVideoLinksAsEmbeds === true}
className="reader-html"
onMouseUp={handleSelectionEnd}
onTouchEnd={handleSelectionEnd}
/>
)}

View File

@@ -6,8 +6,6 @@ interface VideoEmbedProcessorProps {
html: string
renderVideoLinksAsEmbeds: boolean
className?: string
onMouseUp?: (e: React.MouseEvent) => void
onTouchEnd?: (e: React.TouchEvent) => void
}
/**
@@ -17,9 +15,7 @@ interface VideoEmbedProcessorProps {
const VideoEmbedProcessor = forwardRef<HTMLDivElement, VideoEmbedProcessorProps>(({
html,
renderVideoLinksAsEmbeds,
className,
onMouseUp,
onTouchEnd
className
}, ref) => {
// Process HTML and extract video URLs in a single pass to keep them in sync
const { processedHtml, videoUrls } = useMemo(() => {
@@ -109,8 +105,6 @@ const VideoEmbedProcessor = forwardRef<HTMLDivElement, VideoEmbedProcessorProps>
ref={ref}
className={className}
dangerouslySetInnerHTML={{ __html: processedHtml }}
onMouseUp={onMouseUp}
onTouchEnd={onTouchEnd}
/>
)
}
@@ -119,7 +113,7 @@ const VideoEmbedProcessor = forwardRef<HTMLDivElement, VideoEmbedProcessorProps>
const parts = processedHtml.split(/(__VIDEO_EMBED_\d+__)/)
return (
<div ref={ref} className={className} onMouseUp={onMouseUp} onTouchEnd={onTouchEnd}>
<div ref={ref} className={className}>
{parts.map((part, index) => {
const videoMatch = part.match(/^__VIDEO_EMBED_(\d+)__$/)
if (videoMatch) {

View File

@@ -93,26 +93,37 @@ export const useHighlightInteractions = ({
return () => clearTimeout(timeoutId)
}, [selectedHighlightId, contentVersion])
// Handle text selection (works for both mouse and touch)
const handleSelectionEnd = useCallback(() => {
setTimeout(() => {
const selection = window.getSelection()
if (!selection || selection.rangeCount === 0) {
onClearSelection?.()
return
}
// Shared function to check and handle text selection
const checkSelection = useCallback(() => {
const selection = window.getSelection()
if (!selection || selection.rangeCount === 0) {
onClearSelection?.()
return
}
const range = selection.getRangeAt(0)
const text = selection.toString().trim()
const range = selection.getRangeAt(0)
const text = selection.toString().trim()
if (text.length > 0 && contentRef.current?.contains(range.commonAncestorContainer)) {
onTextSelection?.(text)
} else {
onClearSelection?.()
}
}, 10)
if (text.length > 0 && contentRef.current?.contains(range.commonAncestorContainer)) {
onTextSelection?.(text)
} else {
onClearSelection?.()
}
}, [onTextSelection, onClearSelection])
return { contentRef, handleSelectionEnd }
// Listen to selectionchange events for immediate detection (works reliably on mobile)
useEffect(() => {
const handleSelectionChange = () => {
// Use requestAnimationFrame to ensure selection is checked after browser updates
requestAnimationFrame(checkSelection)
}
document.addEventListener('selectionchange', handleSelectionChange)
return () => {
document.removeEventListener('selectionchange', handleSelectionChange)
}
}, [checkSelection])
return { contentRef }
}