fix(ui): improve chat navigation buttons UX and fix scroll interference
This commit enhances the chat navigation buttons with a more intelligent visibility system that prevents interference with scrolling while maintaining easy access to navigation controls. Key improvements: - Replace static trigger area with dynamic cursor position tracking to allow unobstructed scrolling - Show navigation buttons only when cursor is near the button area or when actively interacting with them - Add throttled mouse position detection (50ms) for better performance - Use passive scroll event listeners for smoother scrolling - Implement smarter auto-hide behavior with 1.5s timeout when cursor leaves the button area This change resolves the issue where navigation buttons would interfere with scrolling when the cursor was in the detection area, creating a more seamless user experience.
This commit is contained in:
parent
145be1fd87
commit
866ce86cc0
@ -1,7 +1,7 @@
|
||||
import { DownOutlined, UpOutlined } from '@ant-design/icons'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { Button, Tooltip } from 'antd'
|
||||
import { FC, useCallback, useEffect, useState } from 'react'
|
||||
import { FC, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
@ -12,22 +12,53 @@ interface ChatNavigationProps {
|
||||
const ChatNavigation: FC<ChatNavigationProps> = ({ containerId }) => {
|
||||
const { t } = useTranslation()
|
||||
const [isVisible, setIsVisible] = useState(false)
|
||||
const [isNearButtons, setIsNearButtons] = useState(false)
|
||||
const [hideTimer, setHideTimer] = useState<NodeJS.Timeout | null>(null)
|
||||
const lastMoveTime = useRef(0)
|
||||
const { topicPosition, showTopics } = useSettings()
|
||||
const showRightTopics = topicPosition === 'right' && showTopics
|
||||
const right = showRightTopics ? 'calc(var(--topic-list-width) + 16px)' : '16px'
|
||||
|
||||
// Reset hide timer and make buttons visible
|
||||
const resetHideTimer = useCallback(() => {
|
||||
if (hideTimer) {
|
||||
clearTimeout(hideTimer)
|
||||
}
|
||||
|
||||
setIsVisible(true)
|
||||
|
||||
// Only set a hide timer if cursor is not near the buttons
|
||||
if (!isNearButtons) {
|
||||
const timer = setTimeout(() => {
|
||||
setIsVisible(false)
|
||||
}, 1000)
|
||||
}, 1500)
|
||||
setHideTimer(timer)
|
||||
}
|
||||
}, [hideTimer, isNearButtons])
|
||||
|
||||
// Handle mouse entering button area
|
||||
const handleMouseEnter = useCallback(() => {
|
||||
setIsNearButtons(true)
|
||||
setIsVisible(true)
|
||||
|
||||
// Clear any existing hide timer
|
||||
if (hideTimer) {
|
||||
clearTimeout(hideTimer)
|
||||
setHideTimer(null)
|
||||
}
|
||||
}, [hideTimer])
|
||||
|
||||
// Handle mouse leaving button area
|
||||
const handleMouseLeave = useCallback(() => {
|
||||
setIsNearButtons(false)
|
||||
|
||||
// Set a timer to hide the buttons
|
||||
const timer = setTimeout(() => {
|
||||
setIsVisible(false)
|
||||
}, 1500)
|
||||
setHideTimer(timer)
|
||||
}, [])
|
||||
|
||||
const findUserMessages = () => {
|
||||
const container = document.getElementById(containerId)
|
||||
if (!container) return []
|
||||
@ -155,28 +186,84 @@ const ChatNavigation: FC<ChatNavigationProps> = ({ containerId }) => {
|
||||
scrollToMessage(userMessages[targetIndex])
|
||||
}
|
||||
|
||||
// Set up scroll event listener and mouse position tracking
|
||||
useEffect(() => {
|
||||
const container = document.getElementById(containerId)
|
||||
if (!container) return
|
||||
|
||||
// Handle scroll events on the container
|
||||
const handleScroll = () => {
|
||||
setIsVisible(true)
|
||||
// Only show buttons when scrolling if cursor is near the button area
|
||||
if (isNearButtons) {
|
||||
resetHideTimer()
|
||||
}
|
||||
}
|
||||
|
||||
// Throttled mouse move handler to improve performance
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
// Throttle mouse move to every 50ms for performance
|
||||
const now = Date.now()
|
||||
if (now - lastMoveTime.current < 50) return
|
||||
lastMoveTime.current = now
|
||||
|
||||
// Calculate if the mouse is in the trigger area
|
||||
const triggerWidth = 80 // Same as the width in styled component
|
||||
|
||||
// Safe way to calculate position when using calc expressions
|
||||
let rightOffset = 16 // Default right offset
|
||||
if (showRightTopics) {
|
||||
// When topics are shown on right, we need to account for topic list width
|
||||
rightOffset = 16 + 300 // Assuming topic list width is 300px, adjust if different
|
||||
}
|
||||
|
||||
const rightPosition = window.innerWidth - rightOffset - triggerWidth
|
||||
const topPosition = window.innerHeight * 0.3 // 30% from top
|
||||
const height = window.innerHeight * 0.4 // 40% of window height
|
||||
|
||||
const isInTriggerArea =
|
||||
e.clientX > rightPosition &&
|
||||
e.clientX < rightPosition + triggerWidth &&
|
||||
e.clientY > topPosition &&
|
||||
e.clientY < topPosition + height
|
||||
|
||||
// Update state based on mouse position
|
||||
if (isInTriggerArea && !isNearButtons) {
|
||||
handleMouseEnter()
|
||||
} else if (!isInTriggerArea && isNearButtons) {
|
||||
// Only trigger mouse leave when not in the navigation area
|
||||
// This ensures we don't leave when hovering over the actual buttons
|
||||
handleMouseLeave()
|
||||
}
|
||||
}
|
||||
|
||||
// Use passive: true for better scroll performance
|
||||
container.addEventListener('scroll', handleScroll, { passive: true })
|
||||
window.addEventListener('mousemove', handleMouseMove)
|
||||
|
||||
container.addEventListener('scroll', handleScroll)
|
||||
return () => {
|
||||
container.removeEventListener('scroll', handleScroll)
|
||||
window.removeEventListener('mousemove', handleMouseMove)
|
||||
if (hideTimer) {
|
||||
clearTimeout(hideTimer)
|
||||
}
|
||||
}
|
||||
}, [containerId, hideTimer, resetHideTimer])
|
||||
}, [
|
||||
containerId,
|
||||
hideTimer,
|
||||
resetHideTimer,
|
||||
isNearButtons,
|
||||
handleMouseEnter,
|
||||
handleMouseLeave,
|
||||
right,
|
||||
showRightTopics
|
||||
])
|
||||
|
||||
return (
|
||||
<>
|
||||
<TriggerArea $right={right} onMouseEnter={() => setIsVisible(true)} onMouseLeave={() => resetHideTimer()} />
|
||||
<NavigationContainer $isVisible={isVisible} $right={right}>
|
||||
<NavigationContainer
|
||||
$isVisible={isVisible}
|
||||
$right={right}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}>
|
||||
<ButtonGroup>
|
||||
<Tooltip title={t('chat.navigation.prev')} placement="left">
|
||||
<NavigationButton
|
||||
@ -184,7 +271,6 @@ const ChatNavigation: FC<ChatNavigationProps> = ({ containerId }) => {
|
||||
icon={<UpOutlined />}
|
||||
onClick={handlePrevMessage}
|
||||
aria-label={t('chat.navigation.prev')}
|
||||
onMouseLeave={() => resetHideTimer()}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Divider />
|
||||
@ -194,24 +280,13 @@ const ChatNavigation: FC<ChatNavigationProps> = ({ containerId }) => {
|
||||
icon={<DownOutlined />}
|
||||
onClick={handleNextMessage}
|
||||
aria-label={t('chat.navigation.next')}
|
||||
onMouseLeave={() => resetHideTimer()}
|
||||
/>
|
||||
</Tooltip>
|
||||
</ButtonGroup>
|
||||
</NavigationContainer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const TriggerArea = styled.div<{ $right: string }>`
|
||||
position: fixed;
|
||||
right: ${(props) => props.$right};
|
||||
top: 40%;
|
||||
width: 20px;
|
||||
height: 20%;
|
||||
z-index: 998;
|
||||
`
|
||||
|
||||
interface NavigationContainerProps {
|
||||
$isVisible: boolean
|
||||
$right: string
|
||||
@ -228,12 +303,6 @@ const NavigationContainer = styled.div<NavigationContainerProps>`
|
||||
transform 0.3s ease-in-out,
|
||||
opacity 0.3s ease-in-out;
|
||||
pointer-events: ${(props) => (props.$isVisible ? 'auto' : 'none')};
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-50%) translateX(0);
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
`
|
||||
|
||||
const ButtonGroup = styled.div`
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user