Skip to content
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(Select): add checkbox variant of the simple select template #10159

Merged
merged 3 commits into from
Mar 28, 2024
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
256 changes: 256 additions & 0 deletions packages/react-templates/src/components/Select/CheckboxSelect.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
import * as React from 'react';
import { render, screen, waitForElementToBeRemoved } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { CheckboxSelect } from './CheckboxSelect';
import styles from '@patternfly/react-styles/css/components/Badge/badge';

test('renders checkbox select with options', async () => {
const initialOptions = [
{ content: 'Option 1', value: 'option1' },
{ content: 'Option 2', value: 'option2' },
{ content: 'Option 3', value: 'option3' }
];

const user = userEvent.setup();

render(<CheckboxSelect initialOptions={initialOptions} />);

const toggle = screen.getByRole('button', { name: 'Filter by status' });

await user.click(toggle);

const option1 = screen.getByRole('checkbox', { name: 'Option 1' });
const option2 = screen.getByRole('checkbox', { name: 'Option 2' });
const option3 = screen.getByRole('checkbox', { name: 'Option 3' });

expect(option1).toBeInTheDocument();
expect(option2).toBeInTheDocument();
expect(option3).toBeInTheDocument();
});

test('selects options when clicked', async () => {
const initialOptions = [
{ content: 'Option 1', value: 'option1' },
{ content: 'Option 2', value: 'option2' },
{ content: 'Option 3', value: 'option3' }
];

const user = userEvent.setup();

render(<CheckboxSelect initialOptions={initialOptions} />);

const toggle = screen.getByRole('button', { name: 'Filter by status' });

await user.click(toggle);

const option1 = screen.getByRole('checkbox', { name: 'Option 1' });

expect(option1).not.toBeChecked();

await user.click(option1);

expect(option1).toBeChecked();
});

test('deselects options when an already selected option is clicked', async () => {
const initialOptions = [
{ content: 'Option 1', value: 'option1' },
{ content: 'Option 2', value: 'option2' },
{ content: 'Option 3', value: 'option3' }
];

const user = userEvent.setup();

render(<CheckboxSelect initialOptions={initialOptions} />);

const toggle = screen.getByRole('button', { name: 'Filter by status' });

await user.click(toggle);

const option1 = screen.getByRole('checkbox', { name: 'Option 1' });

await user.click(option1);
await user.click(option1);

expect(option1).not.toBeChecked();
});

test('calls the onSelect callback with the selected value when an option is selected', async () => {
const initialOptions = [
{ content: 'Option 1', value: 'option1' },
{ content: 'Option 2', value: 'option2' },
{ content: 'Option 3', value: 'option3' }
];

const user = userEvent.setup();
const onSelectMock = jest.fn();

render(<CheckboxSelect initialOptions={initialOptions} onSelect={onSelectMock} />);

const toggle = screen.getByRole('button', { name: 'Filter by status' });

await user.click(toggle);

const option1 = screen.getByRole('checkbox', { name: 'Option 1' });

await user.click(option1);

expect(onSelectMock).toHaveBeenCalledTimes(1);
expect(onSelectMock).toHaveBeenCalledWith(expect.anything(), 'option1');
});

test('does not call the onSelect callback when no options are selected', async () => {
const initialOptions = [
{ content: 'Option 1', value: 'option1' },
{ content: 'Option 2', value: 'option2' },
{ content: 'Option 3', value: 'option3' }
];

const user = userEvent.setup();
const onSelectMock = jest.fn();

render(<CheckboxSelect initialOptions={initialOptions} onSelect={onSelectMock} />);

const toggle = screen.getByRole('button', { name: 'Filter by status' });

await user.click(toggle);

expect(onSelectMock).not.toHaveBeenCalled();
});

test('toggles the select menu when the toggle button is clicked', async () => {
const initialOptions = [
{ content: 'Option 1', value: 'option1' },
{ content: 'Option 2', value: 'option2' },
{ content: 'Option 3', value: 'option3' }
];

const user = userEvent.setup();

render(<CheckboxSelect initialOptions={initialOptions} />);

const toggleButton = screen.getByRole('button', { name: 'Filter by status' });

await user.click(toggleButton);

expect(screen.getByRole('menu')).toBeInTheDocument();

await user.click(toggleButton);

await waitForElementToBeRemoved(() => screen.queryByRole('menu'));

expect(screen.queryByRole('menu')).not.toBeInTheDocument();
});

test('displays custom toggle content', async () => {
const initialOptions = [
{ content: 'Option 1', value: 'option1' },
{ content: 'Option 2', value: 'option2' },
{ content: 'Option 3', value: 'option3' }
];

render(<CheckboxSelect initialOptions={initialOptions} toggleContent="Custom Toggle" />);

const toggleButton = screen.getByRole('button', { name: 'Custom Toggle' });

expect(toggleButton).toBeInTheDocument();
});

test('calls the onToggle callback when the select opens or closes', async () => {
const initialOptions = [
{ content: 'Option 1', value: 'option1' },
{ content: 'Option 2', value: 'option2' },
{ content: 'Option 3', value: 'option3' }
];

const user = userEvent.setup();
const onToggleMock = jest.fn();

render(<CheckboxSelect initialOptions={initialOptions} onToggle={onToggleMock} />);

const toggle = screen.getByRole('button', { name: 'Filter by status' });

await user.click(toggle);

expect(onToggleMock).toHaveBeenCalledTimes(1);
expect(onToggleMock).toHaveBeenCalledWith(true);

await user.click(toggle);

expect(onToggleMock).toHaveBeenCalledTimes(2);
expect(onToggleMock).toHaveBeenCalledWith(false);
});

