diff --git a/src/components/FilterPopup/FilterPopup.css b/src/components/FilterPopup/FilterPopup.css
new file mode 100644
index 00000000..f3d73f47
--- /dev/null
+++ b/src/components/FilterPopup/FilterPopup.css
@@ -0,0 +1,77 @@
+@import '../../marketplace.css';
+
+.root {
+ position: relative;
+ display: inline-block;
+}
+
+.label {
+ @apply --marketplaceButtonStylesSecondary;
+ @apply --marketplaceSearchFilterLabelFontStyles;
+
+ padding: 9px 16px 10px 16px;
+ width: auto;
+ height: auto;
+ min-height: 0;
+ border-radius: 4px;
+
+ &:focus {
+ outline: none;
+ background-color: var(--matterColorLight);
+ border-color: transparent;
+ text-decoration: none;
+ box-shadow: var(--boxShadowFilterButton);
+ }
+}
+
+.labelSelected {
+ @apply --marketplaceButtonStyles;
+ @apply --marketplaceSearchFilterLabelFontStyles;
+ font-weight: var(--fontWeightSemiBold);
+
+ padding: 9px 16px 10px 16px;
+ width: auto;
+ height: auto;
+ min-height: 0;
+ border-radius: 4px;
+ border: 1px solid var(--marketplaceColor);
+
+ &:hover,
+ &:focus {
+ border: 1px solid var(--marketplaceColorDark);
+ }
+}
+
+.popup {
+ /* By default hide the content */
+ display: block;
+ visibility: hidden;
+ opacity: 0;
+ pointer-events: none;
+
+ /* Position */
+ position: absolute;
+ z-index: var(--zIndexPopup);
+
+ /* Layout */
+ min-width: 300px;
+ margin-top: 7px;
+ background-color: var(--matterColorLight);
+
+ /* Borders */
+ border-top: 1px solid var(--matterColorNegative);
+ box-shadow: var(--boxShadowPopup);
+ border-radius: 4px;
+ transition: var(--transitionStyleButton);
+}
+
+.popupSize {
+ padding: 15px 30px 17px 30px;
+}
+
+.isOpen {
+ display: block;
+ visibility: visible;
+ opacity: 1;
+ pointer-events: auto;
+}
diff --git a/src/components/FilterPopup/FilterPopup.example.js b/src/components/FilterPopup/FilterPopup.example.js
new file mode 100644
index 00000000..7c24ab37
--- /dev/null
+++ b/src/components/FilterPopup/FilterPopup.example.js
@@ -0,0 +1,25 @@
+import React from 'react';
+import { FieldTextInput } from '../../components';
+import FilterPopup from './FilterPopup';
+
+const id = 'FilterPopupExample';
+const field = ;
+
+export const FilterPopupExample = {
+ component: FilterPopup,
+ props: {
+ id,
+ liveEdit: false,
+ showAsPopup: true,
+ urlParam: 'example',
+ initialValues: {},
+ isSelected: false,
+ contentPlacementOffset: -14,
+ onSubmit: (urlParam, values) => {
+ console.log(`onSubmit with urlParam: ${urlParam} and values: ${JSON.stringify(values)}`);
+ },
+ label: 'Example label',
+ children: field,
+ },
+ group: 'misc',
+};
diff --git a/src/components/FilterPopup/FilterPopup.js b/src/components/FilterPopup/FilterPopup.js
new file mode 100644
index 00000000..3ba087c0
--- /dev/null
+++ b/src/components/FilterPopup/FilterPopup.js
@@ -0,0 +1,188 @@
+import React, { Component } from 'react';
+import { bool, func, node, number, object, string } from 'prop-types';
+import classNames from 'classnames';
+import { injectIntl, intlShape } from 'react-intl';
+
+import { FilterForm } from '../../forms';
+import css from './FilterPopup.css';
+
+const KEY_CODE_ESCAPE = 27;
+
+class FilterPopup extends Component {
+ constructor(props) {
+ super(props);
+
+ this.state = { isOpen: false };
+ this.filter = null;
+ this.filterContent = null;
+
+ this.handleSubmit = this.handleSubmit.bind(this);
+ this.handleClear = this.handleClear.bind(this);
+ this.handleCancel = this.handleCancel.bind(this);
+ this.handleBlur = this.handleBlur.bind(this);
+ this.handleKeyDown = this.handleKeyDown.bind(this);
+ this.toggleOpen = this.toggleOpen.bind(this);
+ this.positionStyleForContent = this.positionStyleForContent.bind(this);
+ }
+
+ handleSubmit(values) {
+ const { onSubmit, urlParam } = this.props;
+ this.setState({ isOpen: false });
+ onSubmit(urlParam, values);
+ }
+
+ handleClear() {
+ const { onSubmit, onClear, urlParam } = this.props;
+ this.setState({ isOpen: false });
+
+ if (onClear) {
+ onClear();
+ }
+
+ onSubmit(urlParam, null);
+ }
+
+ handleCancel() {
+ const { onSubmit, onCancel, initialValues, urlParam } = this.props;
+ this.setState({ isOpen: false });
+
+ if (onCancel) {
+ onCancel();
+ }
+
+ onSubmit(urlParam, initialValues);
+ }
+
+ handleBlur(event) {
+ // FocusEvent is fired faster than the link elements native click handler
+ // gets its own event. Therefore, we need to check the origin of this FocusEvent.
+ if (event.relatedTarget && !this.filter.contains(event.relatedTarget)) {
+ this.setState({ isOpen: false });
+ }
+ }
+
+ handleKeyDown(e) {
+ // Gather all escape presses to close menu
+ if (e.keyCode === KEY_CODE_ESCAPE) {
+ this.toggleOpen(false);
+ }
+ }
+
+ toggleOpen(enforcedState) {
+ if (enforcedState) {
+ this.setState({ isOpen: enforcedState });
+ } else {
+ this.setState(prevState => ({ isOpen: !prevState.isOpen }));
+ }
+ }
+
+ positionStyleForContent() {
+ if (this.filter && this.filterContent) {
+ // Render the filter content to the right from the menu
+ // unless there's no space in which case it is rendered
+ // to the left
+ const distanceToRight = window.innerWidth - this.filter.getBoundingClientRect().right;
+ const labelWidth = this.filter.offsetWidth;
+ const contentWidth = this.filterContent.offsetWidth;
+ const contentWidthBiggerThanLabel = contentWidth - labelWidth;
+ const renderToRight = distanceToRight > contentWidthBiggerThanLabel;
+ const contentPlacementOffset = this.props.contentPlacementOffset;
+
+ const offset = renderToRight
+ ? { left: contentPlacementOffset }
+ : { right: contentPlacementOffset };
+ // set a min-width if the content is narrower than the label
+ const minWidth = contentWidth < labelWidth ? { minWidth: labelWidth } : null;
+
+ return { ...offset, ...minWidth };
+ }
+ return {};
+ }
+
+ render() {
+ const {
+ rootClassName,
+ className,
+ popupClassName,
+ id,
+ label,
+ isSelected,
+ children,
+ initialValues,
+ contentPlacementOffset,
+ } = this.props;
+
+ const classes = classNames(rootClassName || css.root, className);
+ const popupClasses = classNames(css.popup, { [css.isOpen]: this.state.isOpen });
+ const popupSizeClasses = popupClassName || css.popupSize;
+ const labelStyles = isSelected ? css.labelSelected : css.label;
+ const contentStyle = this.positionStyleForContent();
+
+ return (
+
{
+ this.filter = node;
+ }}
+ >
+
+
{
+ this.filterContent = node;
+ }}
+ style={contentStyle}
+ >
+ {this.state.isOpen ? (
+
+ {children}
+
+ ) : null}
+
+
+ );
+ }
+}
+
+FilterPopup.defaultProps = {
+ rootClassName: null,
+ className: null,
+ popupClassName: null,
+ initialValues: null,
+ contentPlacementOffset: 0,
+ liveEdit: false,
+ label: null,
+};
+
+FilterPopup.propTypes = {
+ rootClassName: string,
+ className: string,
+ popupClassName: string,
+ id: string.isRequired,
+ urlParam: string.isRequired,
+ onSubmit: func.isRequired,
+ initialValues: object,
+ contentPlacementOffset: number,
+ label: string.isRequired,
+ isSelected: bool.isRequired,
+ children: node.isRequired,
+
+ // form injectIntl
+ intl: intlShape.isRequired,
+};
+
+export default injectIntl(FilterPopup);
diff --git a/src/components/index.js b/src/components/index.js
index 719e4f40..f8bbbf84 100644
--- a/src/components/index.js
+++ b/src/components/index.js
@@ -29,6 +29,7 @@ export { default as FieldReviewRating } from './FieldReviewRating/FieldReviewRat
export { default as FieldSelect } from './FieldSelect/FieldSelect';
export { default as FieldTextInput } from './FieldTextInput/FieldTextInput';
export { default as FilterPlain } from './FilterPlain/FilterPlain';
+export { default as FilterPopup } from './FilterPopup/FilterPopup';
export { default as Footer } from './Footer/Footer';
export { default as Form } from './Form/Form';
export { default as IconAdd } from './IconAdd/IconAdd';
diff --git a/src/examples.js b/src/examples.js
index d34d483e..4cf2a4cf 100644
--- a/src/examples.js
+++ b/src/examples.js
@@ -20,6 +20,7 @@ import * as FieldReviewRating from './components/FieldReviewRating/FieldReviewRa
import * as FieldSelect from './components/FieldSelect/FieldSelect.example';
import * as FieldTextInput from './components/FieldTextInput/FieldTextInput.example';
import * as FilterPlain from './components/FilterPlain/FilterPlain.example';
+import * as FilterPopup from './components/FilterPopup/FilterPopup.example';
import * as Footer from './components/Footer/Footer.example';
import * as IconAdd from './components/IconAdd/IconAdd.example';
import * as IconBannedUser from './components/IconBannedUser/IconBannedUser.example';
@@ -122,6 +123,7 @@ export {
FieldTextInput,
FilterForm,
FilterPlain,
+ FilterPopup,
Footer,
IconAdd,
IconBannedUser,