From 0870f01b13c7423858fa79842d093bf221651964 Mon Sep 17 00:00:00 2001 From: Jenni Nurmi Date: Mon, 12 Nov 2018 11:18:16 +0200 Subject: [PATCH] Create FilterPopup component --- src/components/FilterPopup/FilterPopup.css | 77 +++++++ .../FilterPopup/FilterPopup.example.js | 25 +++ src/components/FilterPopup/FilterPopup.js | 188 ++++++++++++++++++ src/components/index.js | 1 + src/examples.js | 2 + 5 files changed, 293 insertions(+) create mode 100644 src/components/FilterPopup/FilterPopup.css create mode 100644 src/components/FilterPopup/FilterPopup.example.js create mode 100644 src/components/FilterPopup/FilterPopup.js 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,