mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-21 17:52:25 +08:00
perf(home): render proxy dropdown options on demand
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable @eslint-react/set-state-in-effect */
|
||||
import {
|
||||
AccessTimeRounded,
|
||||
ArrowDropDown,
|
||||
ChevronRight,
|
||||
NetworkCheckRounded,
|
||||
WifiOff as SignalError,
|
||||
@@ -15,24 +16,22 @@ import {
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
ButtonBase,
|
||||
Chip,
|
||||
ClickAwayListener,
|
||||
FormControl,
|
||||
IconButton,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
Paper,
|
||||
Popper,
|
||||
type PopperProps,
|
||||
Select,
|
||||
Tooltip,
|
||||
Typography,
|
||||
alpha,
|
||||
useTheme,
|
||||
} from '@mui/material'
|
||||
import { useLockFn } from 'ahooks'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useNavigate } from 'react-router'
|
||||
|
||||
@@ -223,34 +222,28 @@ const sortProxyOptions = (
|
||||
|
||||
interface PersistentSelectProps {
|
||||
label: string
|
||||
labelId: string
|
||||
listboxId: string
|
||||
value: string
|
||||
selectedName: string
|
||||
displayValue: React.ReactNode
|
||||
open: boolean
|
||||
disabled: boolean
|
||||
keepOpenRef: React.RefObject<HTMLElement | null>
|
||||
onOpen: () => void
|
||||
onClose: () => void
|
||||
renderValue?: () => React.ReactNode
|
||||
children: React.ReactNode
|
||||
onOpenChange: (open: boolean) => void
|
||||
renderOptions: () => React.ReactNode
|
||||
}
|
||||
|
||||
const PersistentSelect = ({
|
||||
label,
|
||||
labelId,
|
||||
listboxId,
|
||||
value,
|
||||
selectedName,
|
||||
displayValue,
|
||||
open,
|
||||
disabled,
|
||||
keepOpenRef,
|
||||
onOpen,
|
||||
onClose,
|
||||
renderValue,
|
||||
children,
|
||||
onOpenChange,
|
||||
renderOptions,
|
||||
}: PersistentSelectProps) => {
|
||||
const anchorRef = useRef<HTMLDivElement>(null)
|
||||
const id = useId()
|
||||
const labelId = `${id}-label`
|
||||
const valueId = `${id}-value`
|
||||
const listboxId = `${id}-listbox`
|
||||
const anchorRef = useRef<HTMLButtonElement>(null)
|
||||
const listRef = useRef<HTMLUListElement>(null)
|
||||
|
||||
const popperOptions = useMemo<PopperProps['popperOptions']>(
|
||||
@@ -283,17 +276,15 @@ const PersistentSelect = ({
|
||||
|
||||
const closeFromKeyboard = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Tab') {
|
||||
onClose()
|
||||
onOpenChange(false)
|
||||
} else if (event.key === 'Escape') {
|
||||
onClose()
|
||||
anchorRef.current
|
||||
?.querySelector<HTMLElement>('[role="combobox"]')
|
||||
?.focus()
|
||||
onOpenChange(false)
|
||||
anchorRef.current?.focus({ preventScroll: true })
|
||||
}
|
||||
}
|
||||
document.addEventListener('keydown', closeFromKeyboard)
|
||||
return () => document.removeEventListener('keydown', closeFromKeyboard)
|
||||
}, [onClose, open])
|
||||
}, [onOpenChange, open])
|
||||
|
||||
return (
|
||||
<ClickAwayListener
|
||||
@@ -303,31 +294,92 @@ const PersistentSelect = ({
|
||||
if (target instanceof Node && keepOpenRef.current?.contains(target)) {
|
||||
return
|
||||
}
|
||||
onClose()
|
||||
onOpenChange(false)
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<FormControl ref={anchorRef} fullWidth variant="outlined" size="small">
|
||||
<InputLabel id={labelId}>{label}</InputLabel>
|
||||
<Select
|
||||
labelId={labelId}
|
||||
value={value}
|
||||
open={false}
|
||||
onOpen={(event) => {
|
||||
if (open && event.type === 'mousedown') onClose()
|
||||
else onOpen()
|
||||
}}
|
||||
label={label}
|
||||
<Box
|
||||
sx={{
|
||||
position: 'relative',
|
||||
borderRadius: 1,
|
||||
'& fieldset': {
|
||||
borderColor: (theme) =>
|
||||
alpha(theme.palette.text.primary, disabled ? 0.12 : 0.23),
|
||||
},
|
||||
...(!disabled && {
|
||||
'&:hover fieldset': { borderColor: 'text.primary' },
|
||||
'&:focus-within fieldset': {
|
||||
borderColor: 'primary.main',
|
||||
borderWidth: 2,
|
||||
},
|
||||
'&:focus-within legend': { color: 'primary.main' },
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<ButtonBase
|
||||
ref={anchorRef}
|
||||
type="button"
|
||||
role="combobox"
|
||||
aria-labelledby={`${labelId} ${valueId}`}
|
||||
aria-haspopup="listbox"
|
||||
aria-controls={open ? listboxId : undefined}
|
||||
aria-expanded={open}
|
||||
disabled={disabled}
|
||||
renderValue={renderValue}
|
||||
SelectDisplayProps={{
|
||||
'aria-controls': open ? listboxId : undefined,
|
||||
'aria-expanded': open ? 'true' : 'false',
|
||||
onClick={() => onOpenChange(!open)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
||||
event.preventDefault()
|
||||
onOpenChange(true)
|
||||
}
|
||||
}}
|
||||
sx={{
|
||||
width: '100%',
|
||||
height: 40,
|
||||
px: 1.75,
|
||||
borderRadius: 'inherit',
|
||||
justifyContent: 'space-between',
|
||||
textAlign: 'left',
|
||||
color: disabled ? 'text.disabled' : 'text.primary',
|
||||
}}
|
||||
>
|
||||
<MenuItem value={value}>{selectedName}</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<Box id={valueId} sx={{ minWidth: 0, flex: 1 }}>
|
||||
{displayValue}
|
||||
</Box>
|
||||
<ArrowDropDown
|
||||
sx={{
|
||||
ml: 1,
|
||||
color: disabled ? 'action.disabled' : 'action.active',
|
||||
transform: open ? 'rotate(180deg)' : undefined,
|
||||
}}
|
||||
/>
|
||||
</ButtonBase>
|
||||
<Box
|
||||
component="fieldset"
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
inset: '-5px 0 0',
|
||||
m: 0,
|
||||
px: 1,
|
||||
minWidth: 0,
|
||||
border: '1px solid',
|
||||
borderRadius: 'inherit',
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component="legend"
|
||||
id={labelId}
|
||||
sx={{
|
||||
px: 0.5,
|
||||
typography: 'caption',
|
||||
lineHeight: '11px',
|
||||
color: disabled ? 'text.disabled' : 'text.secondary',
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Popper
|
||||
open={open}
|
||||
@@ -354,7 +406,7 @@ const PersistentSelect = ({
|
||||
aria-labelledby={labelId}
|
||||
variant="selectedMenu"
|
||||
>
|
||||
{children}
|
||||
{open && renderOptions()}
|
||||
</MenuList>
|
||||
</Paper>
|
||||
</Popper>
|
||||
@@ -364,10 +416,9 @@ const PersistentSelect = ({
|
||||
}
|
||||
|
||||
interface PersistentProxySelectProps
|
||||
extends Omit<
|
||||
PersistentSelectProps,
|
||||
'labelId' | 'listboxId' | 'renderValue' | 'children'
|
||||
> {
|
||||
extends Omit<PersistentSelectProps, 'displayValue' | 'renderOptions'> {
|
||||
value: string
|
||||
selectedName: string
|
||||
groupName: string
|
||||
fixed?: string
|
||||
selectedDelay: number
|
||||
@@ -390,11 +441,7 @@ const PersistentProxySelect = ({
|
||||
return (
|
||||
<PersistentSelect
|
||||
{...props}
|
||||
labelId="proxy-select-label"
|
||||
listboxId="current-proxy-node-listbox"
|
||||
value={value}
|
||||
selectedName={selectedName}
|
||||
renderValue={() => (
|
||||
displayValue={
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
@@ -421,67 +468,68 @@ const PersistentProxySelect = ({
|
||||
</span>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
>
|
||||
{options.map((option) => {
|
||||
const selected = option.value === value
|
||||
const isFixed = option.name === fixed
|
||||
const delay = option.disabled
|
||||
? -1
|
||||
: delayManager.getDelayFix(option.member, groupName)
|
||||
}
|
||||
renderOptions={() =>
|
||||
options.map((option) => {
|
||||
const selected = option.value === value
|
||||
const isFixed = option.name === fixed
|
||||
const delay = option.disabled
|
||||
? -1
|
||||
: delayManager.getDelayFix(option.member, groupName)
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
key={option.value}
|
||||
role="option"
|
||||
aria-selected={selected}
|
||||
selected={selected}
|
||||
disabled={option.disabled}
|
||||
onClick={() => {
|
||||
if (!option.disabled) onChange(option.value)
|
||||
}}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
pr: 1,
|
||||
}}
|
||||
>
|
||||
{isFixed && (
|
||||
<span
|
||||
style={{
|
||||
position: 'absolute',
|
||||
fontSize: '12px',
|
||||
top: '-5px',
|
||||
right: '5px',
|
||||
...(!fixedProxyInUsed && {
|
||||
filter: 'grayscale(1)',
|
||||
}),
|
||||
}}
|
||||
>
|
||||
📌
|
||||
</span>
|
||||
)}
|
||||
<Typography noWrap sx={{ flex: 1, mr: 1 }}>
|
||||
{option.name}
|
||||
</Typography>
|
||||
{!option.disabled && (
|
||||
<Chip
|
||||
size="small"
|
||||
label={delayManager.formatDelay(delay)}
|
||||
color={convertDelayColor(delay)}
|
||||
sx={{
|
||||
minWidth: '60px',
|
||||
height: '22px',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</MenuItem>
|
||||
)
|
||||
})}
|
||||
</PersistentSelect>
|
||||
return (
|
||||
<MenuItem
|
||||
key={option.value}
|
||||
role="option"
|
||||
aria-selected={selected}
|
||||
selected={selected}
|
||||
disabled={option.disabled}
|
||||
onClick={() => {
|
||||
if (!option.disabled) onChange(option.value)
|
||||
}}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
pr: 1,
|
||||
}}
|
||||
>
|
||||
{isFixed && (
|
||||
<span
|
||||
style={{
|
||||
position: 'absolute',
|
||||
fontSize: '12px',
|
||||
top: '-5px',
|
||||
right: '5px',
|
||||
...(!fixedProxyInUsed && {
|
||||
filter: 'grayscale(1)',
|
||||
}),
|
||||
}}
|
||||
>
|
||||
📌
|
||||
</span>
|
||||
)}
|
||||
<Typography noWrap sx={{ flex: 1, mr: 1 }}>
|
||||
{option.name}
|
||||
</Typography>
|
||||
{!option.disabled && (
|
||||
<Chip
|
||||
size="small"
|
||||
label={delayManager.formatDelay(delay)}
|
||||
color={convertDelayColor(delay)}
|
||||
sx={{
|
||||
minWidth: '60px',
|
||||
height: '22px',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</MenuItem>
|
||||
)
|
||||
})
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -863,7 +911,7 @@ export const CurrentProxyCard = () => {
|
||||
|
||||
const proxyOptions = useMemo(
|
||||
() =>
|
||||
isDirectMode
|
||||
isDirectMode || openSelect !== 'proxy'
|
||||
? []
|
||||
: sortProxyOptions(
|
||||
unsortedProxyOptions,
|
||||
@@ -874,6 +922,7 @@ export const CurrentProxyCard = () => {
|
||||
[
|
||||
delays,
|
||||
isDirectMode,
|
||||
openSelect,
|
||||
unsortedProxyOptions,
|
||||
sortType,
|
||||
defaultLatencyTimeout,
|
||||
@@ -1039,28 +1088,25 @@ export const CurrentProxyCard = () => {
|
||||
<Box sx={{ mb: 1.5 }}>
|
||||
<PersistentSelect
|
||||
label={t('home.components.currentProxy.labels.group')}
|
||||
labelId="proxy-group-select-label"
|
||||
listboxId="current-proxy-group-listbox"
|
||||
value={selectedGroupName}
|
||||
selectedName={selectedGroupName}
|
||||
displayValue={<Typography noWrap>{selectedGroupName}</Typography>}
|
||||
open={openSelect === 'group'}
|
||||
disabled={isGlobalMode || isDirectMode}
|
||||
keepOpenRef={delayButtonRef}
|
||||
onOpen={() => setOpenSelect('group')}
|
||||
onClose={() => setOpenSelect(null)}
|
||||
>
|
||||
{selectableGroups.map((group) => (
|
||||
<MenuItem
|
||||
key={group.name}
|
||||
role="option"
|
||||
aria-selected={group.name === selectedGroupName}
|
||||
selected={group.name === selectedGroupName}
|
||||
onClick={() => handleGroupChange(group.name)}
|
||||
>
|
||||
<Typography noWrap>{group.name}</Typography>
|
||||
</MenuItem>
|
||||
))}
|
||||
</PersistentSelect>
|
||||
onOpenChange={(open) => setOpenSelect(open ? 'group' : null)}
|
||||
renderOptions={() =>
|
||||
selectableGroups.map((group) => (
|
||||
<MenuItem
|
||||
key={group.name}
|
||||
role="option"
|
||||
aria-selected={group.name === selectedGroupName}
|
||||
selected={group.name === selectedGroupName}
|
||||
onClick={() => handleGroupChange(group.name)}
|
||||
>
|
||||
<Typography noWrap>{group.name}</Typography>
|
||||
</MenuItem>
|
||||
))
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<PersistentProxySelect
|
||||
@@ -1074,12 +1120,11 @@ export const CurrentProxyCard = () => {
|
||||
}
|
||||
selectedName={selectedProxyName}
|
||||
selectedDelay={currentDelay}
|
||||
options={isDirectMode ? [] : proxyOptions}
|
||||
options={proxyOptions}
|
||||
open={openSelect === 'proxy'}
|
||||
disabled={isDirectMode}
|
||||
keepOpenRef={delayButtonRef}
|
||||
onOpen={() => setOpenSelect('proxy')}
|
||||
onClose={() => setOpenSelect(null)}
|
||||
onOpenChange={(open) => setOpenSelect(open ? 'proxy' : null)}
|
||||
onChange={handleProxyChange}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
Reference in New Issue
Block a user