test('does not call the onToggle callback when the toggle is not clicked', async () => {
const initialOptions = [
{ content: 'Option 1', value: 'option1' },
{ content: 'Option 2', value: 'option2' },
{ content: 'Option 3', value: 'option3' }
];

const onToggleMock = jest.fn();

render(<CheckboxSelect initialOptions={initialOptions} onToggle={onToggleMock} />);

expect(onToggleMock).not.toHaveBeenCalled();
});

test('disables the select when isDisabled prop is true', async () => {
const initialOptions = [
{ content: 'Option 1', value: 'option1' },
{ content: 'Option 2', value: 'option2' },
{ content: 'Option 3', value: 'option3' }
];

const user = userEvent.setup();

render(<CheckboxSelect initialOptions={initialOptions} isDisabled={true} />);

const toggleButton = screen.getByRole('button', { name: 'Filter by status' });

expect(toggleButton).toBeDisabled();

await user.click(toggleButton);

expect(screen.queryByRole('menu')).not.toBeInTheDocument();
});

test('passes other SelectOption props to the SelectOption component', async () => {
const initialOptions = [{ content: 'Option 1', value: 'option1', isDisabled: true }];

const user = userEvent.setup();

render(<CheckboxSelect initialOptions={initialOptions} />);

const toggle = screen.getByRole('button', { name: 'Filter by status' });

await user.click(toggle);

const option1 = screen.getByRole('checkbox', { name: 'Option 1' });

expect(option1).toBeDisabled();
});

test('displays the badge count when options are selected', async () => {
const initialOptions = [
{ content: 'Option 1', value: 'option1' },
{ content: 'Option 2', value: 'option2' },
{ content: 'Option 3', value: 'option3' }
];

const user = userEvent.setup();

render(<CheckboxSelect initialOptions={initialOptions} />);

const toggle = screen.getByRole('button', { name: 'Filter by status' });

await user.click(toggle);

const option1 = screen.getByRole('checkbox', { name: 'Option 1' });

expect(screen.queryByText('1')).not.toBeInTheDocument();

await user.click(option1);

expect(screen.getByText('1')).toHaveClass(styles.badge, 'pf-m-read');
});
113 changes: 113 additions & 0 deletions packages/react-templates/src/components/Select/CheckboxSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import React from 'react';
import {
Badge,
MenuToggle,
MenuToggleElement,
Select,
SelectList,
SelectOption,
SelectOptionProps
} from '@patternfly/react-core';

export interface CheckboxSelectOption extends Omit<SelectOptionProps, 'content'> {
/** Content of the select option. */
content: React.ReactNode;
/** Value of the select option. */
value: string | number;
}

export interface CheckboxSelectProps {
/** @hide Forwarded ref */
innerRef?: React.Ref<any>;
/** Initial options of the select. */
initialOptions?: CheckboxSelectOption[];
/** Callback triggered on selection. */
onSelect?: (_event: React.MouseEvent<Element, MouseEvent>, value?: string | number) => void;
/** Callback triggered when the select opens or closes. */
onToggle?: (nextIsOpen: boolean) => void;
/** Flag indicating the select should be disabled. */
isDisabled?: boolean;
/** Content of the toggle. Defaults to the selected option. */
toggleContent?: React.ReactNode;
}

const CheckboxSelectBase: React.FunctionComponent<CheckboxSelectProps> = ({
innerRef,
initialOptions,
isDisabled,
onSelect: passedOnSelect,
onToggle,
toggleContent,
...props
}: CheckboxSelectProps) => {
const [isOpen, setIsOpen] = React.useState(false);
const [selected, setSelected] = React.useState<string[]>([]);

const checkboxSelectOptions = initialOptions?.map((option) => {
const { content, value, ...props } = option;
const isSelected = selected.includes(`${value}`);
return (
<SelectOption {...props} value={value} key={value} hasCheckbox isSelected={isSelected}>
{content}
</SelectOption>
);
});

const onToggleClick = () => {
onToggle && onToggle(!isOpen);
setIsOpen(!isOpen);
};

const onSelect = (event: React.MouseEvent<Element, MouseEvent> | undefined, value: string | number | undefined) => {
const valueString = `${value}`;
if (selected.includes(valueString)) {
setSelected((prevSelected) => prevSelected.filter((item) => item !== valueString));
} else {
setSelected((prevSelected) => [...prevSelected, valueString]);
}
passedOnSelect && passedOnSelect(event, value);
};

const defaultToggleContent = (
<>
Filter by status
{selected.length > 0 && <Badge isRead>{selected.length}</Badge>}
</>
);

const toggle = (toggleRef: React.Ref<MenuToggleElement>) => (
<MenuToggle
ref={toggleRef}
onClick={onToggleClick}
isExpanded={isOpen}
isDisabled={isDisabled}
style={
{
width: '200px'
} as React.CSSProperties
}
>
{toggleContent || defaultToggleContent}
</MenuToggle>
);

return (
<Select
id="checkbox-select"
isOpen={isOpen}
selected={selected}
onSelect={onSelect}
onOpenChange={(isOpen) => setIsOpen(isOpen)}
toggle={toggle}
ref={innerRef}
role="menu"
{...props}
>
<SelectList>{checkboxSelectOptions}</SelectList>
</Select>
);
};

export const CheckboxSelect = React.forwardRef((props: CheckboxSelectProps, ref: React.Ref<any>) => (
<CheckboxSelectBase {...props} innerRef={ref} />
));
Loading
Loading