fix(NodeSwitcher): use ui-select-menu/item classes for consistent styling
Build and Deploy / Build and Push Docker Image (push) Successful in 1m57s
Build and Deploy / Build and Push Docker Image (push) Successful in 1m57s
- Use createPortal to render menu in body - Add ChevronDown icon - Add position calculation for menu placement - Fix ref usage with useRef instead of useState
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { MonitorDot } from 'lucide-react'
|
||||
import { useCallback, useEffect, useState, useRef } from 'react'
|
||||
import { MonitorDot, ChevronDown } from 'lucide-react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import {
|
||||
activeInterconnectNodeId,
|
||||
api,
|
||||
@@ -15,6 +16,8 @@ export function NodeSwitcher() {
|
||||
const [selected, setSelected] = useState(activeInterconnectNodeId || 'local')
|
||||
const [isMaster, setIsMaster] = useState(false)
|
||||
const [open, setOpen] = useState(false)
|
||||
const [menuStyle, setMenuStyle] = useState<React.CSSProperties>({})
|
||||
const buttonRef = useRef<HTMLButtonElement>(null)
|
||||
|
||||
const fetchNodes = useCallback(async () => {
|
||||
try {
|
||||
@@ -93,9 +96,52 @@ export function NodeSwitcher() {
|
||||
|
||||
const currentLabel = allOptions.find((o) => o.value === selected)?.label || '本机节点'
|
||||
|
||||
const updateMenuPosition = () => {
|
||||
const el = buttonRef.current
|
||||
if (!el) return
|
||||
const rect = el.getBoundingClientRect()
|
||||
const viewportH = window.innerHeight
|
||||
const spaceBelow = viewportH - rect.bottom
|
||||
const openUp = spaceBelow < 220 && rect.top > spaceBelow
|
||||
setMenuStyle({
|
||||
position: 'fixed',
|
||||
left: rect.left,
|
||||
width: Math.max(rect.width, 140),
|
||||
zIndex: 80,
|
||||
...(openUp
|
||||
? { bottom: viewportH - rect.top + 6, top: 'auto' }
|
||||
: { top: rect.bottom + 6, bottom: 'auto' }),
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
updateMenuPosition()
|
||||
const onDoc = (e: MouseEvent) => {
|
||||
const t = e.target as Node
|
||||
if (buttonRef.current?.contains(t)) return
|
||||
setOpen(false)
|
||||
}
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') setOpen(false)
|
||||
}
|
||||
const onReposition = () => updateMenuPosition()
|
||||
document.addEventListener('mousedown', onDoc)
|
||||
document.addEventListener('keydown', onKey)
|
||||
window.addEventListener('resize', onReposition)
|
||||
window.addEventListener('scroll', onReposition, true)
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', onDoc)
|
||||
document.removeEventListener('keydown', onKey)
|
||||
window.removeEventListener('resize', onReposition)
|
||||
window.removeEventListener('scroll', onReposition, true)
|
||||
}
|
||||
}, [open])
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<button
|
||||
ref={buttonRef}
|
||||
type="button"
|
||||
onClick={() => setOpen(!open)}
|
||||
className={cn(
|
||||
@@ -107,31 +153,25 @@ export function NodeSwitcher() {
|
||||
>
|
||||
<MonitorDot size={12} className="shrink-0" />
|
||||
<span className="max-w-[100px] truncate">{currentLabel}</span>
|
||||
<ChevronDown size={12} className="shrink-0 opacity-60" />
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<div className="absolute left-0 top-full z-[80] mt-1 min-w-[140px] rounded-md border border-[var(--border)] bg-[var(--bg-secondary)] py-1 shadow-lg">
|
||||
{allOptions.map((opt) => (
|
||||
<button
|
||||
key={opt.value}
|
||||
type="button"
|
||||
onClick={() => handleSelect(opt.value)}
|
||||
className={cn(
|
||||
'w-full px-3 py-1.5 text-left text-xs transition-colors',
|
||||
opt.value === selected
|
||||
? 'bg-[var(--bg-selected)] text-[var(--text-primary)]'
|
||||
: 'text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-primary)]',
|
||||
)}
|
||||
>
|
||||
{opt.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{open && (
|
||||
<div className="fixed inset-0 z-[70]" onClick={() => setOpen(false)} />
|
||||
)}
|
||||
{open &&
|
||||
createPortal(
|
||||
<div className="ui-select-menu" style={menuStyle}>
|
||||
{allOptions.map((opt) => (
|
||||
<button
|
||||
key={opt.value}
|
||||
type="button"
|
||||
onClick={() => handleSelect(opt.value)}
|
||||
className={cn('ui-select-item', opt.value === selected && 'is-active')}
|
||||
>
|
||||
<span className="min-w-0 flex-1 truncate">{opt.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>,
|
||||
document.body,
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user