From f59a8a15e2f8ad83eb50aa7f2656b2713b91d382 Mon Sep 17 00:00:00 2001 From: Ryan Herman Date: Thu, 13 Mar 2025 15:37:17 -0600 Subject: [PATCH 1/8] Add new min/max search component --- .../stylesheets/components/__components.scss | 1 + .../components/_search-filters-minmax.scss | 90 ++++++++ src/components/SearchFilters/SearchFilters.js | 15 ++ .../SearchFiltersMinMax.js | 199 ++++++++++++++++++ .../SearchFiltersMinMax.test.js | 15 ++ src/components/SearchFiltersMinMax/index.js | 1 + .../SearchFiltersMinMax.Default.stories.js | 33 +++ 7 files changed, 354 insertions(+) create mode 100644 src/assets/stylesheets/components/_search-filters-minmax.scss create mode 100644 src/components/SearchFiltersMinMax/SearchFiltersMinMax.js create mode 100644 src/components/SearchFiltersMinMax/SearchFiltersMinMax.test.js create mode 100644 src/components/SearchFiltersMinMax/index.js create mode 100644 src/components/SearchFiltersMinMax/stories/SearchFiltersMinMax.Default.stories.js 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..c11eeec --- /dev/null +++ b/src/components/SearchFiltersMinMax/SearchFiltersMinMax.js @@ -0,0 +1,199 @@ +import React, { useState, useEffect } from 'react'; +import PropTypes from 'prop-types'; + +import { chompFloat } from '../../lib/util'; + +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(() => { + if (maxError === true) { + handleInputChange({ target: { id: `${namePrefix}-max`, value: valueMaxLocal.toString() } }); + } + if (minError === true) { + handleInputChange({ target: { id: `${namePrefix}-min`, value: valueMinLocal.toString() } }); + } + }, [valueMinLocal, valueMaxLocal]); + + useEffect(() => { + const { min, max } = value; + if ((min && (valueMinLocal !== min)) || (max && (valueMaxLocal !== max))) { + setValueMinLocal(min); + setValueMaxLocal(max); + } + }, [value]); + + /** + * handleInputChange + * @description When the text inputs change, fire away + */ + + function handleInputChange ({ target = {} } = {}) { + const { id: targetId, value: targetValue } = target; + + if (!targetValue) { + updateMinErrorState(true); + return; + } + + const inputName = targetId.replace(`${namePrefix}-`, ''); + 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; + } + + setValueMaxLocal(valueMaxLocal); + setValueMinLocal(valueMinLocal); + + 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: + } + + handleOnChange({ + ...value, + [inputName]: floatValue + }); + } + + /** + * handleOnChange + * @description Manages all changes to bubble up to the parent component + */ + + function handleOnChange ({ min, max } = {}) { + // Before we update, we want to normalize the values to fix + // them to a maximum of 2 decimal places + + const updatedValue = { + min: typeof min === 'number' ? chompFloat(min, 2) : min, + max: typeof max === 'number' ? chompFloat(max, 2) : max + }; + + // Make sure the min value is normalized and not outside the limits + + if (updatedValue.min < limitsMin.min) { + updatedValue.min = limitsMin.min; + } + + if (updatedValue.min > limitsMin.max) { + updatedValue.min = limitsMax.max; + } + + // Make sure the max value is normalized and not outside the limits + + if (updatedValue.max < limitsMax.min) { + updatedValue.max = limitsMax.min; + } + + if (updatedValue.max > limitsMax.max) { + updatedValue.max = limitsMax.max; + } + + if (typeof onChange === 'function') { + onChange({ + target: { + name: id, + value: updatedValue + } + }); + } + } + + 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..b4b0e41 --- /dev/null +++ b/src/components/SearchFiltersMinMax/SearchFiltersMinMax.test.js @@ -0,0 +1,15 @@ +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 Range = shallow(); + + expect(Range.find('div.search-filters-minmax-input').exists()).toEqual(true); + expect(Range.find('span.error').exists()).toEqual(true); + expect(Range.find('p.search-filters-range-note').text()).toEqual(notice); + }); +}); 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..6a7f3ce --- /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('Default', () => { + return ( + + + + ); +}); From 0b56328e896031c2ae7d65f2b8a1d4153f43d170 Mon Sep 17 00:00:00 2001 From: Ryan Herman Date: Thu, 13 Mar 2025 15:37:37 -0600 Subject: [PATCH 2/8] Fix range filter selector bug --- src/components/SearchFiltersRange/SearchFiltersRange.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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]); From c4cfaac7419e888d02a30cc2cfe5bdab2d22bbe7 Mon Sep 17 00:00:00 2001 From: Ryan Herman Date: Thu, 13 Mar 2025 15:47:44 -0600 Subject: [PATCH 3/8] Fix test naming --- .../SearchFiltersMinMax/SearchFiltersMinMax.test.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/SearchFiltersMinMax/SearchFiltersMinMax.test.js b/src/components/SearchFiltersMinMax/SearchFiltersMinMax.test.js index b4b0e41..362eb20 100644 --- a/src/components/SearchFiltersMinMax/SearchFiltersMinMax.test.js +++ b/src/components/SearchFiltersMinMax/SearchFiltersMinMax.test.js @@ -6,10 +6,10 @@ import SearchFiltersMinMax from './'; describe('SearchFiltersMinMax', () => { const notice = 'Values outside of limits will default to min and max.'; it('renders a SearchFiltersMinMax', () => { - const Range = shallow(); + const MinMax = shallow(); - expect(Range.find('div.search-filters-minmax-input').exists()).toEqual(true); - expect(Range.find('span.error').exists()).toEqual(true); - expect(Range.find('p.search-filters-range-note').text()).toEqual(notice); + 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); }); }); From e40f33ff6254218db7d5e80f2c34be9d7242e550 Mon Sep 17 00:00:00 2001 From: Ryan Herman Date: Tue, 25 Mar 2025 16:02:24 -0600 Subject: [PATCH 4/8] Add error display tests --- .../SearchFiltersMinMax.test.js | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/components/SearchFiltersMinMax/SearchFiltersMinMax.test.js b/src/components/SearchFiltersMinMax/SearchFiltersMinMax.test.js index 362eb20..99ed225 100644 --- a/src/components/SearchFiltersMinMax/SearchFiltersMinMax.test.js +++ b/src/components/SearchFiltersMinMax/SearchFiltersMinMax.test.js @@ -12,4 +12,42 @@ describe('SearchFiltersMinMax', () => { 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); + }); }); From 58d7809ab75a2d1e1dff5fed57744546ec97394a Mon Sep 17 00:00:00 2001 From: Ryan Herman Date: Thu, 27 Mar 2025 09:34:42 -0600 Subject: [PATCH 5/8] Update src/components/SearchFiltersMinMax/SearchFiltersMinMax.js Co-authored-by: Michael Parks --- src/components/SearchFiltersMinMax/SearchFiltersMinMax.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/SearchFiltersMinMax/SearchFiltersMinMax.js b/src/components/SearchFiltersMinMax/SearchFiltersMinMax.js index c11eeec..fc4c846 100644 --- a/src/components/SearchFiltersMinMax/SearchFiltersMinMax.js +++ b/src/components/SearchFiltersMinMax/SearchFiltersMinMax.js @@ -35,11 +35,13 @@ const SearchFiltersMinMax = ({ useEffect(() => { const { min, max } = value; - if ((min && (valueMinLocal !== min)) || (max && (valueMaxLocal !== max))) { + if (valueMinLocal !== min) { setValueMinLocal(min); + } + if (valueMaxLocal !== max) { setValueMaxLocal(max); } - }, [value]); + }, [value, valueMinLocal, valueMaxLocal]); /** * handleInputChange From 545cfe13fd1f9518a14a154957e217408a818eb9 Mon Sep 17 00:00:00 2001 From: Ryan Herman Date: Thu, 27 Mar 2025 09:48:45 -0600 Subject: [PATCH 6/8] Update src/components/SearchFiltersMinMax/stories/SearchFiltersMinMax.Default.stories.js Co-authored-by: Michael Parks --- .../stories/SearchFiltersMinMax.Default.stories.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/SearchFiltersMinMax/stories/SearchFiltersMinMax.Default.stories.js b/src/components/SearchFiltersMinMax/stories/SearchFiltersMinMax.Default.stories.js index 6a7f3ce..868a5f8 100644 --- a/src/components/SearchFiltersMinMax/stories/SearchFiltersMinMax.Default.stories.js +++ b/src/components/SearchFiltersMinMax/stories/SearchFiltersMinMax.Default.stories.js @@ -10,7 +10,7 @@ const STORY_NAME = 'Default'; const stories = storiesOf(`Components/${STORY_COMPONENT}`, module); -stories.add('Default', () => { +stories.add(STORY_NAME, () => { return ( Date: Thu, 27 Mar 2025 09:49:54 -0600 Subject: [PATCH 7/8] MR feedback --- .../SearchFiltersMinMax.js | 64 ++----------------- .../SearchFiltersMinMax.Default.stories.js | 2 +- 2 files changed, 8 insertions(+), 58 deletions(-) diff --git a/src/components/SearchFiltersMinMax/SearchFiltersMinMax.js b/src/components/SearchFiltersMinMax/SearchFiltersMinMax.js index fc4c846..f5b150f 100644 --- a/src/components/SearchFiltersMinMax/SearchFiltersMinMax.js +++ b/src/components/SearchFiltersMinMax/SearchFiltersMinMax.js @@ -1,8 +1,6 @@ import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; -import { chompFloat } from '../../lib/util'; - import FormInputDebounced from '../FormInputDebounced'; const SearchFiltersMinMax = ({ @@ -24,15 +22,6 @@ const SearchFiltersMinMax = ({ const [minError, updateMinErrorState] = useState(false); const [maxError, updateMaxErrorState] = useState(false); - useEffect(() => { - if (maxError === true) { - handleInputChange({ target: { id: `${namePrefix}-max`, value: valueMaxLocal.toString() } }); - } - if (minError === true) { - handleInputChange({ target: { id: `${namePrefix}-min`, value: valueMinLocal.toString() } }); - } - }, [valueMinLocal, valueMaxLocal]); - useEffect(() => { const { min, max } = value; if (valueMinLocal !== min) { @@ -50,13 +39,17 @@ const SearchFiltersMinMax = ({ function handleInputChange ({ target = {} } = {}) { const { id: targetId, value: targetValue } = target; + const inputName = targetId.replace(`${namePrefix}-`, ''); if (!targetValue) { - updateMinErrorState(true); + if (inputName === 'min') { + updateMinErrorState(true); + } else if (inputName === 'max') { + updateMaxErrorState(true); + } return; } - const inputName = targetId.replace(`${namePrefix}-`, ''); const isCompleteFloat = targetValue && targetValue.substr(-1) !== '.'; let floatValue = isCompleteFloat && parseFloat(targetValue); @@ -68,9 +61,6 @@ const SearchFiltersMinMax = ({ floatValue = targetValue; } - setValueMaxLocal(valueMaxLocal); - setValueMinLocal(valueMinLocal); - switch (inputName) { case 'min': setValueMinLocal(floatValue); @@ -93,51 +83,11 @@ const SearchFiltersMinMax = ({ default: } - handleOnChange({ - ...value, - [inputName]: floatValue - }); - } - - /** - * handleOnChange - * @description Manages all changes to bubble up to the parent component - */ - - function handleOnChange ({ min, max } = {}) { - // Before we update, we want to normalize the values to fix - // them to a maximum of 2 decimal places - - const updatedValue = { - min: typeof min === 'number' ? chompFloat(min, 2) : min, - max: typeof max === 'number' ? chompFloat(max, 2) : max - }; - - // Make sure the min value is normalized and not outside the limits - - if (updatedValue.min < limitsMin.min) { - updatedValue.min = limitsMin.min; - } - - if (updatedValue.min > limitsMin.max) { - updatedValue.min = limitsMax.max; - } - - // Make sure the max value is normalized and not outside the limits - - if (updatedValue.max < limitsMax.min) { - updatedValue.max = limitsMax.min; - } - - if (updatedValue.max > limitsMax.max) { - updatedValue.max = limitsMax.max; - } - if (typeof onChange === 'function') { onChange({ target: { name: id, - value: updatedValue + value: floatValue } }); } diff --git a/src/components/SearchFiltersMinMax/stories/SearchFiltersMinMax.Default.stories.js b/src/components/SearchFiltersMinMax/stories/SearchFiltersMinMax.Default.stories.js index 868a5f8..b9b4966 100644 --- a/src/components/SearchFiltersMinMax/stories/SearchFiltersMinMax.Default.stories.js +++ b/src/components/SearchFiltersMinMax/stories/SearchFiltersMinMax.Default.stories.js @@ -5,7 +5,7 @@ import Story from '../../../../stories/helpers/Story'; import SearchFiltersMinMax from '../SearchFiltersMinMax'; -const STORY_COMPONENT = 'Search Filters Min/Max'; +const STORY_COMPONENT = 'Search Filters Min-Max'; const STORY_NAME = 'Default'; const stories = storiesOf(`Components/${STORY_COMPONENT}`, module); From 1a265875e497d66a72e43ecb8d353b946a6c55e3 Mon Sep 17 00:00:00 2001 From: Michael Parks Date: Wed, 2 Apr 2025 13:38:22 -0400 Subject: [PATCH 8/8] Update src/components/SearchFiltersMinMax/SearchFiltersMinMax.js --- src/components/SearchFiltersMinMax/SearchFiltersMinMax.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/SearchFiltersMinMax/SearchFiltersMinMax.js b/src/components/SearchFiltersMinMax/SearchFiltersMinMax.js index f5b150f..57c7fda 100644 --- a/src/components/SearchFiltersMinMax/SearchFiltersMinMax.js +++ b/src/components/SearchFiltersMinMax/SearchFiltersMinMax.js @@ -30,7 +30,7 @@ const SearchFiltersMinMax = ({ if (valueMaxLocal !== max) { setValueMaxLocal(max); } - }, [value, valueMinLocal, valueMaxLocal]); + }, [value]); /** * handleInputChange