Skip to content

Commit

Permalink
Merge pull request #421 from bartoval/create_multi_typehead_checkbox
Browse files Browse the repository at this point in the history
feat(Topology): ✨ Add multiTypeheadWithCheckbox
  • Loading branch information
bartoval authored May 21, 2024
2 parents 3ad5a0a + aae472d commit a1884bf
Show file tree
Hide file tree
Showing 26 changed files with 1,057 additions and 418 deletions.
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;
}
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 src/core/SkMultiTypeheadWithCheckbox/__tests__/useData.spec.ts
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);
});
});
121 changes: 121 additions & 0 deletions src/core/SkMultiTypeheadWithCheckbox/index.tsx
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;
Loading

0 comments on commit a1884bf

Please sign in to comment.