diff --git a/src/assets/stylesheets/components/__components.scss b/src/assets/stylesheets/components/__components.scss
index 5fdf73f..5222ce0 100644
--- a/src/assets/stylesheets/components/__components.scss
+++ b/src/assets/stylesheets/components/__components.scss
@@ -30,6 +30,7 @@
@import "search-complete";
@import "search-filters";
@import "search-filters-range";
+@import "search-filters-minmax";
@import "search-panel-filters";
@import "select";
@import "status-indicator";
diff --git a/src/assets/stylesheets/components/_search-filters-minmax.scss b/src/assets/stylesheets/components/_search-filters-minmax.scss
new file mode 100644
index 0000000..becda3f
--- /dev/null
+++ b/src/assets/stylesheets/components/_search-filters-minmax.scss
@@ -0,0 +1,90 @@
+.search-filters-minmax {}
+
+.search-filters-minmax-input {
+
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+
+ .form-input {
+
+ padding: 0 .25em;
+ flex: 1 0 50%;
+
+ &:first-child {
+ margin-left: -.25em;
+ }
+
+ &:last-child {
+ margin-right: -.25em;
+ }
+
+ }
+
+ &.min-error {
+
+ .form-input {
+
+ &:first-child {
+
+ .form-input-field {
+ border: 2px $color-red solid;
+ }
+
+ }
+
+ }
+
+ }
+
+ &.max-error {
+
+ .form-input {
+
+ &:nth-child(2) {
+
+ .form-input-field {
+ border: 2px $color-red solid;
+ }
+
+ }
+
+ }
+
+ span.error {
+
+ &:last-child {
+ padding: 0 .25em;
+ margin-left: 0em;
+ }
+
+ }
+
+ }
+
+ span.error {
+
+ flex: 1 0 50%;
+ color: $color-red;
+ font-size: .6rem;
+
+ }
+
+ .form-label {
+ color: $color-gray6;
+ font-size: .8em;
+ font-weight: bold;
+ padding: 0;
+ }
+
+ .form-input-field {
+ border: solid 2px $color-gray3;
+ }
+
+}
+
+.search-filters-minmax-note {
+ font-size: .9em;
+ color: $color-gray5;
+ padding-top: .6em;
+}
diff --git a/src/components/SearchFilters/SearchFilters.js b/src/components/SearchFilters/SearchFilters.js
index c1f3d49..fb570c4 100644
--- a/src/components/SearchFilters/SearchFilters.js
+++ b/src/components/SearchFilters/SearchFilters.js
@@ -9,6 +9,7 @@ import InputButton from '../InputButton';
import Button from '../Button';
import SearchFiltersList from '../SearchFiltersList';
import SearchFiltersRange from '../SearchFiltersRange';
+import SearchFiltersMinMax from '../SearchFiltersMinMax';
import { ALL_VALUES_ITEM } from '../../data/search-filters';
const SearchFilters = ({
@@ -179,6 +180,20 @@ const SearchFilters = ({
);
})()}
+ {type === 'minmax' &&
+ (() => {
+ return (
+
+ );
+ })()}
+
{(!type || type === 'default') &&
(() => {
return (
diff --git a/src/components/SearchFiltersMinMax/SearchFiltersMinMax.js b/src/components/SearchFiltersMinMax/SearchFiltersMinMax.js
new file mode 100644
index 0000000..57c7fda
--- /dev/null
+++ b/src/components/SearchFiltersMinMax/SearchFiltersMinMax.js
@@ -0,0 +1,151 @@
+import React, { useState, useEffect } from 'react';
+import PropTypes from 'prop-types';
+
+import FormInputDebounced from '../FormInputDebounced';
+
+const SearchFiltersMinMax = ({
+ id,
+ label,
+ subLabel,
+ onChange,
+ value = {},
+ limits = {}
+}) => {
+ const { min: valueMin, max: valueMax } = value;
+ const { min: limitsMin, max: limitsMax } = limits;
+
+ const namePrefix = `${id}-minmax`;
+
+ const [valueMinLocal, setValueMinLocal] = useState(valueMin);
+ const [valueMaxLocal, setValueMaxLocal] = useState(valueMax);
+
+ const [minError, updateMinErrorState] = useState(false);
+ const [maxError, updateMaxErrorState] = useState(false);
+
+ useEffect(() => {
+ const { min, max } = value;
+ if (valueMinLocal !== min) {
+ setValueMinLocal(min);
+ }
+ if (valueMaxLocal !== max) {
+ setValueMaxLocal(max);
+ }
+ }, [value]);
+
+ /**
+ * handleInputChange
+ * @description When the text inputs change, fire away
+ */
+
+ function handleInputChange ({ target = {} } = {}) {
+ const { id: targetId, value: targetValue } = target;
+ const inputName = targetId.replace(`${namePrefix}-`, '');
+
+ if (!targetValue) {
+ if (inputName === 'min') {
+ updateMinErrorState(true);
+ } else if (inputName === 'max') {
+ updateMaxErrorState(true);
+ }
+ return;
+ }
+
+ const isCompleteFloat = targetValue && targetValue.substr(-1) !== '.';
+
+ let floatValue = isCompleteFloat && parseFloat(targetValue);
+
+ // If we dont have an actual number, reset back to the value so we can
+ // avoid overwriting WIP numbers such as 0.
+
+ if (typeof floatValue !== 'number') {
+ floatValue = targetValue;
+ }
+
+ switch (inputName) {
+ case 'min':
+ setValueMinLocal(floatValue);
+ if (floatValue < limitsMin.min || floatValue > limitsMin.max) {
+ updateMinErrorState(true);
+ return;
+ } else {
+ updateMinErrorState(false);
+ }
+ break;
+ case 'max':
+ setValueMaxLocal(floatValue);
+ if (floatValue > limitsMax.max || floatValue < limitsMax.min) {
+ updateMaxErrorState(true);
+ return;
+ } else {
+ updateMaxErrorState(false);
+ }
+ break;
+ default:
+ }
+
+ if (typeof onChange === 'function') {
+ onChange({
+ target: {
+ name: id,
+ value: floatValue
+ }
+ });
+ }
+ }
+
+ return (
+ <>
+ {(label || subLabel) && (
+
+ {label &&
{label}}
+ {subLabel && (
+
{subLabel}
+ )}
+
+ )}
+
+
+
+
+ {`${minError ? 'Invalid Min Value' : ''}`}
+ {`${maxError ? 'Invalid Max Value' : ''}`}
+
+
+ Values outside of limits will default to min and max.
+
+
+ >
+ );
+};
+
+SearchFiltersMinMax.propTypes = {
+ id: PropTypes.string,
+ label: PropTypes.string,
+ subLabel: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
+ onChange: PropTypes.func,
+ value: PropTypes.object,
+ limits: PropTypes.shape({
+ min: PropTypes.shape({
+ min: PropTypes.number,
+ max: PropTypes.number
+ }),
+ max: PropTypes.shape({
+ min: PropTypes.number,
+ max: PropTypes.number
+ })
+ })
+};
+
+export default SearchFiltersMinMax;
diff --git a/src/components/SearchFiltersMinMax/SearchFiltersMinMax.test.js b/src/components/SearchFiltersMinMax/SearchFiltersMinMax.test.js
new file mode 100644
index 0000000..99ed225
--- /dev/null
+++ b/src/components/SearchFiltersMinMax/SearchFiltersMinMax.test.js
@@ -0,0 +1,53 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+
+import SearchFiltersMinMax from './';
+
+describe('SearchFiltersMinMax', () => {
+ const notice = 'Values outside of limits will default to min and max.';
+ it('renders a SearchFiltersMinMax', () => {
+ const MinMax = shallow();
+
+ expect(MinMax.find('div.search-filters-minmax-input').exists()).toEqual(true);
+ expect(MinMax.find('span.error').exists()).toEqual(true);
+ expect(MinMax.find('p.search-filters-minmax-note').text()).toEqual(notice);
+ });
+
+ it('displays an error when min is out of range', () => {
+ const limits = {
+ min: {
+ min: 0,
+ max: 25
+ },
+ max: {
+ min: 0,
+ max: 50
+ }
+ };
+
+ const MinMax = shallow();
+ MinMax.find('#test-minmax-min').simulate('change', { target: { id: 'test-minmax-min', value: '30' } });
+ MinMax.update();
+
+ expect(MinMax.findWhere(node => node.text() === 'Invalid Min Value').exists()).toEqual(true);
+ });
+
+ it('displays an error when max is out of range', () => {
+ const limits = {
+ min: {
+ min: 0,
+ max: 25
+ },
+ max: {
+ min: 0,
+ max: 50
+ }
+ };
+
+ const MinMax = shallow();
+ MinMax.find('#test-minmax-max').simulate('change', { target: { id: 'test-minmax-max', value: '55' } });
+ MinMax.update();
+
+ expect(MinMax.findWhere(node => node.text() === 'Invalid Max Value').exists()).toEqual(true);
+ });
+});
diff --git a/src/components/SearchFiltersMinMax/index.js b/src/components/SearchFiltersMinMax/index.js
new file mode 100644
index 0000000..550c224
--- /dev/null
+++ b/src/components/SearchFiltersMinMax/index.js
@@ -0,0 +1 @@
+export { default } from './SearchFiltersMinMax';
diff --git a/src/components/SearchFiltersMinMax/stories/SearchFiltersMinMax.Default.stories.js b/src/components/SearchFiltersMinMax/stories/SearchFiltersMinMax.Default.stories.js
new file mode 100644
index 0000000..b9b4966
--- /dev/null
+++ b/src/components/SearchFiltersMinMax/stories/SearchFiltersMinMax.Default.stories.js
@@ -0,0 +1,33 @@
+import React from 'react';
+import { storiesOf } from '@storybook/react';
+
+import Story from '../../../../stories/helpers/Story';
+
+import SearchFiltersMinMax from '../SearchFiltersMinMax';
+
+const STORY_COMPONENT = 'Search Filters Min-Max';
+const STORY_NAME = 'Default';
+
+const stories = storiesOf(`Components/${STORY_COMPONENT}`, module);
+
+stories.add(STORY_NAME, () => {
+ return (
+
+
+
+ );
+});
diff --git a/src/components/SearchFiltersRange/SearchFiltersRange.js b/src/components/SearchFiltersRange/SearchFiltersRange.js
index 9dfc3b1..abe4896 100644
--- a/src/components/SearchFiltersRange/SearchFiltersRange.js
+++ b/src/components/SearchFiltersRange/SearchFiltersRange.js
@@ -29,10 +29,10 @@ const SearchFiltersRange = ({
useEffect(() => {
if (maxError === true) {
- handleInputChange({ target: { id: 'incidence_angle-range-max', value: valueMaxLocal.toString() } });
+ handleInputChange({ target: { id: `${namePrefix}-max`, value: valueMaxLocal.toString() } });
}
if (minError === true) {
- handleInputChange({ target: { id: 'incidence_angle-range-min', value: valueMinLocal.toString() } });
+ handleInputChange({ target: { id: `${namePrefix}-min`, value: valueMinLocal.toString() } });
}
}, [valueMinLocal, valueMaxLocal, rangeValue]);