diff --git a/packages/react-templates/src/components/Select/CheckboxSelect.test.tsx b/packages/react-templates/src/components/Select/CheckboxSelect.test.tsx
new file mode 100644
index 00000000000..e47a40783fe
--- /dev/null
+++ b/packages/react-templates/src/components/Select/CheckboxSelect.test.tsx
@@ -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();
+
+ 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();
+
+ 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();
+
+ 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();
+
+ 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();
+
+ 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();
+
+ 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();
+
+ 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();
+
+ 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();
+
+ 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();
+
+ 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();
+
+ 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();
+
+ 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');
+});
diff --git a/packages/react-templates/src/components/Select/CheckboxSelect.tsx b/packages/react-templates/src/components/Select/CheckboxSelect.tsx
new file mode 100644
index 00000000000..df62017676b
--- /dev/null
+++ b/packages/react-templates/src/components/Select/CheckboxSelect.tsx
@@ -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 {
+ /** 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;
+ /** Initial options of the select. */
+ initialOptions?: CheckboxSelectOption[];
+ /** Callback triggered on selection. */
+ onSelect?: (_event: React.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 = ({
+ innerRef,
+ initialOptions,
+ isDisabled,
+ onSelect: passedOnSelect,
+ onToggle,
+ toggleContent,
+ ...props
+}: CheckboxSelectProps) => {
+ const [isOpen, setIsOpen] = React.useState(false);
+ const [selected, setSelected] = React.useState([]);
+
+ const checkboxSelectOptions = initialOptions?.map((option) => {
+ const { content, value, ...props } = option;
+ const isSelected = selected.includes(`${value}`);
+ return (
+
+ {content}
+
+ );
+ });
+
+ const onToggleClick = () => {
+ onToggle && onToggle(!isOpen);
+ setIsOpen(!isOpen);
+ };
+
+ const onSelect = (event: React.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 && {selected.length}}
+ >
+ );
+
+ const toggle = (toggleRef: React.Ref) => (
+
+ {toggleContent || defaultToggleContent}
+
+ );
+
+ return (
+
+ );
+};
+
+export const CheckboxSelect = React.forwardRef((props: CheckboxSelectProps, ref: React.Ref) => (
+
+));
diff --git a/packages/react-templates/src/components/Select/CheckboxSelectSnapshots.test.tsx b/packages/react-templates/src/components/Select/CheckboxSelectSnapshots.test.tsx
new file mode 100644
index 00000000000..5e305d00042
--- /dev/null
+++ b/packages/react-templates/src/components/Select/CheckboxSelectSnapshots.test.tsx
@@ -0,0 +1,32 @@
+import * as React from 'react';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { CheckboxSelect } from './CheckboxSelect';
+
+jest.mock('@patternfly/react-core/dist/js/helpers/GenerateId/GenerateId', () => ({
+ GenerateId: ({ children }) => children('generated-id')
+}));
+
+test('checkbox select with no props snapshot', () => {
+ const { asFragment } = render();
+
+ expect(asFragment()).toMatchSnapshot();
+});
+
+test('opened checkbox select snapshot', async () => {
+ const initialOptions = [
+ { content: 'Option 1', value: 'option1' },
+ { content: 'Option 2', value: 'option2' },
+ { content: 'Option 3', value: 'option3' }
+ ];
+
+ const user = userEvent.setup();
+
+ const { asFragment } = render();
+
+ const toggle = screen.getByRole('button', { name: 'Filter by status' });
+
+ await user.click(toggle);
+
+ expect(asFragment()).toMatchSnapshot();
+});
diff --git a/packages/react-templates/src/components/Select/__snapshots__/CheckboxSelectSnapshots.test.tsx.snap b/packages/react-templates/src/components/Select/__snapshots__/CheckboxSelectSnapshots.test.tsx.snap
new file mode 100644
index 00000000000..6d8189bf009
--- /dev/null
+++ b/packages/react-templates/src/components/Select/__snapshots__/CheckboxSelectSnapshots.test.tsx.snap
@@ -0,0 +1,212 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`checkbox select with no props snapshot 1`] = `
+
+
+
+`;
+
+exports[`opened checkbox select snapshot 1`] = `
+
+
+
+
+`;
diff --git a/packages/react-templates/src/components/Select/examples/CheckboxSelectDemo.tsx b/packages/react-templates/src/components/Select/examples/CheckboxSelectDemo.tsx
new file mode 100644
index 00000000000..676bd922868
--- /dev/null
+++ b/packages/react-templates/src/components/Select/examples/CheckboxSelectDemo.tsx
@@ -0,0 +1,13 @@
+import React from 'react';
+import { CheckboxSelect, CheckboxSelectOption } from '@patternfly/react-templates';
+
+export const SelectBasic: React.FunctionComponent = () => {
+ const initialOptions: CheckboxSelectOption[] = [
+ { content: 'Option 1', value: 'option-1' },
+ { content: 'Option 2', value: 'option-2' },
+ { content: 'Option 3', value: 'option-3', isDisabled: true },
+ { content: 'Option 4', value: 'option-4' }
+ ];
+
+ return ;
+};
diff --git a/packages/react-templates/src/components/Select/examples/SelectTemplates.md b/packages/react-templates/src/components/Select/examples/SelectTemplates.md
index 89cf3616271..db7af312d29 100644
--- a/packages/react-templates/src/components/Select/examples/SelectTemplates.md
+++ b/packages/react-templates/src/components/Select/examples/SelectTemplates.md
@@ -4,7 +4,7 @@ section: components
subsection: menus
template: true
beta: true
-propComponents: ['SimpleSelect']
+propComponents: ['SelectSimple', 'CheckboxSelect']
---
Note: Templates live in their own package at [@patternfly/react-templates](https://www.npmjs.com/package/@patternfly/react-templates)!
@@ -12,7 +12,7 @@ Note: Templates live in their own package at [@patternfly/react-templates](https
For custom use cases, please see the select component suite from [@patternfly/react-core](https://www.npmjs.com/package/@patternfly/react-core).
import { SelectOption, Checkbox } from '@patternfly/react-core';
-import { SelectSimple } from '@patternfly/react-templates';
+import { SelectSimple, CheckboxSelect } from '@patternfly/react-templates';
## Select template examples
@@ -21,3 +21,8 @@ import { SelectSimple } from '@patternfly/react-templates';
```ts file="SelectSimpleDemo.tsx"
```
+
+### Checkbox
+
+```ts file="CheckboxSelectDemo.tsx"
+```
diff --git a/packages/react-templates/src/components/Select/index.ts b/packages/react-templates/src/components/Select/index.ts
index 9c0381bfb99..c8752c8faa5 100644
--- a/packages/react-templates/src/components/Select/index.ts
+++ b/packages/react-templates/src/components/Select/index.ts
@@ -1 +1,2 @@
export * from './SelectSimple';
+export * from './CheckboxSelect';