Create FilterPopup component

This commit is contained in:
Jenni Nurmi 2018-11-12 11:18:16 +02:00 committed by Vesa Luusua
parent b928c95a3b
commit 0870f01b13
5 changed files with 293 additions and 0 deletions

View file

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

View file

@ -0,0 +1,25 @@
import React from 'react';
import { FieldTextInput } from '../../components';
import FilterPopup from './FilterPopup';
const id = 'FilterPopupExample';
const field = <FieldTextInput type="text" id={`${id}.input1`} name="input1" label="Input:" />;
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',
};

View file

@ -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 (
<div
className={classes}
onBlur={this.handleBlur}
onKeyDown={this.handleKeyDown}
ref={node => {
this.filter = node;
}}
>
<button className={labelStyles} onClick={() => this.toggleOpen()}>
{label}
</button>
<div
id={id}
className={popupClasses}
ref={node => {
this.filterContent = node;
}}
style={contentStyle}
>
{this.state.isOpen ? (
<FilterForm
id={`${id}.form`}
paddingClasses={popupSizeClasses}
showAsPopup
contentPlacementOffset={contentPlacementOffset}
initialValues={initialValues}
onSubmit={this.handleSubmit}
onCancel={this.handleCancel}
onClear={this.handleClear}
>
{children}
</FilterForm>
) : null}
</div>
</div>
);
}
}
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);

View file

@ -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';

View file

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