diff --git a/web/app/components/base/segmented-control/index.css b/web/app/components/base/segmented-control/index.css index 45c10389cf..f34a439d51 100644 --- a/web/app/components/base/segmented-control/index.css +++ b/web/app/components/base/segmented-control/index.css @@ -9,7 +9,7 @@ .segmented-control-large { @apply rounded-lg } - + .segmented-control-large.padding, .segmented-control-regular.padding { @apply p-0.5 @@ -52,8 +52,11 @@ } .active { - @apply border-components-segmented-control-item-active-border bg-components-segmented-control-item-active-bg shadow-xs shadow-shadow-shadow-3 - text-text-secondary + @apply border-components-segmented-control-item-active-border bg-components-segmented-control-item-active-bg shadow-xs shadow-shadow-shadow-3 text-text-secondary + } + + .disabled { + @apply cursor-not-allowed text-text-disabled hover:text-text-disabled bg-transparent hover:bg-transparent } .active.accent { diff --git a/web/app/components/base/segmented-control/index.spec.tsx b/web/app/components/base/segmented-control/index.spec.tsx new file mode 100644 index 0000000000..8b4386b9c0 --- /dev/null +++ b/web/app/components/base/segmented-control/index.spec.tsx @@ -0,0 +1,98 @@ +import { fireEvent, render, screen } from '@testing-library/react' +import '@testing-library/jest-dom' +import SegmentedControl from './index' + +describe('SegmentedControl', () => { + const options = [ + { value: 'option1', text: 'Option 1' }, + { value: 'option2', text: 'Option 2' }, + { value: 'option3', text: 'Option 3' }, + ] + + const optionsWithDisabled = [ + { value: 'option1', text: 'Option 1' }, + { value: 'option2', text: 'Option 2', disabled: true }, + { value: 'option3', text: 'Option 3' }, + ] + + const onSelectMock = jest.fn((value: string | number | symbol) => value) + + beforeEach(() => { + onSelectMock.mockClear() + }) + + it('renders all options correctly', () => { + render() + + options.forEach((option) => { + expect(screen.getByText(option.text)).toBeInTheDocument() + }) + + const divider = screen.getByTestId('segmented-control-divider-1') + expect(divider).toBeInTheDocument() + }) + + it('renders with custom activeClassName when provided', () => { + render( + , + ) + + const selectedOption = screen.getByText('Option 1').closest('button') + expect(selectedOption).toHaveClass('custom-active-class') + }) + + it('highlights the selected option', () => { + render() + + const selectedOption = screen.getByText('Option 2').closest('button') + expect(selectedOption).toHaveClass('active') + }) + + it('calls onChange when an option is clicked', () => { + render() + + fireEvent.click(screen.getByText('Option 3')) + expect(onSelectMock).toHaveBeenCalledWith('option3') + }) + + it('does not call onChange when clicking the already selected option', () => { + render() + + fireEvent.click(screen.getByText('Option 1')) + expect(onSelectMock).not.toHaveBeenCalled() + }) + + it('handles disabled state correctly', () => { + render() + + fireEvent.click(screen.getByText('Option 2')) + expect(onSelectMock).not.toHaveBeenCalled() + + const optionElement = screen.getByText('Option 2').closest('button') + expect(optionElement).toHaveAttribute('disabled') + expect(optionElement).toHaveClass('disabled') + + fireEvent.click(screen.getByText('Option 3')) + expect(onSelectMock).toHaveBeenCalledWith('option3') + }) + + it('renders with custom className when provided', () => { + const customClass = 'my-custom-class' + render( + , + ) + + const selectedOption = screen.getByText('Option 1').closest('button')?.closest('div') + expect(selectedOption).toHaveClass(customClass) + }) +}) diff --git a/web/app/components/base/segmented-control/index.tsx b/web/app/components/base/segmented-control/index.tsx index 46d27c2e53..e81886dec9 100644 --- a/web/app/components/base/segmented-control/index.tsx +++ b/web/app/components/base/segmented-control/index.tsx @@ -9,8 +9,9 @@ import './index.css' type SegmentedControlOption = { value: T text?: string - Icon: RemixiconComponentType + Icon?: RemixiconComponentType count?: number + disabled?: boolean } type SegmentedControlProps = { @@ -90,9 +91,9 @@ export const SegmentedControl = ({ activeState, activeClassName, }: SegmentedControlProps -& VariantProps -& VariantProps -& VariantProps) => { + & VariantProps + & VariantProps + & VariantProps) => { const selectedOptionIndex = options.findIndex(option => option.value === value) return ( @@ -101,7 +102,7 @@ export const SegmentedControl = ({ className, )}> {options.map((option, index) => { - const { Icon, text, count } = option + const { Icon, text, count, disabled } = option const isSelected = index === selectedOptionIndex const isNextSelected = index === selectedOptionIndex - 1 const isLast = index === options.length - 1 @@ -113,10 +114,15 @@ export const SegmentedControl = ({ isSelected ? 'active' : 'default', SegmentedControlItemVariants({ size, activeState: isSelected ? activeState : 'default' }), isSelected && activeClassName, + disabled && 'disabled', )} - onClick={() => onChange(option.value)} + onClick={() => { + if (!isSelected) + onChange(option.value) + }} + disabled={disabled} > - + {Icon && } {text && (
{text} @@ -128,7 +134,7 @@ export const SegmentedControl = ({
)} {!isLast && !isSelected && !isNextSelected && ( -
+
)}