mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-01-14 21:23:40 +01:00
- Separate selected item from panel open state (closing panel keeps selection) - Scroll selected item into view when detail panel opens - Enter/Space opens detail panel, Escape closes it - Remove browser focus outline on scroll container - Add prefix prop to EventDetailHeader for labels - Make timestamp optional in EventViewerRow - Add close button to EventDetailHeader - Fix title truncation with min-w-0 - Consolidate HttpResponseTimeline title generation - Add ID/event labels to SSE detail header - Remove fake timestamp from SSE events Closes https://feedback.yaak.app/p/feedback-on-sse-viewer-ux-in-yaak
86 lines
2.2 KiB
TypeScript
86 lines
2.2 KiB
TypeScript
import type { Virtualizer } from '@tanstack/react-virtual';
|
|
import { useCallback } from 'react';
|
|
import { useKey } from 'react-use';
|
|
|
|
interface UseEventViewerKeyboardProps {
|
|
totalCount: number;
|
|
activeIndex: number | null;
|
|
setActiveIndex: (index: number | null) => void;
|
|
virtualizer?: Virtualizer<HTMLDivElement, Element> | null;
|
|
isContainerFocused: () => boolean;
|
|
enabled?: boolean;
|
|
closePanel?: () => void;
|
|
openPanel?: () => void;
|
|
}
|
|
|
|
export function useEventViewerKeyboard({
|
|
totalCount,
|
|
activeIndex,
|
|
setActiveIndex,
|
|
virtualizer,
|
|
isContainerFocused,
|
|
enabled = true,
|
|
closePanel,
|
|
openPanel,
|
|
}: UseEventViewerKeyboardProps) {
|
|
const selectPrev = useCallback(() => {
|
|
if (totalCount === 0) return;
|
|
|
|
const newIndex = activeIndex == null ? 0 : Math.max(0, activeIndex - 1);
|
|
setActiveIndex(newIndex);
|
|
virtualizer?.scrollToIndex(newIndex, { align: 'auto' });
|
|
}, [activeIndex, setActiveIndex, totalCount, virtualizer]);
|
|
|
|
const selectNext = useCallback(() => {
|
|
if (totalCount === 0) return;
|
|
|
|
const newIndex = activeIndex == null ? 0 : Math.min(totalCount - 1, activeIndex + 1);
|
|
setActiveIndex(newIndex);
|
|
virtualizer?.scrollToIndex(newIndex, { align: 'auto' });
|
|
}, [activeIndex, setActiveIndex, totalCount, virtualizer]);
|
|
|
|
useKey(
|
|
(e) => e.key === 'ArrowUp' || e.key === 'k',
|
|
(e) => {
|
|
if (!enabled || !isContainerFocused()) return;
|
|
e.preventDefault();
|
|
selectPrev();
|
|
},
|
|
undefined,
|
|
[enabled, isContainerFocused, selectPrev],
|
|
);
|
|
|
|
useKey(
|
|
(e) => e.key === 'ArrowDown' || e.key === 'j',
|
|
(e) => {
|
|
if (!enabled || !isContainerFocused()) return;
|
|
e.preventDefault();
|
|
selectNext();
|
|
},
|
|
undefined,
|
|
[enabled, isContainerFocused, selectNext],
|
|
);
|
|
|
|
useKey(
|
|
(e) => e.key === 'Escape',
|
|
(e) => {
|
|
if (!enabled || !isContainerFocused()) return;
|
|
e.preventDefault();
|
|
closePanel?.();
|
|
},
|
|
undefined,
|
|
[enabled, isContainerFocused, closePanel],
|
|
);
|
|
|
|
useKey(
|
|
(e) => e.key === 'Enter' || e.key === ' ',
|
|
(e) => {
|
|
if (!enabled || !isContainerFocused() || activeIndex == null) return;
|
|
e.preventDefault();
|
|
openPanel?.();
|
|
},
|
|
undefined,
|
|
[enabled, isContainerFocused, activeIndex, openPanel],
|
|
);
|
|
}
|