-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #421 from bartoval/create_multi_typehead_checkbox
feat(Topology): ✨ Add multiTypeheadWithCheckbox
- Loading branch information
Showing
26 changed files
with
1,057 additions
and
418 deletions.
There are no files selected for viewing
16 changes: 16 additions & 0 deletions
16
src/core/SkMultiTypeheadWithCheckbox/SkMultiTypeheadWithCheckbox.interfaces.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
interface Option { | ||
value: string; | ||
label: string; | ||
isDisabled?: boolean; | ||
} | ||
|
||
export interface UseDataProps { | ||
initIdsSelected: string[]; | ||
initOptions: Option[]; | ||
onSelected?: (items: string[] | undefined) => void; | ||
} | ||
|
||
export interface SkSelectMultiTypeaheadCheckboxProps extends UseDataProps { | ||
initIdsSelected: string[]; | ||
isDisabled?: boolean; | ||
} |
97 changes: 97 additions & 0 deletions
97
src/core/SkMultiTypeheadWithCheckbox/__tests__/SkMultiTypeheadWithCheckbox.spec.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
import { fireEvent, render } from '@testing-library/react'; | ||
|
||
import SkSelectMultiTypeaheadCheckbox from '@core/SkMultiTypeheadWithCheckbox'; | ||
|
||
const initialIdsSelected: string[] = []; | ||
const mockOnSelected = jest.fn(); | ||
const initOptions = [ | ||
{ key: '1', value: '1', label: 'Service 1' }, | ||
{ key: '2', value: '2', label: 'Service 2' }, | ||
{ key: '3', value: '3', label: 'Service 3' } | ||
]; | ||
|
||
describe('SkSelectMultiTypeaheadCheckbox', () => { | ||
it('renders select component with correct props', () => { | ||
const { getByRole, getByPlaceholderText } = render( | ||
<SkSelectMultiTypeaheadCheckbox | ||
initIdsSelected={initialIdsSelected} | ||
initOptions={initOptions} | ||
onSelected={mockOnSelected} | ||
/> | ||
); | ||
|
||
const selectElement = getByRole('button'); | ||
expect(selectElement).toBeInTheDocument(); | ||
|
||
const placeholderTextElement = getByPlaceholderText(`${initialIdsSelected.length} services selected`); | ||
expect(placeholderTextElement).toBeInTheDocument(); | ||
}); | ||
|
||
it('renders options correctly', () => { | ||
const { getByRole, getByText } = render( | ||
<SkSelectMultiTypeaheadCheckbox | ||
initIdsSelected={initialIdsSelected} | ||
initOptions={initOptions} | ||
onSelected={mockOnSelected} | ||
/> | ||
); | ||
|
||
const selectElement = getByRole('button'); | ||
fireEvent.click(selectElement); | ||
|
||
initOptions.forEach((option) => { | ||
const optionElement = getByText(option.label); | ||
expect(optionElement).toBeInTheDocument(); | ||
}); | ||
}); | ||
|
||
it('calls selectService function on selecting an option', () => { | ||
const { getByRole, getByText } = render( | ||
<SkSelectMultiTypeaheadCheckbox | ||
initIdsSelected={initialIdsSelected} | ||
initOptions={initOptions} | ||
onSelected={mockOnSelected} | ||
/> | ||
); | ||
|
||
const selectElement = getByRole('button'); | ||
fireEvent.click(selectElement); | ||
|
||
const optionToSelect = getByText('Service 1'); | ||
fireEvent.click(optionToSelect); | ||
|
||
expect(mockOnSelected).toHaveBeenCalledWith(['1']); | ||
}); | ||
|
||
it('filters options correctly based on search input', () => { | ||
const { getByRole, getByPlaceholderText, queryByText } = render( | ||
<SkSelectMultiTypeaheadCheckbox | ||
initIdsSelected={initialIdsSelected} | ||
initOptions={initOptions} | ||
onSelected={mockOnSelected} | ||
/> | ||
); | ||
|
||
const selectElement = getByRole('button'); | ||
fireEvent.click(selectElement); | ||
|
||
const searchInput = getByPlaceholderText(`${initialIdsSelected.length} services selected`); | ||
fireEvent.change(searchInput, { target: { value: 'Service 1' } }); | ||
|
||
expect(queryByText('Service 1')).toBeInTheDocument(); | ||
expect(queryByText('Service 2')).not.toBeInTheDocument(); | ||
expect(queryByText('Service 3')).not.toBeInTheDocument(); | ||
|
||
fireEvent.change(searchInput, { target: { value: 'Service' } }); | ||
|
||
expect(queryByText('Service 1')).toBeInTheDocument(); | ||
expect(queryByText('Service 2')).toBeInTheDocument(); | ||
expect(queryByText('Service 3')).toBeInTheDocument(); | ||
|
||
fireEvent.change(searchInput, { target: { value: 'xyz' } }); | ||
|
||
expect(queryByText('Service 1')).not.toBeInTheDocument(); | ||
expect(queryByText('Service 2')).not.toBeInTheDocument(); | ||
expect(queryByText('Service 3')).not.toBeInTheDocument(); | ||
}); | ||
}); |
143 changes: 143 additions & 0 deletions
143
src/core/SkMultiTypeheadWithCheckbox/__tests__/useData.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
import { act, renderHook } from '@testing-library/react'; | ||
|
||
import { useData } from '@core/SkMultiTypeheadWithCheckbox/useData'; | ||
|
||
describe('useData', () => { | ||
const mockOnSelected = jest.fn(); | ||
const initOptions = [ | ||
{ key: '1', value: '1', label: 'Service 1' }, | ||
{ key: '2', value: '2', label: 'Service 2' }, | ||
{ key: '3', value: '3', label: 'Service 3' } | ||
]; | ||
|
||
it('should toggle service menu correctly', () => { | ||
const { result } = renderHook(() => useData({ initIdsSelected: [], initOptions, onSelected: mockOnSelected })); | ||
|
||
expect(result.current.isOpen).toBe(false); | ||
act(() => { | ||
result.current.toggleServiceMenu(); | ||
}); | ||
expect(result.current.isOpen).toBe(true); | ||
}); | ||
|
||
it('should select and deselect services correctly', () => { | ||
const { result } = renderHook(() => useData({ initIdsSelected: [], initOptions, onSelected: mockOnSelected })); | ||
|
||
expect(result.current.selected).toEqual([]); | ||
act(() => { | ||
result.current.selectService('1'); | ||
}); | ||
expect(result.current.selected).toEqual(['1']); | ||
|
||
act(() => { | ||
result.current.selectService('1'); | ||
}); | ||
expect(result.current.selected).toEqual([]); | ||
|
||
act(() => { | ||
result.current.selectService('2'); | ||
}); | ||
expect(result.current.selected).toEqual(['2']); | ||
}); | ||
|
||
it('should select all services correctly', () => { | ||
const { result } = renderHook(() => useData({ initIdsSelected: [], initOptions, onSelected: mockOnSelected })); | ||
|
||
expect(result.current.selected).toEqual([]); | ||
act(() => { | ||
result.current.selectAllServices(); | ||
}); | ||
expect(result.current.selected).toEqual(['1', '2', '3']); | ||
|
||
act(() => { | ||
result.current.selectAllServices(); | ||
}); | ||
expect(result.current.selected).toEqual([]); | ||
}); | ||
|
||
it('should handle menu arrow keys (ArrowUp and ArrowDown)', () => { | ||
const { result } = renderHook(() => useData({ initIdsSelected: [], initOptions, onSelected: mockOnSelected })); | ||
|
||
act(() => { | ||
result.current.toggleServiceMenu(); | ||
}); | ||
|
||
// Simulate ArrowDown key press (focus first option) | ||
act(() => { | ||
result.current.handleMenuArrowKeys('ArrowDown'); | ||
}); | ||
|
||
expect(result.current.focusedItemIndex).toBe(0); | ||
expect(result.current.activeItem).toBe('select-multi-typeahead-checkbox-1'); | ||
|
||
// Simulate another ArrowDown key press (focus second enabled option) | ||
act(() => { | ||
result.current.handleMenuArrowKeys('ArrowDown'); | ||
}); | ||
|
||
expect(result.current.focusedItemIndex).toBe(1); | ||
expect(result.current.activeItem).toBe('select-multi-typeahead-checkbox-2'); | ||
|
||
// Simulate ArrowUp key press (wrap around to last option) | ||
act(() => { | ||
result.current.handleMenuArrowKeys('ArrowUp'); | ||
}); | ||
|
||
expect(result.current.focusedItemIndex).toBe(0); | ||
expect(result.current.activeItem).toBe('select-multi-typeahead-checkbox-1'); | ||
}); | ||
|
||
it('should handle input key down events correctly', () => { | ||
const { result } = renderHook(() => useData({ initIdsSelected: [], initOptions, onSelected: mockOnSelected })); | ||
|
||
// Simulate Enter key press (open menu) | ||
act(() => { | ||
result.current.onInputKeyDown({ key: 'Enter' }); | ||
}); | ||
expect(result.current.isOpen).toBe(true); | ||
|
||
// Simulate Tab key press (close menu) | ||
act(() => { | ||
result.current.onInputKeyDown({ key: 'Tab' }); | ||
}); | ||
expect(result.current.isOpen).toBe(false); | ||
|
||
// Simulate Escape key press (close menu) | ||
act(() => { | ||
result.current.onInputKeyDown({ key: 'Escape' }); | ||
}); | ||
expect(result.current.isOpen).toBe(false); | ||
|
||
// Simulate ArrowDown key press (focus first option) | ||
|
||
act(() => { | ||
result.current.toggleServiceMenu(); | ||
}); | ||
|
||
act(() => { | ||
result.current.onInputKeyDown({ key: 'ArrowDown' }); | ||
}); | ||
expect(result.current.focusedItemIndex).toBe(0); | ||
expect(result.current.activeItem).toBe('select-multi-typeahead-checkbox-1'); | ||
|
||
// Simulate ArrowUp key press (wrap around to last option) | ||
act(() => { | ||
result.current.onInputKeyDown({ key: 'ArrowUp' }); | ||
}); | ||
expect(result.current.focusedItemIndex).toBe(2); | ||
expect(result.current.activeItem).toBe('select-multi-typeahead-checkbox-3'); | ||
|
||
// Simulate Enter key press (select focused option) | ||
act(() => { | ||
result.current.onInputKeyDown({ key: 'Enter' }); | ||
}); | ||
expect(result.current.selected).toEqual(['3']); | ||
expect(mockOnSelected).toHaveBeenCalledWith(['3']); | ||
|
||
act(() => { | ||
result.current.onInputKeyDown({ key: 'No existing key' }); | ||
}); | ||
//keep the previous value | ||
expect(result.current.isOpen).toBe(true); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
import { FC, Ref, useEffect, useRef, useState } from 'react'; | ||
|
||
import { | ||
Select, | ||
SelectOption, | ||
SelectList, | ||
MenuToggle, | ||
MenuToggleElement, | ||
TextInputGroup, | ||
TextInputGroupMain, | ||
Button, | ||
MenuFooter | ||
} from '@patternfly/react-core'; | ||
|
||
import { TopologyLabels } from '@pages/Topology/Topology.enum'; | ||
|
||
import { SkSelectMultiTypeaheadCheckboxProps } from './SkMultiTypeheadWithCheckbox.interfaces'; | ||
import { useData } from './useData'; | ||
|
||
const SkSelectMultiTypeaheadCheckbox: FC<SkSelectMultiTypeaheadCheckboxProps> = function ({ | ||
initIdsSelected = [], | ||
initOptions, | ||
isDisabled = false, | ||
onSelected | ||
}) { | ||
const { | ||
inputValue, | ||
selected, | ||
selectOptions, | ||
isOpen, | ||
activeItem, | ||
focusedItemIndex, | ||
toggleServiceMenu, | ||
selectAllServices, | ||
selectService, | ||
onTextInputChange, | ||
onInputKeyDown | ||
} = useData({ | ||
initIdsSelected, | ||
initOptions, | ||
onSelected | ||
}); | ||
|
||
const [placeholder, setPlaceholder] = useState(`${selected.length} services selected`); | ||
const textInputRef = useRef<HTMLInputElement>(); | ||
|
||
const handleSelectService = (item: string) => { | ||
selectService(item); | ||
textInputRef.current?.focus(); | ||
}; | ||
|
||
useEffect(() => { | ||
setPlaceholder(`${selected.length} services selected`); | ||
}, [selected]); | ||
|
||
const toggle = (toggleRef: Ref<MenuToggleElement>) => ( | ||
<MenuToggle | ||
isDisabled={isDisabled} | ||
variant="typeahead" | ||
role="togglebox" | ||
aria-label="Multi typeahead checkbox menu toggle" | ||
onClick={toggleServiceMenu} | ||
innerRef={toggleRef} | ||
isExpanded={isOpen} | ||
isFullWidth | ||
> | ||
<TextInputGroup isPlain> | ||
<TextInputGroupMain | ||
value={inputValue} | ||
onClick={toggleServiceMenu} | ||
onChange={(_, value) => onTextInputChange(value)} | ||
onKeyDown={onInputKeyDown} | ||
id="multi-typeahead-select-checkbox-input" | ||
autoComplete="off" | ||
innerRef={textInputRef} | ||
placeholder={placeholder} | ||
{...(activeItem && { 'aria-activedescendant': activeItem })} | ||
role="combobox" | ||
isExpanded={isOpen} | ||
aria-controls="select-multi-typeahead-checkbox-listbox" | ||
/> | ||
</TextInputGroup> | ||
</MenuToggle> | ||
); | ||
|
||
return ( | ||
<Select | ||
role="menu" | ||
id="multi-typeahead-checkbox-select" | ||
isOpen={isOpen} | ||
selected={selected} | ||
onSelect={(_, selection) => handleSelectService(selection as string)} | ||
// onOpenChange={() => setIsOpen(false)} | ||
toggle={toggle} | ||
> | ||
<SelectList id="select-multi-typeahead-checkbox-listbox"> | ||
{selectOptions.map((option, index) => ( | ||
<SelectOption | ||
{...(!option.isDisabled && { hasCheckbox: true })} | ||
isSelected={selected.includes(option.value)} | ||
key={option.value} | ||
isFocused={focusedItemIndex === index} | ||
className={option.className} | ||
id={`select-multi-typeahead-${option.value.replace(' ', '-')}`} | ||
{...option} | ||
ref={null} | ||
> | ||
{option.label} | ||
</SelectOption> | ||
))} | ||
</SelectList> | ||
<MenuFooter> | ||
<Button variant="link" isInline onClick={selectAllServices}> | ||
{selected.length === selectOptions.length ? TopologyLabels.DeselectAll : TopologyLabels.SelectAll} | ||
</Button> | ||
</MenuFooter> | ||
</Select> | ||
); | ||
}; | ||
|
||
export default SkSelectMultiTypeaheadCheckbox; |
Oops, something went wrong.