From ac3db9efa3d512557f58dcd9afd936e2bf3e0bbd Mon Sep 17 00:00:00 2001 From: Jenni Nurmi Date: Wed, 21 Nov 2018 14:51:52 +0200 Subject: [PATCH] Refactor SelectSingleFilter to use subcomponents SelectSingleFilterPlain and SelectSingleFilterPopup --- src/components/SearchFilters/SearchFilters.js | 1 + .../SearchFiltersMobile.js | 5 +- .../SelectSingleFilter/SelectSingleFilter.js | 114 ++----------- .../SelectSingleFilterPlain.css | 151 ++++++++++++++++++ .../SelectSingleFilterPlain.js | 121 ++++++++++++++ .../SelectSingleFilterPopup.css | 127 +++++++++++++++ .../SelectSingleFilterPopup.js | 111 +++++++++++++ src/translations/en.json | 4 +- 8 files changed, 529 insertions(+), 105 deletions(-) create mode 100644 src/components/SelectSingleFilter/SelectSingleFilterPlain.css create mode 100644 src/components/SelectSingleFilter/SelectSingleFilterPlain.js create mode 100644 src/components/SelectSingleFilter/SelectSingleFilterPopup.css create mode 100644 src/components/SelectSingleFilter/SelectSingleFilterPopup.js diff --git a/src/components/SearchFilters/SearchFilters.js b/src/components/SearchFilters/SearchFilters.js index f47b89df..a7315962 100644 --- a/src/components/SearchFilters/SearchFilters.js +++ b/src/components/SearchFilters/SearchFilters.js @@ -152,6 +152,7 @@ const SearchFiltersComponent = props => { urlParam={categoryFilter.paramName} label={categoryLabel} onSelect={handleSelectOption} + showAsPopup options={categoryFilter.options} initialValue={initialCategory} contentPlacementOffset={FILTER_DROPDOWN_OFFSET} diff --git a/src/components/SearchFiltersMobile/SearchFiltersMobile.js b/src/components/SearchFiltersMobile/SearchFiltersMobile.js index f5948d51..383c6419 100644 --- a/src/components/SearchFiltersMobile/SearchFiltersMobile.js +++ b/src/components/SearchFiltersMobile/SearchFiltersMobile.js @@ -13,7 +13,7 @@ import { ModalInMobile, Button, PriceFilter, - SelectSingleFilterPlain, + SelectSingleFilter, SelectMultipleFilter, BookingDateRangeFilter, } from '../../components'; @@ -221,10 +221,11 @@ class SearchFiltersMobileComponent extends Component { const initialCategory = categoryFilter ? this.initialValue(categoryFilter.paramName) : null; const categoryFilterElement = categoryFilter ? ( - { - const option = options.find(o => o.key === key); - return option ? option.label : key; +const SelectSingleFilter = props => { + const { showAsPopup, ...rest } = props; + return showAsPopup ? ( + + ) : ( + + ); }; -class SelectSingleFilter extends Component { - constructor(props) { - super(props); - - this.state = { isOpen: false }; - this.onToggleActive = this.onToggleActive.bind(this); - this.selectOption = this.selectOption.bind(this); - } - - onToggleActive(isOpen) { - this.setState({ isOpen: isOpen }); - } - - selectOption(urlParam, option) { - this.setState({ isOpen: false }); - this.props.onSelect(urlParam, option); - } - - render() { - const { - rootClassName, - className, - urlParam, - label, - options, - initialValue, - contentPlacementOffset, - } = this.props; - - // resolve menu label text and class - const menuLabel = initialValue ? optionLabel(options, initialValue) : label; - const menuLabelClass = initialValue ? css.menuLabelSelected : css.menuLabel; - - const classes = classNames(rootClassName || css.root, className); - - return ( - - {menuLabel} - - {options.map(option => { - // check if this option is selected - const selected = initialValue === option.key; - // menu item border class - const menuItemBorderClass = selected ? css.menuItemBorderSelected : css.menuItemBorder; - - return ( - - - - ); - })} - - - - - - ); - } -} - SelectSingleFilter.defaultProps = { - rootClassName: null, - className: null, - initialValue: null, - contentPlacementOffset: 0, + showAsPopup: false, }; SelectSingleFilter.propTypes = { - rootClassName: string, - className: string, - urlParam: string.isRequired, - label: string.isRequired, - onSelect: func.isRequired, - options: arrayOf( - shape({ - key: string.isRequired, - label: string.isRequired, - }) - ).isRequired, - initialValue: string, - contentPlacementOffset: number, + showAsPopup: bool, }; export default SelectSingleFilter; diff --git a/src/components/SelectSingleFilter/SelectSingleFilterPlain.css b/src/components/SelectSingleFilter/SelectSingleFilterPlain.css new file mode 100644 index 00000000..8460c5df --- /dev/null +++ b/src/components/SelectSingleFilter/SelectSingleFilterPlain.css @@ -0,0 +1,151 @@ +@import '../../marketplace.css'; + +.root { + padding-top: 24px; + padding-bottom: 17px; + border-bottom: 1px solid var(--matterColorNegative); +} + +.filterLabel, +.filterLabelSelected { + @apply --marketplaceH3FontStyles; + + /* Baseline adjustment for label text */ + margin-top: 0; + margin-bottom: 12px; + padding: 4px 0 2px 0; +} + +.filterLabel { + color: var(--matterColorDark); +} + +.filterLabelSelected { + color: var(--marketplaceColor); +} + +.labelButton { + /* Override button styles */ + outline: none; + text-align: left; + border: none; + padding: 0; + cursor: pointer; +} + +.optionsContainerOpen { + height: auto; + padding-bottom: 30px; +} + +.optionsContainerClosed { + height: 0; + overflow: hidden; +} + +.hasBullets { + padding-left: 26px; +} + +.twoColumns { + @media (--viewportMedium) { + column-count: 2; + } +} + +.optionBorder, +.optionBorderSelected { + position: absolute; + height: calc(100% - 12px); + top: 4px; + left: -24px; + transition: width var(--transitionStyleButton); +} + +/* left animated "border" like hover element */ +.optionBorder { + width: 0; + background-color: var(--matterColorDark); +} + +/* left static border for selected element */ +.optionBorderSelected { + width: 8px; + background-color: var(--matterColorDark); +} + +.optionBullet, +.optionBulletSelected { + position: absolute; + left: -5px; + top: 13px; + width: 8px; + height: 8px; + border-radius: 4px; + background-color: var(--marketplaceColor); + transition: opacity var(--transitionStyleButton); +} + +/* left animated "border" like hover element */ +.optionBullet { + opacity: 0; +} + +/* left static border for selected element */ +.optionBulletSelected { + opacity: 1; +} + +.option { + @apply --marketplaceH4FontStyles; + font-weight: var(--fontWeightMedium); + font-size: 18px; + color: var(--matterColor); + + /* Layout */ + display: block; + position: relative; + margin: 0; + padding: 4px 0 8px 20px; + + /* Override button styles */ + outline: none; + border: none; + cursor: pointer; + + &:focus, + &:hover { + color: var(--matterColorDark); + } + + &:hover .menuItemBorder { + width: 6px; + } +} + +.optionSelected { + composes: option; + color: var(--matterColorDark); +} + +.clearButton { + @apply --marketplaceH5FontStyles; + font-weight: var(--fontWeightMedium); + color: var(--matterColorAnti); + + /* Layout */ + display: inline; + float: right; + margin-top: 6px; + padding: 0; + + /* Override button styles */ + outline: none; + text-align: left; + border: none; + + &:focus, + &:hover { + color: var(--matterColor); + } +} diff --git a/src/components/SelectSingleFilter/SelectSingleFilterPlain.js b/src/components/SelectSingleFilter/SelectSingleFilterPlain.js new file mode 100644 index 00000000..e99cfb07 --- /dev/null +++ b/src/components/SelectSingleFilter/SelectSingleFilterPlain.js @@ -0,0 +1,121 @@ +import React, { Component } from 'react'; +import { arrayOf, bool, func, shape, string } from 'prop-types'; +import classNames from 'classnames'; +import { FormattedMessage } from 'react-intl'; + +import css from './SelectSingleFilterPlain.css'; + +class SelectSingleFilterPlain extends Component { + constructor(props) { + super(props); + this.state = { isOpen: true }; + this.selectOption = this.selectOption.bind(this); + this.toggleIsOpen = this.toggleIsOpen.bind(this); + } + + selectOption(option, e) { + const { urlParam, onSelect } = this.props; + onSelect(urlParam, option); + + // blur event target if event is passed + if (e && e.currentTarget) { + e.currentTarget.blur(); + } + } + + toggleIsOpen() { + this.setState({ isOpen: !this.state.isOpen }); + } + + render() { + const { + rootClassName, + className, + label, + options, + initialValue, + twoColumns, + useBullets, + } = this.props; + + const labelClass = initialValue ? css.filterLabelSelected : css.filterLabel; + + const hasBullets = useBullets || twoColumns; + const optionsContainerClass = classNames({ + [css.optionsContainerOpen]: this.state.isOpen, + [css.optionsContainerClosed]: !this.state.isOpen, + [css.hasBullets]: hasBullets, + [css.twoColumns]: twoColumns, + }); + + const classes = classNames(rootClassName || css.root, className); + + return ( +
+
+ + +
+
+ {options.map(option => { + // check if this option is selected + const selected = initialValue === option.key; + const optionClass = hasBullets && selected ? css.optionSelected : css.option; + // menu item selected bullet or border class + const optionBorderClass = hasBullets + ? classNames({ + [css.optionBulletSelected]: selected, + [css.optionBullet]: !selected, + }) + : classNames({ + [css.optionBorderSelected]: selected, + [css.optionBorder]: !selected, + }); + return ( + + ); + })} +
+
+ ); + } +} + +SelectSingleFilterPlain.defaultProps = { + rootClassName: null, + className: null, + initialValue: null, + twoColumns: false, + useBullets: false, +}; + +SelectSingleFilterPlain.propTypes = { + rootClassName: string, + className: string, + urlParam: string.isRequired, + label: string.isRequired, + onSelect: func.isRequired, + + options: arrayOf( + shape({ + key: string.isRequired, + label: string.isRequired, + }) + ).isRequired, + initialValue: string, + twoColumns: bool, + useBullets: bool, +}; + +export default SelectSingleFilterPlain; diff --git a/src/components/SelectSingleFilter/SelectSingleFilterPopup.css b/src/components/SelectSingleFilter/SelectSingleFilterPopup.css new file mode 100644 index 00000000..06edfcff --- /dev/null +++ b/src/components/SelectSingleFilter/SelectSingleFilterPopup.css @@ -0,0 +1,127 @@ +@import '../../marketplace.css'; + +.root { + display: inline-block; + + &:last-of-type { + padding-right: 0; + } +} + +.menuLabel { + @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); + } +} + +.menuLabelSelected { + @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); + } +} + +.menuContent { + margin-top: 7px; + padding-top: 13px; + min-width: 300px; + border-radius: 4px; +} + +/* left animated "border" like hover element */ +.menuItemBorder { + position: absolute; + top: 2px; + left: 0px; + height: calc(100% - 4px); + width: 0; + background-color: var(--marketplaceColor); + transition: width var(--transitionStyleButton); +} + +/* left static border for selected element */ +.menuItemBorderSelected { + position: absolute; + top: 2px; + left: 0px; + height: calc(100% - 7px); + width: 6px; + background-color: var(--matterColorDark); +} + +.menuItem { + @apply --marketplaceListingAttributeFontStyles; + color: var(--matterColor); + + /* Layout */ + position: relative; + min-width: 300px; + margin: 0; + padding: 4px 30px; + + /* Override button styles */ + outline: none; + text-align: left; + border: none; + + cursor: pointer; + + &:focus, + &:hover { + color: var(--matterColorDark); + } + + &:hover .menuItemBorder { + width: 6px; + } +} + +.clearMenuItem { + @apply --marketplaceH4FontStyles; + font-weight: var(--fontWeightMedium); + color: var(--matterColorAnti); + + /* Layout */ + position: relative; + min-width: 300px; + margin: 0; + padding: 32px 30px 18px 30px; + + /* Override button styles */ + outline: none; + text-align: left; + border: none; + + cursor: pointer; + transition: width var(--transitionStyleButton); + + &:focus, + &:hover { + color: var(--matterColor); + transition: width var(--transitionStyleButton); + } +} diff --git a/src/components/SelectSingleFilter/SelectSingleFilterPopup.js b/src/components/SelectSingleFilter/SelectSingleFilterPopup.js new file mode 100644 index 00000000..bcce2899 --- /dev/null +++ b/src/components/SelectSingleFilter/SelectSingleFilterPopup.js @@ -0,0 +1,111 @@ +import React, { Component } from 'react'; +import { string, func, arrayOf, shape, number } from 'prop-types'; +import { FormattedMessage } from 'react-intl'; +import classNames from 'classnames'; + +import { Menu, MenuContent, MenuItem, MenuLabel } from '..'; +import css from './SelectSingleFilterPopup.css'; + +const optionLabel = (options, key) => { + const option = options.find(o => o.key === key); + return option ? option.label : key; +}; + +class SelectSingleFilterPopup extends Component { + constructor(props) { + super(props); + + this.state = { isOpen: false }; + this.onToggleActive = this.onToggleActive.bind(this); + this.selectOption = this.selectOption.bind(this); + } + + onToggleActive(isOpen) { + this.setState({ isOpen: isOpen }); + } + + selectOption(urlParam, option) { + this.setState({ isOpen: false }); + this.props.onSelect(urlParam, option); + } + + render() { + const { + rootClassName, + className, + urlParam, + label, + options, + initialValue, + contentPlacementOffset, + } = this.props; + + // resolve menu label text and class + const menuLabel = initialValue ? optionLabel(options, initialValue) : label; + const menuLabelClass = initialValue ? css.menuLabelSelected : css.menuLabel; + + const classes = classNames(rootClassName || css.root, className); + + return ( + + {menuLabel} + + {options.map(option => { + // check if this option is selected + const selected = initialValue === option.key; + // menu item border class + const menuItemBorderClass = selected ? css.menuItemBorderSelected : css.menuItemBorder; + + return ( + + + + ); + })} + + + + + + ); + } +} + +SelectSingleFilterPopup.defaultProps = { + rootClassName: null, + className: null, + initialValue: null, + contentPlacementOffset: 0, +}; + +SelectSingleFilterPopup.propTypes = { + rootClassName: string, + className: string, + urlParam: string.isRequired, + label: string.isRequired, + onSelect: func.isRequired, + options: arrayOf( + shape({ + key: string.isRequired, + label: string.isRequired, + }) + ).isRequired, + initialValue: string, + contentPlacementOffset: number, +}; + +export default SelectSingleFilterPopup; diff --git a/src/translations/en.json b/src/translations/en.json index d2d15955..8ffe89d4 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -691,8 +691,8 @@ "SelectMultipleFilter.labelSelected": "{labelText} • {count}", "SelectMultipleFilterPlainForm.clear": "Clear", "SelectMultipleFilterPlainForm.labelSelected": "{labelText} • {count}", - "SelectSingleFilter.clear": "Clear", - "SelectSingleFilterPlain.clear": "Clear", + "SelectSingleFilter.popupClear": "Clear", + "SelectSingleFilter.plainClear": "Clear", "SendMessageForm.sendFailed": "Failed to send. Please try again.", "SendMessageForm.sendMessage": "Send message", "SignupForm.emailInvalid": "A valid email address is required",