diff --git a/src/components/FilterPanel/FilterPanel.css b/src/components/FilterPanel/FilterPanel.css
deleted file mode 100644
index c8ac683c..00000000
--- a/src/components/FilterPanel/FilterPanel.css
+++ /dev/null
@@ -1,37 +0,0 @@
-.filterTitle {
- text-align: center;
-}
-
-.toListingsButton {
- position: fixed;
- bottom: 1rem;
- left: 50%;
- width: 200px;
- margin-left: -100px;
- display: block;
- text-align: center;
- text-decoration: none;
- font-size: 1.4rem;
- padding: 0.5rem;
- color: #fff;
- background-color: #9b9b9b;
- cursor: pointer;
-
- &:hover {
- background-color: #888;
- }
-}
-
-.close {
- position: absolute;
- top: 0;
- left: 0;
- height: 3em;
- padding: 1em;
- color: #999;
- text-decoration: none;
-
- &:hover {
- color: #000;
- }
-}
diff --git a/src/components/FilterPanel/FilterPanel.js b/src/components/FilterPanel/FilterPanel.js
deleted file mode 100644
index dcc5a762..00000000
--- a/src/components/FilterPanel/FilterPanel.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import React from 'react';
-import { NamedLink } from '../../components';
-import css from './FilterPanel.css';
-
-const FilterPanel = () => (
-
-
Filters
-
- See studios
-
-
- X
-
-
-);
-
-export default FilterPanel;
diff --git a/src/components/FilterPanel/FilterPanel.test.js b/src/components/FilterPanel/FilterPanel.test.js
deleted file mode 100644
index 28ada261..00000000
--- a/src/components/FilterPanel/FilterPanel.test.js
+++ /dev/null
@@ -1,10 +0,0 @@
-import React from 'react';
-import { renderShallow } from '../../util/test-helpers';
-import FilterPanel from './FilterPanel';
-
-describe('FilterPanel', () => {
- it('matches snapshot', () => {
- const tree = renderShallow();
- expect(tree).toMatchSnapshot();
- });
-});
diff --git a/src/components/FilterPanel/__snapshots__/FilterPanel.test.js.snap b/src/components/FilterPanel/__snapshots__/FilterPanel.test.js.snap
deleted file mode 100644
index 7ed8959d..00000000
--- a/src/components/FilterPanel/__snapshots__/FilterPanel.test.js.snap
+++ /dev/null
@@ -1,19 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`FilterPanel matches snapshot 1`] = `
-
-
- Filters
-
-
- See studios
-
-
- X
-
-
-`;
diff --git a/src/components/Form/Form.js b/src/components/Form/Form.js
index 961e205c..07201002 100644
--- a/src/components/Form/Form.js
+++ b/src/components/Form/Form.js
@@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
const Form = props => {
- const { children, ...restProps } = props;
+ const { children, contentRef, ...restProps } = props;
const formProps = {
// These are mainly default values for the server
@@ -12,6 +12,9 @@ const Form = props => {
method: 'post',
action: '/',
+ // allow content ref function to be passed to the form
+ ref: contentRef,
+
...restProps,
};
return ;
@@ -19,12 +22,14 @@ const Form = props => {
Form.defaultProps = {
children: null,
+ contentRef: null,
};
-const { node } = PropTypes;
+const { func, node } = PropTypes;
Form.propTypes = {
children: node,
+ contentRef: func,
};
export default Form;
diff --git a/src/components/SelectMultipleFilter/SelectMultipleFilter.css b/src/components/SelectMultipleFilter/SelectMultipleFilter.css
new file mode 100644
index 00000000..67812d59
--- /dev/null
+++ b/src/components/SelectMultipleFilter/SelectMultipleFilter.css
@@ -0,0 +1,43 @@
+@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);
+ }
+}
diff --git a/src/components/SelectMultipleFilter/SelectMultipleFilter.example.js b/src/components/SelectMultipleFilter/SelectMultipleFilter.example.js
new file mode 100644
index 00000000..97ea524f
--- /dev/null
+++ b/src/components/SelectMultipleFilter/SelectMultipleFilter.example.js
@@ -0,0 +1,67 @@
+import React from 'react';
+import { withRouter } from 'react-router-dom';
+import SelectMultipleFilter from './SelectMultipleFilter';
+import { stringify, parse } from '../../util/urlHelpers';
+
+const options = [
+ {
+ key: 'towels',
+ label: 'Towels',
+ },
+ {
+ key: 'bathroom',
+ label: 'Bathroom',
+ },
+ {
+ key: 'swimming_pool',
+ label: 'Swimming pool',
+ },
+ {
+ key: 'own_drinks',
+ label: 'Own drinks allowed',
+ },
+ {
+ key: 'jacuzzi',
+ label: 'Jacuzzi',
+ },
+ {
+ key: 'audiovisual_entertainment',
+ label: 'Audiovisual entertainment',
+ },
+ {
+ key: 'barbeque',
+ label: 'Barbeque',
+ },
+ {
+ key: 'own_food_allowed',
+ label: 'Own food allowed',
+ },
+];
+
+const handleSubmit = (values, history) => {
+ const queryParams = values ? `?${stringify({ pub_amenities: values.join(',') })}` : '';
+ history.push(`${window.location.pathname}${queryParams}`);
+};
+
+const AmenitiesFilterComponent = withRouter(props => {
+ const { history, location } = props;
+
+ const params = parse(location.search);
+ const amenities = params.pub_amenities;
+ const initialValues = !!amenities ? amenities.split(',') : [];
+
+ return (
+ handleSubmit(values, history)}
+ options={options}
+ initialValues={initialValues}
+ contentPlacementOffset={-14}
+ />
+ );
+});
+
+export const AmenitiesFilter = {
+ component: AmenitiesFilterComponent,
+ props: {},
+ group: 'misc',
+};
diff --git a/src/components/SelectMultipleFilter/SelectMultipleFilter.js b/src/components/SelectMultipleFilter/SelectMultipleFilter.js
new file mode 100644
index 00000000..627232c2
--- /dev/null
+++ b/src/components/SelectMultipleFilter/SelectMultipleFilter.js
@@ -0,0 +1,178 @@
+import React, { Component } from 'react';
+import { array, arrayOf, func, number, string } from 'prop-types';
+import classNames from 'classnames';
+import { injectIntl, intlShape } from 'react-intl';
+import { toPairs } from 'lodash';
+
+import SelectMultipleFilterForm from './SelectMultipleFilterForm';
+import css from './SelectMultipleFilter.css';
+
+const KEY_CODE_ESCAPE = 27;
+
+/**
+ * Convert an array of option keys into
+ * an object that can be passed to a redux
+ * form as initial values.
+ */
+const keysToValues = keys => {
+ return keys.reduce((map, key) => {
+ map[key] = true;
+ return map;
+ }, {});
+};
+
+/**
+ * Convert an object containing values received
+ * from a redux form into an array of values.
+ */
+const valuesToKeys = values => {
+ const entries = toPairs(values);
+ return entries.filter(entry => entry[1] === true).map(entry => entry[0]);
+};
+
+class SelectMultipleFilter 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 selectedKeys = valuesToKeys(values);
+ this.setState({ isOpen: false });
+ this.props.onSubmit(selectedKeys);
+ }
+
+ handleClear() {
+ this.setState({ isOpen: false });
+ this.props.onSubmit(null);
+ }
+
+ handleCancel() {
+ const { onSubmit, initialValues } = this.props;
+ this.setState({ isOpen: false });
+ onSubmit(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 (!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, options, initialValues, intl } = this.props;
+ const classes = classNames(rootClassName || css.root, className);
+
+ const hasInitialValues = initialValues.length > 0;
+ const labelStyles = hasInitialValues ? css.labelSelected : css.label;
+ const label = hasInitialValues
+ ? intl.formatMessage(
+ { id: 'SelectMultipleFilterMobile.labelSelected' },
+ { count: initialValues.length }
+ )
+ : intl.formatMessage({ id: 'SelectMultipleFilterMobile.label' });
+
+ const contentStyle = this.positionStyleForContent();
+
+ return (
+ {
+ this.filter = node;
+ }}
+ >
+
+ {
+ this.filterContent = node;
+ }}
+ style={contentStyle}
+ />
+
+ );
+ }
+}
+
+SelectMultipleFilter.defaultProps = {
+ rootClassName: null,
+ className: null,
+ initialValues: [],
+ contentPlacementOffset: 0,
+};
+
+SelectMultipleFilter.propTypes = {
+ rootClassName: string,
+ className: string,
+ onSubmit: func.isRequired,
+ options: array.isRequired,
+ initialValues: arrayOf(string),
+ contentPlacementOffset: number,
+
+ // form injectIntl
+ intl: intlShape.isRequired,
+};
+
+export default injectIntl(SelectMultipleFilter);
diff --git a/src/components/SelectMultipleFilter/SelectMultipleFilterForm.css b/src/components/SelectMultipleFilter/SelectMultipleFilterForm.css
new file mode 100644
index 00000000..1fa0e41a
--- /dev/null
+++ b/src/components/SelectMultipleFilter/SelectMultipleFilterForm.css
@@ -0,0 +1,102 @@
+@import '../../marketplace.css';
+
+.root {
+ /* By default hide the content */
+ visibility: hidden;
+ opacity: 0;
+ pointer-events: none;
+
+ /* Position */
+ position: absolute;
+ z-index: var(--zIndexPopup);
+
+ /* Layout */
+ margin-top: 7px;
+ padding: 15px 40px 20px 30px;
+
+ /* Borders */
+ background-color: var(--matterColorLight);
+ border-top: 1px solid var(--matterColorNegative);
+ box-shadow: var(--boxShadowPopup);
+ border-radius: 4px;
+ transition: var(--transitionStyleButton);
+ outline: none;
+}
+
+.isOpen {
+ visibility: visible;
+ opacity: 1;
+ pointer-events: auto;
+}
+
+.fieldGroup {
+ margin-bottom: 35px;
+ white-space: nowrap;
+}
+
+.buttonsWrapper {
+ display: flex;
+}
+
+.clearButton {
+ @apply --marketplaceH4FontStyles;
+ font-weight: var(--fontWeightMedium);
+ color: var(--matterColorAnti);
+
+ /* Layout */
+ margin: 0 auto 0 0;
+ padding: 0;
+
+ /* Override button styles */
+ outline: none;
+ border: none;
+ cursor: pointer;
+
+ &:focus,
+ &:hover {
+ color: var(--matterColor);
+ transition: width var(--transitionStyleButton);
+ }
+}
+
+.cancelButton {
+ @apply --marketplaceH4FontStyles;
+ font-weight: var(--fontWeightMedium);
+ color: var(--matterColorAnti);
+
+ /* Layout */
+ margin: 0;
+ padding: 0;
+
+ /* Override button styles */
+ outline: none;
+ border: none;
+ cursor: pointer;
+
+ &:focus,
+ &:hover {
+ color: var(--matterColor);
+ transition: width var(--transitionStyleButton);
+ }
+}
+
+.submitButton {
+ @apply --marketplaceH4FontStyles;
+ font-weight: var(--fontWeightMedium);
+ color: var(--marketplaceColor);
+
+ /* Layout */
+ margin: 0 0 0 19px;
+ padding: 0;
+
+ /* Override button styles */
+ outline: none;
+ border: none;
+ cursor: pointer;
+
+ &:focus,
+ &:hover {
+ color: var(--marketplaceColorDark);
+ transition: width var(--transitionStyleButton);
+ }
+}
diff --git a/src/components/SelectMultipleFilter/SelectMultipleFilterForm.js b/src/components/SelectMultipleFilter/SelectMultipleFilterForm.js
new file mode 100644
index 00000000..effcc2d6
--- /dev/null
+++ b/src/components/SelectMultipleFilter/SelectMultipleFilterForm.js
@@ -0,0 +1,98 @@
+import React from 'react';
+import { arrayOf, bool, func, node, object, shape, string } from 'prop-types';
+import classNames from 'classnames';
+import { compose } from 'redux';
+import { reduxForm, propTypes as formPropTypes } from 'redux-form';
+import { injectIntl, intlShape } from 'react-intl';
+
+import { FieldGroupCheckbox, Form } from '../../components';
+import css from './SelectMultipleFilterForm.css';
+
+const SelectMultipleFilterFormComponent = props => {
+ const {
+ form,
+ destroy,
+ reset,
+ handleSubmit,
+ onClear,
+ onCancel,
+ options,
+ isOpen,
+ contentRef,
+ style,
+ intl,
+ } = props;
+ const classes = classNames(css.root, { [css.isOpen]: isOpen });
+
+ const handleClear = () => {
+ // clear the redux form state
+ destroy();
+ onClear();
+ };
+
+ const handleCancel = () => {
+ // reset the redux form to initialValues
+ reset();
+ onCancel();
+ };
+
+ const clear = intl.formatMessage({ id: 'SelectMultipleFilterMobile.clear' });
+ const cancel = intl.formatMessage({ id: 'SelectMultipleFilterMobile.cancel' });
+ const submit = intl.formatMessage({ id: 'SelectMultipleFilterMobile.submit' });
+
+ return (
+
+ );
+};
+
+SelectMultipleFilterFormComponent.defaultProps = {
+ contentRef: null,
+ style: null,
+};
+
+SelectMultipleFilterFormComponent.propTypes = {
+ ...formPropTypes,
+ onClear: func.isRequired,
+ onCancel: func.isRequired,
+ options: arrayOf(
+ shape({
+ key: string.isRequired,
+ label: node.isRequired,
+ })
+ ).isRequired,
+ isOpen: bool.isRequired,
+ contentRef: func,
+ style: object,
+
+ // form injectIntl
+ intl: intlShape.isRequired,
+};
+
+const defaultFormName = 'SelectMultipleFilterForm';
+
+export default compose(reduxForm({ form: defaultFormName }), injectIntl)(
+ SelectMultipleFilterFormComponent
+);
diff --git a/src/components/index.js b/src/components/index.js
index d75ae34e..b581ac2b 100644
--- a/src/components/index.js
+++ b/src/components/index.js
@@ -29,7 +29,6 @@ export {
export { default as EditListingWizard } from './EditListingWizard/EditListingWizard';
export { default as ExpandingTextarea } from './ExpandingTextarea/ExpandingTextarea';
export { default as ExternalLink } from './ExternalLink/ExternalLink';
-export { default as FilterPanel } from './FilterPanel/FilterPanel';
export { default as FieldBirthdayInput } from './FieldBirthdayInput/FieldBirthdayInput';
export { default as FieldCheckbox } from './FieldCheckbox/FieldCheckbox';
export { default as FieldCurrencyInput } from './FieldCurrencyInput/FieldCurrencyInput';
@@ -108,6 +107,7 @@ export { default as SectionHowItWorks } from './SectionHowItWorks/SectionHowItWo
export { default as SectionLocations } from './SectionLocations/SectionLocations';
export { default as SectionThumbnailLinks } from './SectionThumbnailLinks/SectionThumbnailLinks';
export { default as SelectField } from './SelectField/SelectField';
+export { default as SelectMultipleFilter } from './SelectMultipleFilter/SelectMultipleFilter';
export { default as SelectSingleFilter } from './SelectSingleFilter/SelectSingleFilter';
export {
default as SelectSingleFilterMobile,
diff --git a/src/containers/BookingDatesForm/__snapshots__/BookingDatesForm.test.js.snap b/src/containers/BookingDatesForm/__snapshots__/BookingDatesForm.test.js.snap
index 8ca5cdc2..2164cfc9 100644
--- a/src/containers/BookingDatesForm/__snapshots__/BookingDatesForm.test.js.snap
+++ b/src/containers/BookingDatesForm/__snapshots__/BookingDatesForm.test.js.snap
@@ -3,6 +3,7 @@
exports[`BookingDatesForm matches snapshot without selected dates 1`] = `