fix(ui): исправить фокус списка серверов и контраст общих элементов

This commit is contained in:
Denozordec committed 2026-10-15 14:15:56 +07:00
1 parent 9b0d7bb6e8
commit 3c9096804b
21 files changed
+6952 -58

No files matched your search

+34 -6
View File
@@ -35,6 +35,10 @@ import { LayersIcon, SearchIcon, ServerIcon } from "lucide-react"
/** Preview: https://reui.io/preview/base/list-9 · https://reui.io/docs/components/base/icon-tile · https://reui.io/docs/components/base/frame · https://reui.io/docs/components/base/badge */
export const ALL_SERVERS_ID = "all"
// Device URL changes can remount the page. Keep only the current keyboard intent,
// and never restore it after the user has focused another control.
let pendingKeyboardFocus: { path: string; id: string; source: Element | null; until: number } | null = null
export interface ServerTileItem {
id: string
name: string
@@ -185,7 +189,15 @@ function ServerTileRail({
useEffect(() => {
const viewport = listRef.current?.closest<HTMLElement>('[data-slot="scroll-area-viewport"]')
viewport?.scrollTo({ top: 0 })
listRef.current?.querySelector<HTMLElement>('[aria-selected="true"]')?.scrollIntoView({ block: "nearest" })
const selected = listRef.current?.querySelector<HTMLElement>('[aria-selected="true"]')
selected?.scrollIntoView({ block: "nearest" })
const pending = pendingKeyboardFocus
if (pending && (pending.until < performance.now() || pending.path !== window.location.pathname)) {
pendingKeyboardFocus = null
} else if (pending && pending.id === resolvedId && selected?.getClientRects().length) {
if (document.activeElement === document.body || document.activeElement === pending.source) selected.focus({ preventScroll: true })
pendingKeyboardFocus = null
}
}, [pageIndex, resolvedId, q, pageSize])
const visibleIds = new Set(table.getRowModel().rows.map(row => row.id))
const visibleGroups = groups.map(group => ({ ...group, items: group.items.filter(item => visibleIds.has(item.id)) })).filter(group => group.items.length > 0)
@@ -200,6 +212,19 @@ function ServerTileRail({
return ids
}, [filtered, showAllTile])
const tabStopId = activeId && selectableIds.includes(activeId)
? activeId
: showAllTile ? ALL_SERVERS_ID : table.getRowModel().rows.find(row => isSelectable(row.original))?.id
function selectWithKeyboard(id: string) {
if (id === resolvedId) {
listRef.current?.querySelector<HTMLElement>('[aria-selected="true"]')?.focus()
return
}
pendingKeyboardFocus = { path: window.location.pathname, id, source: document.activeElement, until: performance.now() + 10000 }
onSelect(id)
}
function moveSelection(delta: number) {
if (selectableIds.length === 0) return
const idx = selectableIds.indexOf(resolvedId)
@@ -208,7 +233,7 @@ function ServerTileRail({
? delta > 0 ? 0 : selectableIds.length - 1
: Math.min(selectableIds.length - 1, Math.max(0, idx + delta))
const nextId = selectableIds[nextIdx]
if (nextId) onSelect(nextId)
if (nextId) selectWithKeyboard(nextId)
}
function handleListKeyDown(event: KeyboardEvent<HTMLDivElement>) {
@@ -225,13 +250,13 @@ function ServerTileRail({
}
if (event.key === "Home") {
event.preventDefault()
if (selectableIds[0]) onSelect(selectableIds[0])
if (selectableIds[0]) selectWithKeyboard(selectableIds[0])
return
}
if (event.key === "End") {
event.preventDefault()
const last = selectableIds[selectableIds.length - 1]
if (last) onSelect(last)
if (last) selectWithKeyboard(last)
}
}
@@ -283,8 +308,6 @@ function ServerTileRail({
className="flex flex-col gap-0.5 p-1.5"
role="listbox"
aria-label="Серверы"
aria-activedescendant={activeId ? `${railId}-server-tile-${activeId}` : undefined}
tabIndex={0}
onKeyDown={handleListKeyDown}
>
{showAllTile ? (
@@ -292,6 +315,7 @@ function ServerTileRail({
id={`${railId}-server-tile-${ALL_SERVERS_ID}`}
item={allItem}
selected={resolvedId === ALL_SERVERS_ID}
tabStop={tabStopId === ALL_SERVERS_ID}
onSelect={() => onSelect(ALL_SERVERS_ID)}
showCount={showCount}
showType={false}
@@ -315,6 +339,7 @@ function ServerTileRail({
id={`${railId}-server-tile-${item.id}`}
item={item}
selected={resolvedId === item.id}
tabStop={tabStopId === item.id}
onSelect={() => onSelect(item.id)}
showCount={showCount}
showType={showType}
@@ -354,6 +379,7 @@ function ServerTileButton({
id,
item,
selected,
tabStop,
onSelect,
showCount,
showType,
@@ -363,6 +389,7 @@ function ServerTileButton({
id: string
item: ServerTileItem
selected: boolean
tabStop: boolean
onSelect: () => void
showCount: boolean
showType: boolean
@@ -385,6 +412,7 @@ function ServerTileButton({
type="button"
role="option"
aria-selected={selected}
tabIndex={tabStop ? 0 : -1}
title={item.title ?? hostname}
disabled={!selectable}
onClick={onSelect}