-
Notifications
You must be signed in to change notification settings - Fork 171
feat: navigate tab #219
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: navigate tab #219
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,4 +1,5 @@ | ||||||
| import { useMemo } from 'react'; | ||||||
| import { useMemo, useRef, useState, useEffect, useCallback } from 'react'; | ||||||
| import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react'; | ||||||
| import { cn } from '../../lib/utils'; | ||||||
| import { ResultsTable } from './ResultsTable'; | ||||||
| import type { ResultTab } from './types'; | ||||||
|
|
@@ -28,11 +29,41 @@ export function ResultsTabs({ | |||||
| onTabClose, | ||||||
| isLoading, | ||||||
| }: ResultsTabsProps) { | ||||||
| const tabsContainerRef = useRef<HTMLDivElement>(null); | ||||||
| const [canScrollLeft, setCanScrollLeft] = useState(false); | ||||||
| const [canScrollRight, setCanScrollRight] = useState(false); | ||||||
|
|
||||||
| const activeTab = useMemo( | ||||||
| () => tabs.find((tab) => tab.id === activeTabId), | ||||||
| [tabs, activeTabId] | ||||||
| ); | ||||||
|
|
||||||
| const updateScrollButtons = useCallback(() => { | ||||||
| const container = tabsContainerRef.current; | ||||||
| if (!container) return; | ||||||
|
|
||||||
| const { scrollLeft, scrollWidth, clientWidth } = container; | ||||||
| setCanScrollLeft(scrollLeft > 0); | ||||||
| setCanScrollRight(scrollLeft + clientWidth < scrollWidth - 1); | ||||||
| }, []); | ||||||
|
|
||||||
| useEffect(() => { | ||||||
| updateScrollButtons(); | ||||||
| window.addEventListener('resize', updateScrollButtons); | ||||||
| return () => window.removeEventListener('resize', updateScrollButtons); | ||||||
| }, [updateScrollButtons, tabs]); | ||||||
|
|
||||||
| const scroll = (direction: 'left' | 'right') => { | ||||||
| const container = tabsContainerRef.current; | ||||||
| if (!container) return; | ||||||
|
|
||||||
| const scrollAmount = 150; | ||||||
| container.scrollBy({ | ||||||
| left: direction === 'left' ? -scrollAmount : scrollAmount, | ||||||
| behavior: 'smooth', | ||||||
| }); | ||||||
| }; | ||||||
|
Comment on lines
+56
to
+65
|
||||||
|
|
||||||
| // Loading state (no tabs yet) | ||||||
| if (isLoading && tabs.length === 0) { | ||||||
| return ( | ||||||
|
|
@@ -56,44 +87,79 @@ export function ResultsTabs({ | |||||
| return ( | ||||||
| <div className="space-y-2"> | ||||||
| {/* Tab bar */} | ||||||
| <div className="flex items-center gap-1 border-b border-border overflow-x-auto overflow-y-hidden"> | ||||||
| {tabs.map((tab) => ( | ||||||
| <div className="relative flex items-center border-b border-border"> | ||||||
| {/* Left scroll button */} | ||||||
| {canScrollLeft && ( | ||||||
| <button | ||||||
| key={tab.id} | ||||||
| type="button" | ||||||
| onClick={() => onTabSelect(tab.id)} | ||||||
| className={cn( | ||||||
| 'group flex items-center gap-1.5 px-3 py-1.5 text-sm whitespace-nowrap', | ||||||
| 'border-b-2 -mb-px transition-colors cursor-pointer', | ||||||
| tab.id === activeTabId | ||||||
| ? 'border-primary text-foreground' | ||||||
| : 'border-transparent text-muted-foreground hover:text-foreground' | ||||||
| )} | ||||||
| onClick={() => scroll('left')} | ||||||
| className="absolute left-0 z-10 flex items-center justify-center w-6 h-full bg-background hover:bg-muted cursor-pointer" | ||||||
| aria-label="Scroll tabs left" | ||||||
| > | ||||||
| <span>{formatTimestamp(tab.timestamp)}</span> | ||||||
| {tab.error && ( | ||||||
| <span className="w-1.5 h-1.5 rounded-full bg-destructive" aria-label="Error" /> | ||||||
| )} | ||||||
| <span | ||||||
| role="button" | ||||||
| tabIndex={0} | ||||||
| aria-label="Close tab" | ||||||
| onClick={(e) => { | ||||||
| e.stopPropagation(); | ||||||
| onTabClose(tab.id); | ||||||
| }} | ||||||
| onKeyDown={(e) => { | ||||||
| if (e.key === 'Enter' || e.key === ' ') { | ||||||
| <ChevronLeftIcon className="w-4 h-4 text-muted-foreground" /> | ||||||
| </button> | ||||||
|
Comment on lines
61
to
+100
|
||||||
| )} | ||||||
|
|
||||||
| {/* Tabs container */} | ||||||
| <div | ||||||
| ref={tabsContainerRef} | ||||||
| onScroll={updateScrollButtons} | ||||||
| className={cn( | ||||||
| "flex items-center gap-1 overflow-hidden", | ||||||
|
||||||
| "flex items-center gap-1 overflow-hidden", | |
| "flex items-center gap-1 overflow-x-auto overflow-y-hidden", |
Copilot
AI
Dec 26, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The scroll buttons lack keyboard accessibility. The buttons can only be activated by mouse clicks, but users navigating with a keyboard cannot use them effectively. While the buttons have type="button" and aria-label attributes, they should also be properly focusable and respond to keyboard events (Enter and Space keys) to ensure full accessibility compliance.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The magic number 150 for scroll amount lacks context and makes the scrolling behavior difficult to adjust. Consider extracting this to a named constant (e.g., SCROLL_AMOUNT or TAB_SCROLL_DISTANCE) at the top of the component or file to improve code maintainability and make the purpose clearer.