diff --git a/index.js b/index.js
index 5ef94e780..6a839053d 100644
--- a/index.js
+++ b/index.js
@@ -26,7 +26,11 @@ export { default as PasswordValidationField } from './lib/PasswordValidationFiel
export { default as ProxyManager } from './lib/ProxyManager';
export { default as SearchAndSort } from './lib/SearchAndSort';
+
+export { default as CheckboxFilter } from './lib/SearchAndSort/components/CheckboxFilter';
+
export { default as MultiSelectionFilter } from './lib/SearchAndSort/components/MultiSelectionFilter';
+
export { default as makeQueryFunction } from './lib/SearchAndSort/makeQueryFunction';
export { default as Settings } from './lib/Settings';
diff --git a/lib/SearchAndSort/components/CheckboxFilter/CheckboxFilter.js b/lib/SearchAndSort/components/CheckboxFilter/CheckboxFilter.js
new file mode 100644
index 000000000..804c27473
--- /dev/null
+++ b/lib/SearchAndSort/components/CheckboxFilter/CheckboxFilter.js
@@ -0,0 +1,63 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import { Checkbox } from '@folio/stripes-components';
+
+export default class CheckboxFilter extends React.Component {
+ static propTypes = {
+ dataOptions: PropTypes.arrayOf(PropTypes.shape({
+ disabled: PropTypes.bool,
+ label: PropTypes.node,
+ readOnly: PropTypes.bool,
+ value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ })).isRequired,
+ name: PropTypes.string.isRequired,
+ onChange: PropTypes.func.isRequired,
+ selectedValues: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])),
+ };
+
+ static defaultProps = {
+ selectedValues: [],
+ }
+
+ createOnChangeHandler = (filterValue) => (e) => {
+ const {
+ name,
+ selectedValues,
+ onChange,
+ } = this.props;
+
+ const newValues = e.target.checked
+ ? [...selectedValues, filterValue]
+ : selectedValues.filter((value) => value !== filterValue);
+
+ onChange({
+ name,
+ values: newValues,
+ });
+ };
+
+ render() {
+ const {
+ dataOptions,
+ selectedValues,
+ } = this.props;
+
+ return (
+ dataOptions.map(({ value, label, disabled, readOnly }) => {
+ return (
+
+ );
+ })
+ );
+ }
+}
diff --git a/lib/SearchAndSort/components/CheckboxFilter/index.js b/lib/SearchAndSort/components/CheckboxFilter/index.js
new file mode 100644
index 000000000..370af3605
--- /dev/null
+++ b/lib/SearchAndSort/components/CheckboxFilter/index.js
@@ -0,0 +1 @@
+export { default } from './CheckboxFilter';
diff --git a/lib/SearchAndSort/components/CheckboxFilter/tests/CheckboxFilter-test.js b/lib/SearchAndSort/components/CheckboxFilter/tests/CheckboxFilter-test.js
new file mode 100644
index 000000000..af72dcf22
--- /dev/null
+++ b/lib/SearchAndSort/components/CheckboxFilter/tests/CheckboxFilter-test.js
@@ -0,0 +1,119 @@
+import React from 'react';
+import { describe, beforeEach, it } from '@bigtest/mocha';
+import { expect } from 'chai';
+import sinon from 'sinon';
+
+import { mountWithContext } from '@folio/stripes-components/tests/helpers';
+
+import CheckboxFilter from '../CheckboxFilter';
+import CheckboxFilterInteractor from './interactor';
+
+
+describe('CheckboxFilter', () => {
+ const checkboxFilter = new CheckboxFilterInteractor();
+ const onChangeHandler = sinon.spy();
+
+ const renderComponent = (props) => {
+ return mountWithContext(
+
+ );
+ };
+
+ beforeEach(async () => {
+ await renderComponent();
+
+ onChangeHandler.resetHistory();
+ });
+
+ it('should render all filter data options', () => {
+ expect(checkboxFilter.dataOptionsCount).to.equal(4);
+ });
+
+
+ describe('after click on readonly data option', () => {
+ beforeEach(async () => {
+ await renderComponent({
+ dataOptions: [
+ {
+ value: 'en',
+ label: 'English',
+ readOnly: true,
+ },
+ ]
+ });
+ await checkboxFilter.dataOptions(0).click();
+ });
+
+ it('should not call onChange callback', () => {
+ expect(onChangeHandler.called).to.equal(false);
+ });
+ });
+
+ describe('after click on disabled data option', () => {
+ beforeEach(async () => {
+ await renderComponent({
+ dataOptions: [
+ {
+ value: 'en',
+ label: 'English',
+ disabled: true,
+ },
+ ]
+ });
+ await checkboxFilter.dataOptions(0).click();
+ });
+
+ it('should not call onChange callback', () => {
+ expect(onChangeHandler.called).to.equal(false);
+ });
+ });
+
+ describe('when there are selected data options', () => {
+ beforeEach(async () => {
+ await renderComponent({
+ selectedValues: ['fr', 'ge'],
+ });
+ });
+
+ it('should render selected data options', () => {
+ expect(checkboxFilter.dataOptions(1).isSelected).to.equal(true);
+ expect(checkboxFilter.dataOptions(2).isSelected).to.equal(true);
+ });
+
+ describe('after click on unselected data option', () => {
+ beforeEach(async () => {
+ await checkboxFilter.dataOptions(0).click();
+ });
+
+ it('should raise filter name and new selected values to onChange callback', () => {
+ expect(onChangeHandler.args[0]).to.deep.equal([{
+ name: 'language',
+ values: ['fr', 'ge', 'en'],
+ }]);
+ });
+ });
+
+ describe('after click on selected option', () => {
+ beforeEach(async () => {
+ await checkboxFilter.dataOptions(1).click();
+ });
+
+ it('should raise filter name with the remaining selected values to onChange callback', () => {
+ expect(onChangeHandler.args[0]).to.deep.equal([{
+ name: 'language',
+ values: ['ge'],
+ }]);
+ });
+ });
+ });
+});
diff --git a/lib/SearchAndSort/components/CheckboxFilter/tests/interactor.js b/lib/SearchAndSort/components/CheckboxFilter/tests/interactor.js
new file mode 100644
index 000000000..81bd6a34f
--- /dev/null
+++ b/lib/SearchAndSort/components/CheckboxFilter/tests/interactor.js
@@ -0,0 +1,19 @@
+import {
+ count,
+ clickable,
+ interactor,
+ collection,
+ is
+} from '@bigtest/interactor';
+
+const DataOption = interactor(class DataOptionInteractor {
+ click = clickable();
+ isSelected = is('[checked]');
+ isDisabled = is('disabled');
+ isReadOnly = is('readonly')
+});
+
+export default interactor(class CheckboxFilterInteractor {
+ dataOptionsCount = count('[data-test-checkbox-filter-data-option]');
+ dataOptions = collection('[data-test-checkbox-filter-data-option]', DataOption);
+});