Refactor SelectSingleFilter to use subcomponents SelectSingleFilterPlain and SelectSingleFilterPopup

This commit is contained in:
Jenni Nurmi 2018-11-21 14:51:52 +02:00 committed by Vesa Luusua
parent d89aa64e57
commit ac3db9efa3
8 changed files with 529 additions and 105 deletions

View file

@ -152,6 +152,7 @@ const SearchFiltersComponent = props => {
urlParam={categoryFilter.paramName}
label={categoryLabel}
onSelect={handleSelectOption}
showAsPopup
options={categoryFilter.options}
initialValue={initialCategory}
contentPlacementOffset={FILTER_DROPDOWN_OFFSET}

View file

@ -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 ? (
<SelectSingleFilterPlain
<SelectSingleFilter
urlParam={categoryFilter.paramName}
label={categoryLabel}
onSelect={this.handleSelectSingle}
liveEdit
options={categoryFilter.options}
initialValue={initialCategory}
intl={intl}

View file

@ -1,111 +1,23 @@
import React, { Component } from 'react';
import { string, func, arrayOf, shape, number } from 'prop-types';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import React from 'react';
import { bool } from 'prop-types';
import SelectSingleFilterPlain from './SelectSingleFilterPlain';
import SelectSingleFilterPopup from './SelectSingleFilterPopup';
import { Menu, MenuContent, MenuItem, MenuLabel } from '../../components';
import css from './SelectSingleFilter.css';
const optionLabel = (options, key) => {
const option = options.find(o => o.key === key);
return option ? option.label : key;
const SelectSingleFilter = props => {
const { showAsPopup, ...rest } = props;
return showAsPopup ? (
<SelectSingleFilterPopup {...rest} />
) : (
<SelectSingleFilterPlain {...rest} />
);
};
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 (
<Menu
className={classes}
useArrow={false}
contentPlacementOffset={contentPlacementOffset}
onToggleActive={this.onToggleActive}
isOpen={this.state.isOpen}
>
<MenuLabel className={menuLabelClass}>{menuLabel}</MenuLabel>
<MenuContent className={css.menuContent}>
{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 (
<MenuItem key={option.key}>
<button
className={css.menuItem}
onClick={() => this.selectOption(urlParam, option.key)}
>
<span className={menuItemBorderClass} />
{option.label}
</button>
</MenuItem>
);
})}
<MenuItem key={'clearLink'}>
<button className={css.clearMenuItem} onClick={() => this.selectOption(urlParam, null)}>
<FormattedMessage id={'SelectSingleFilter.clear'} />
</button>
</MenuItem>
</MenuContent>
</Menu>
);
}
}
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;

View file

@ -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);
}
}

View file

@ -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 (
<div className={classes}>
<div className={labelClass}>
<button className={css.labelButton} onClick={this.toggleIsOpen}>
<span className={labelClass}>{label}</span>
</button>
<button className={css.clearButton} onClick={e => this.selectOption(null, e)}>
<FormattedMessage id={'SelectSingleFilter.plainClear'} />
</button>
</div>
<div className={optionsContainerClass}>
{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 (
<button
key={option.key}
className={optionClass}
onClick={() => this.selectOption(option.key)}
>
<span className={optionBorderClass} />
{option.label}
</button>
);
})}
</div>
</div>
);
}
}
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;

View file

@ -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);
}
}

View file

@ -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 (
<Menu
className={classes}
useArrow={false}
contentPlacementOffset={contentPlacementOffset}
onToggleActive={this.onToggleActive}
isOpen={this.state.isOpen}
>
<MenuLabel className={menuLabelClass}>{menuLabel}</MenuLabel>
<MenuContent className={css.menuContent}>
{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 (
<MenuItem key={option.key}>
<button
className={css.menuItem}
onClick={() => this.selectOption(urlParam, option.key)}
>
<span className={menuItemBorderClass} />
{option.label}
</button>
</MenuItem>
);
})}
<MenuItem key={'clearLink'}>
<button className={css.clearMenuItem} onClick={() => this.selectOption(urlParam, null)}>
<FormattedMessage id={'SelectSingleFilter.popupClear'} />
</button>
</MenuItem>
</MenuContent>
</Menu>
);
}
}
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;

View file

@ -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",