feat: Implement smart positioning and dynamic opening for desktop menus, and refine overlay navigation button pointer event handling.

This commit is contained in:
kuekhaoyang
2026-02-04 19:27:29 +08:00
parent cfebce6f56
commit b12bdc06da
3 changed files with 102 additions and 29 deletions
+46 -11
View File
@@ -51,7 +51,7 @@ export function DesktopMoreMenu({
const buttonRef = React.useRef<HTMLButtonElement>(null);
const menuRef = React.useRef<HTMLDivElement>(null);
const [menuPosition, setMenuPosition] = React.useState({ top: 0, left: 0, maxHeight: 'none', openUpward: false });
const [menuPosition, setMenuPosition] = React.useState({ top: 0, left: 0, maxHeight: 'none', openUpward: false, align: 'right' as 'left' | 'right' });
const [isAdFilterOpen, setAdFilterOpen] = React.useState(false);
const AD_FILTER_LABELS: Record<string, string> = {
@@ -86,12 +86,46 @@ export function DesktopMoreMenu({
if (!isRotated) {
// Normal Mode: Non-rotated (Portrait on Mobile)
// Center the menu on screen for better visibility and touch access
// Use Viewport Coordinates but position relative to button (User Request: "Below button")
// And use Body Portal to escape container clipping
const buttonRect = buttonRef.current.getBoundingClientRect();
const viewportHeight = window.innerHeight;
const viewportWidth = window.innerWidth;
const spaceBelow = viewportHeight - buttonRect.bottom - 10;
const spaceAbove = buttonRect.top - 10;
const estimatedMenuHeight = 450;
const actualMenuHeight = menuRef.current?.offsetHeight || estimatedMenuHeight;
const openUpward = spaceBelow < Math.min(actualMenuHeight, 300) && spaceAbove > spaceBelow;
const maxHeight = openUpward
? Math.min(spaceAbove, actualMenuHeight)
: Math.min(spaceBelow, viewportHeight * 0.7);
// Smart Horizontal Alignment:
// If button is on the left half of screen, align menu's left edge to button's left.
// If button is on the right half of screen, align menu's right edge to button's right.
const isLeftHalf = buttonRect.left < viewportWidth / 2;
const align = isLeftHalf ? 'left' : 'right';
let left = isLeftHalf ? buttonRect.left : buttonRect.right;
// Boundary clamping
if (isLeftHalf) {
left = Math.max(left, 10);
} else {
left = Math.min(left, viewportWidth - 10);
}
setMenuPosition({
top: window.innerHeight / 2,
left: window.innerWidth / 2,
maxHeight: '80vh',
openUpward: false
top: openUpward
? buttonRect.top - 10
: buttonRect.bottom + 10,
left: left,
maxHeight: `${maxHeight}px`,
openUpward: openUpward,
align: align
});
} else {
// Rotated Mode: Use Container Coordinates (offset loop) and Portal to Container
@@ -125,14 +159,16 @@ export function DesktopMoreMenu({
top: top - 10,
left: left + buttonWidth,
maxHeight: `${maxHeight}px`,
openUpward: true
openUpward: true,
align: 'right'
});
} else {
setMenuPosition({
top: top + buttonHeight + 10,
left: left + buttonWidth,
maxHeight: `${maxHeight}px`,
openUpward: false
openUpward: false,
align: 'right'
});
}
}
@@ -174,9 +210,8 @@ export function DesktopMoreMenu({
style={{
top: `${menuPosition.top}px`, // Always use absolute top
left: `${menuPosition.left}px`,
// If not rotated (Centered), translate -50% -50% to center
// If rotated (Side), translate -100% 0 (or whatever previous logic was)
transform: !isRotated ? 'translate(-50%, -50%)' : 'translateX(-100%)',
// If aligned right, shift left by 100% of own width. If aligned left, stay at 0.
transform: menuPosition.align === 'right' ? 'translateX(-100%)' : 'none',
maxHeight: menuPosition.maxHeight,
bottom: 'auto'
}}
+4 -4
View File
@@ -142,14 +142,14 @@ export function DesktopOverlay({
<div
className={`absolute left-0 top-0 bottom-0 flex items-center justify-center p-4 md:p-8 transition-opacity duration-300 z-10 ${showNavButtons ? 'opacity-100' : 'opacity-0'
}`}
style={{ pointerEvents: showNavButtons ? 'auto' : 'none' }}
style={{ pointerEvents: 'none' }}
>
<button
onClick={(e) => {
e.stopPropagation();
onSkipBackward();
}}
className="group flex items-center justify-center w-10 h-10 md:w-16 md:h-16 rounded-full bg-black/40 hover:bg-black/60 backdrop-blur-sm transition-all duration-300 hover:scale-110 active:scale-95"
className="group flex items-center justify-center w-10 h-10 md:w-16 md:h-16 rounded-full bg-black/40 hover:bg-black/60 backdrop-blur-sm transition-all duration-300 hover:scale-110 active:scale-95 pointer-events-auto cursor-pointer"
aria-label="后退 10 秒"
>
<Icons.SkipBack className="w-5 h-5 md:w-8 md:h-8 text-white/80 group-hover:text-white" />
@@ -160,14 +160,14 @@ export function DesktopOverlay({
<div
className={`absolute right-0 top-0 bottom-0 flex items-center justify-center p-4 md:p-8 transition-opacity duration-300 z-10 ${showNavButtons ? 'opacity-100' : 'opacity-0'
}`}
style={{ pointerEvents: showNavButtons ? 'auto' : 'none' }}
style={{ pointerEvents: 'none' }}
>
<button
onClick={(e) => {
e.stopPropagation();
onSkipForward();
}}
className="group flex items-center justify-center w-10 h-10 md:w-16 md:h-16 rounded-full bg-black/40 hover:bg-black/60 backdrop-blur-sm transition-all duration-300 hover:scale-110 active:scale-95"
className="group flex items-center justify-center w-10 h-10 md:w-16 md:h-16 rounded-full bg-black/40 hover:bg-black/60 backdrop-blur-sm transition-all duration-300 hover:scale-110 active:scale-95 pointer-events-auto cursor-pointer"
aria-label="前进 10 秒"
>
<Icons.FastForward className="w-5 h-5 md:w-8 md:h-8 text-white/80 group-hover:text-white" />
+52 -14
View File
@@ -26,7 +26,7 @@ export function DesktopSpeedMenu({
}: DesktopSpeedMenuProps) {
const buttonRef = React.useRef<HTMLButtonElement>(null);
const menuRef = React.useRef<HTMLDivElement>(null);
const [menuPosition, setMenuPosition] = React.useState({ top: 0, left: 0, maxHeight: 'none', openUpward: false });
const [menuPosition, setMenuPosition] = React.useState({ top: 0, left: 0, maxHeight: 'none', openUpward: false, align: 'right' as 'left' | 'right' });
const [isFullscreen, setIsFullscreen] = React.useState(false);
@@ -52,13 +52,47 @@ export function DesktopSpeedMenu({
if (!buttonRef.current || !containerRef.current) return;
if (!isRotated) {
// Normal Mode: Non-rotated (Portrait on Mobile)
// Center the menu on screen for better visibility and touch access
// Normal Mode: Non-rotated
// Use Viewport Coordinates but position relative to button (User Request: "Below button")
// And use Body Portal to escape container clipping
const buttonRect = buttonRef.current.getBoundingClientRect();
const viewportHeight = window.innerHeight;
const viewportWidth = window.innerWidth;
const spaceBelow = viewportHeight - buttonRect.bottom - 10;
const spaceAbove = buttonRect.top - 10;
const estimatedMenuHeight = 250;
const actualMenuHeight = menuRef.current?.offsetHeight || estimatedMenuHeight;
const openUpward = spaceBelow < Math.min(actualMenuHeight, 200) && spaceAbove > spaceBelow;
const maxHeight = openUpward
? Math.min(spaceAbove, actualMenuHeight)
: Math.min(spaceBelow, viewportHeight * 0.7);
// Smart Horizontal Alignment:
// If button is on the left half of screen, align menu's left edge to button's left.
// If button is on the right half of screen, align menu's right edge to button's right.
const isLeftHalf = buttonRect.left < viewportWidth / 2;
const align = isLeftHalf ? 'left' : 'right';
let left = isLeftHalf ? buttonRect.left : buttonRect.right;
// Boundary clamping
if (isLeftHalf) {
left = Math.max(left, 10);
} else {
left = Math.min(left, viewportWidth - 10);
}
setMenuPosition({
top: window.innerHeight / 2,
left: window.innerWidth / 2,
maxHeight: '80vh',
openUpward: false
top: openUpward
? buttonRect.top - 10
: buttonRect.bottom + 10,
left: left,
maxHeight: `${maxHeight}px`,
openUpward: openUpward,
align: align
});
} else {
// Rotated Mode: Fullscreen/Landscape forced
@@ -95,14 +129,16 @@ export function DesktopSpeedMenu({
top: top - 10,
left: left + buttonWidth,
maxHeight: `${maxHeight}px`,
openUpward: true
openUpward: true,
align: 'right'
});
} else {
setMenuPosition({
top: top + buttonHeight + 10,
left: left + buttonWidth,
maxHeight: `${maxHeight}px`,
openUpward: false
openUpward: false,
align: 'right'
});
}
}
@@ -142,13 +178,15 @@ export function DesktopSpeedMenu({
ref={menuRef}
className={`absolute z-[2147483647] bg-[var(--glass-bg)] backdrop-blur-[25px] saturate-[180%] rounded-[var(--radius-2xl)] border border-[var(--glass-border)] shadow-[var(--shadow-md)] p-1 sm:p-1.5 w-fit min-w-[3.5rem] sm:min-w-[4.5rem] animate-in fade-in zoom-in-95 duration-200 overflow-y-auto`}
style={{
top: `${menuPosition.top}px`, // Always use absolute top
top: isRotated
? (menuPosition.openUpward ? 'auto' : `${menuPosition.top}px`)
: `${menuPosition.top}px`, // Absolute top for viewport mode
bottom: isRotated
? (menuPosition.openUpward ? `calc(100% - ${menuPosition.top}px + 10px)` : 'auto')
: 'auto', // We calculate Top, so Bottom is auto (or could use max-height)
left: `${menuPosition.left}px`,
// If not rotated (Centered), translate -50% -50% to center
// If rotated (Side), translate -100% 0 (or whatever previous logic was)
transform: !isRotated ? 'translate(-50%, -50%)' : 'translateX(-100%)',
transform: menuPosition.align === 'right' ? 'translateX(-100%)' : 'none',
maxHeight: menuPosition.maxHeight,
bottom: 'auto'
}}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}