Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions web/src/components/ui/segmented.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import {cleanup, fireEvent, render, screen} from '@testing-library/react'
import {afterEach, describe, expect, it, vi} from 'vitest'
import {Segmented} from './segmented'
import {TooltipProvider} from './tooltip'

// The Log's show filter has All as the empty value. Radix reports a deselect
// as '', so a click on All was once dropped and the filter stuck on Running.
describe('Segmented', () => {
afterEach(cleanup)

const options = [
{value: '', label: 'All'},
{value: 'running', label: 'Running'},
]

it('selects an option whose value is empty', () => {
const onValueChange = vi.fn()
render(
<TooltipProvider>
<Segmented value="running" onValueChange={onValueChange} options={options} />
</TooltipProvider>,
)
expect(screen.getByRole('radio', {name: 'Running'}).getAttribute('aria-checked')).toBe('true')
fireEvent.click(screen.getByRole('radio', {name: 'All'}))
expect(onValueChange).toHaveBeenCalledWith('')
})

it('keeps the selected option when it is clicked again', () => {
const onValueChange = vi.fn()
render(
<TooltipProvider>
<Segmented value="running" onValueChange={onValueChange} options={options} />
</TooltipProvider>,
)
fireEvent.click(screen.getByRole('radio', {name: 'Running'}))
expect(onValueChange).not.toHaveBeenCalled()
})
})
8 changes: 5 additions & 3 deletions web/src/components/ui/segmented.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export interface SegmentOption<T extends string> {
// All | Running. Clicking the selected option again keeps it selected. The
// chosen option is styled by aria-checked, not data-state: a Tooltip around an
// option puts its own data-state (closed / open) on the same element.
// Values go to Radix with a prefix: it reports a deselect as '', which would
// otherwise swallow a click on an option whose value is '' (the Log's All).
export function Segmented<T extends string>({
value,
onValueChange,
Expand All @@ -35,15 +37,15 @@ export function Segmented<T extends string>({
return (
<ToggleGroup.Root
type="single"
value={value}
value={`v${value}`}
aria-label={ariaLabel}
onValueChange={(v) => v && onValueChange(v as T)}
onValueChange={(v) => v && onValueChange(v.slice(1) as T)}
className={cn('inline-flex shrink-0 items-center rounded-md bg-surface-3/80 p-0.5', className)}
>
{options.map((o) => (
<Tooltip key={o.value} content={o.tip}>
<ToggleGroup.Item
value={o.value}
value={`v${o.value}`}
className={cn(
'inline-flex items-center gap-1.5 rounded-[6px] px-2.5 font-medium text-muted-foreground transition-all outline-none hover:text-foreground focus-visible:ring-2 focus-visible:ring-accent/40 aria-checked:bg-surface aria-checked:text-foreground aria-checked:shadow-sm dark:aria-checked:bg-border-strong [&_svg]:size-3.5',
size === 'sm' ? 'h-6 text-[12px]' : 'h-7 text-[12.5px]',
Expand Down
Loading