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

View File

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

View File

@@ -93,9 +93,8 @@ export const useHighlightInteractions = ({
return () => clearTimeout(timeoutId) return () => clearTimeout(timeoutId)
}, [selectedHighlightId, contentVersion]) }, [selectedHighlightId, contentVersion])
// Handle text selection (works for both mouse and touch) // Shared function to check and handle text selection
const handleSelectionEnd = useCallback(() => { const checkSelection = useCallback(() => {
setTimeout(() => {
const selection = window.getSelection() const selection = window.getSelection()
if (!selection || selection.rangeCount === 0) { if (!selection || selection.rangeCount === 0) {
onClearSelection?.() onClearSelection?.()
@@ -110,9 +109,21 @@ export const useHighlightInteractions = ({
} else { } else {
onClearSelection?.() onClearSelection?.()
} }
}, 10)
}, [onTextSelection, 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 }
} }