diff --git a/src/actions/filter.js b/src/actions/filter.js
index 605884e2a..1dcc2ea0e 100644
--- a/src/actions/filter.js
+++ b/src/actions/filter.js
@@ -112,7 +112,7 @@ export function addFilter(filterName, filterValue) {
*
* @param {string} filterName - which filter was clicked
* @param {string} filterValue - the value of the filter that was clicked
- * @returns {string} a packaged payload to be used by Redux reducers
+ * @returns {object} a packaged payload to be used by Redux reducers
*/
export function removeFilter(filterName, filterValue) {
return {
diff --git a/src/components/Trends/ExternalTooltip.js b/src/components/Trends/ExternalTooltip.js
deleted file mode 100644
index d8c13ade5..000000000
--- a/src/components/Trends/ExternalTooltip.js
+++ /dev/null
@@ -1,160 +0,0 @@
-/* eslint complexity: ["error", 5] */
-import { CompanyTypeahead } from '../Filters/CompanyTypeahead';
-import { connect } from 'react-redux';
-import { externalTooltipFormatter } from '../../utils/chart';
-import getIcon from '../iconMap';
-import PropTypes from 'prop-types';
-import React from 'react';
-import { removeFilter } from '../../actions/filter';
-import { sanitizeHtmlId } from '../../utils';
-
-const WARN_SERIES_BREAK =
- 'CFPB updated product and issue options in April 2017 and August 2023.';
-
-const LEARN_SERIES_BREAK =
- 'https://www.consumerfinance.gov/data-research/consumer-complaints/#past-changes';
-
-export class ExternalTooltip extends React.Component {
- _spanFormatter(value) {
- const { focus, lens, hasCompanyTypeahead, subLens } = this.props;
- const elements = [];
- const lensToUse = focus ? subLens : lens;
- const plurals = {
- Product: 'products',
- product: 'products',
- issue: 'issues',
- 'Sub-Issue': 'sub-issues',
- sub_product: 'sub-products',
- Company: 'companies',
- };
-
- // Other should never be a selectable focus item
- if (value.name === 'Other') {
- elements.push(
-
- All other {plurals[lensToUse]}
- ,
- );
- return elements;
- }
-
- if (focus) {
- elements.push(
-
- {value.name}
- ,
- );
- return elements;
- }
-
- elements.push(
-
- {value.name}
- ,
- );
-
- // add in the close button for Company and there's no focus yet
- if (hasCompanyTypeahead) {
- elements.push(
- ,
- );
- }
-
- return elements;
- }
-
- render() {
- const { focus, hasTotal, tooltip } = this.props;
- if (tooltip && tooltip.values) {
- return (
-
- {!!this.props.hasCompanyTypeahead && (
-
- )}
-
- {this.props.tooltip.heading}
- {this.props.tooltip.date}
-
-
-
- {tooltip.values.map((val, key) => (
- -
- {this._spanFormatter(val)}
- {val.value.toLocaleString()}
-
- ))}
-
-
- {!!hasTotal && (
-
- -
- Total
-
- {tooltip.total.toLocaleString()}
-
-
-
- )}
-
-
- {WARN_SERIES_BREAK}{' '}
-
- Learn More
-
-
-
- );
- }
- return null;
- }
-}
-
-export const mapDispatchToProps = (dispatch) => ({
- remove: (value) => {
- dispatch(removeFilter('company', value));
- },
-});
-
-export const mapStateToProps = (state) => {
- const { focus, lens, subLens } = state.query;
- const { chartType, tooltip } = state.trends;
- return {
- focus: focus ? 'focus' : '',
- lens,
- subLens,
- hasCompanyTypeahead: lens === 'Company' && !focus,
- hasTotal: chartType === 'area',
- tooltip: externalTooltipFormatter(tooltip),
- };
-};
-
-// eslint-disable-next-line react-redux/prefer-separate-component-file
-export default connect(mapStateToProps, mapDispatchToProps)(ExternalTooltip);
-
-ExternalTooltip.propTypes = {
- focus: PropTypes.string,
- lens: PropTypes.string.isRequired,
- hasCompanyTypeahead: PropTypes.bool.isRequired,
- subLens: PropTypes.string,
- remove: PropTypes.func.isRequired,
- hasTotal: PropTypes.bool,
- tooltip: PropTypes.oneOfType([PropTypes.bool, PropTypes.object]).isRequired,
-};
diff --git a/src/components/Trends/ExternalTooltip/ExternalTooltip.js b/src/components/Trends/ExternalTooltip/ExternalTooltip.js
new file mode 100644
index 000000000..a01712fc3
--- /dev/null
+++ b/src/components/Trends/ExternalTooltip/ExternalTooltip.js
@@ -0,0 +1,74 @@
+import { CompanyTypeahead } from '../../Filters/CompanyTypeahead';
+import { useSelector } from 'react-redux';
+import { TooltipRow } from './TooltipRow';
+import {
+ selectQueryFocus,
+ selectQueryLens,
+} from '../../../reducers/query/selectors';
+import {
+ selectTrendsChartType,
+ selectTrendsTooltip,
+} from '../../../reducers/trends/selectors';
+import { externalTooltipFormatter } from '../../../utils/chart';
+
+const WARN_SERIES_BREAK =
+ 'CFPB updated product and issue options in April 2017 and August 2023.';
+
+const LEARN_SERIES_BREAK =
+ 'https://www.consumerfinance.gov/data-research/consumer-complaints/#past-changes';
+
+export const ExternalTooltip = () => {
+ const trendsFocus = useSelector(selectQueryFocus);
+ const focus = trendsFocus ? 'focus' : '';
+ const lens = useSelector(selectQueryLens);
+ const chartType = useSelector(selectTrendsChartType);
+ const tip = useSelector(selectTrendsTooltip);
+ const hasCompanyTypeahead = lens === 'Company' && !focus;
+ const hasTotal = chartType === 'area';
+ const tooltip = externalTooltipFormatter(tip);
+ if (tooltip && tooltip.values) {
+ return (
+
+ {!!hasCompanyTypeahead && }
+
+ {tooltip.heading}
+ {tooltip.date}
+
+
+
+ {tooltip.values.map((val, key) => (
+ -
+
+ {val.value.toLocaleString()}
+
+ ))}
+
+
+ {!!hasTotal && (
+
+ -
+ Total
+
+ {tooltip.total.toLocaleString()}
+
+
+
+ )}
+
+
+ {WARN_SERIES_BREAK}{' '}
+
+ Learn More
+
+
+
+ );
+ }
+ return null;
+};
diff --git a/src/components/Trends/ExternalTooltip/ExternalTooltip.spec.js b/src/components/Trends/ExternalTooltip/ExternalTooltip.spec.js
new file mode 100644
index 000000000..23119482c
--- /dev/null
+++ b/src/components/Trends/ExternalTooltip/ExternalTooltip.spec.js
@@ -0,0 +1,105 @@
+import { ExternalTooltip } from './ExternalTooltip';
+import { merge } from '../../../testUtils/functionHelpers';
+import { defaultQuery } from '../../../reducers/query/query';
+import { defaultTrends } from '../../../reducers/trends/trends';
+import * as filterActions from '../../../actions/filter';
+import { testRender as render, screen } from '../../../testUtils/test-utils';
+import userEvent from '@testing-library/user-event';
+
+const renderComponent = (newQueryState, newTrendsState) => {
+ merge(newTrendsState, defaultQuery);
+ merge(newTrendsState, defaultTrends);
+
+ const data = {
+ query: newQueryState,
+ trends: newTrendsState,
+ };
+
+ render(, {
+ preloadedState: data,
+ });
+};
+
+jest.useRealTimers();
+
+describe('component: ExternalTooltip', () => {
+ const user = userEvent.setup();
+ test('empty rendering', () => {
+ renderComponent({}, {});
+ expect(screen.queryByText('foobar')).toBeNull();
+ });
+ test('rendering', () => {
+ const query = {
+ focus: '',
+ lens: '',
+ };
+ const trends = {
+ tooltip: {
+ title: 'Date Range: 1/1/1900 - 1/1/2000',
+ total: 2900,
+ values: [
+ { colorIndex: 1, name: 'foo', value: 1000 },
+ { colorIndex: 2, name: 'bar', value: 1000 },
+ { colorIndex: 3, name: 'All other', value: 900 },
+ { colorIndex: 4, name: "Eat at Joe's", value: 1000 },
+ ],
+ },
+ };
+ renderComponent(query, trends);
+ expect(screen.getByText('foo')).toBeInTheDocument();
+ expect(screen.getByText('Date Range:')).toBeInTheDocument();
+ expect(screen.getByText('1/1/1900 - 1/1/2000')).toBeInTheDocument();
+ expect(screen.getByText('900')).toBeInTheDocument();
+ });
+
+ test('rendering Company typeahead', async () => {
+ const filterRemovedSpy = jest.spyOn(filterActions, 'removeFilter');
+ const query = { focus: '', lens: 'Company' };
+ const trends = {
+ tooltip: {
+ title: 'Date Range: 1/1/1900 - 1/1/2000',
+ total: 2900,
+ values: [
+ { colorIndex: 1, name: 'foo', value: 1000 },
+ { colorIndex: 2, name: 'bar', value: 1000 },
+ { colorIndex: 3, name: 'All other', value: 900 },
+ { colorIndex: 4, name: "Eat at Joe's", value: 1000 },
+ ],
+ },
+ };
+ renderComponent(query, trends);
+ expect(screen.getByText('foo')).toBeInTheDocument();
+ expect(
+ screen.getByPlaceholderText('Enter company name'),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole('button', { name: 'Remove foo from comparison set' }),
+ ).toBeInTheDocument();
+ await user.click(
+ screen.getByRole('button', { name: 'Remove foo from comparison set' }),
+ );
+ expect(filterRemovedSpy).toHaveBeenCalledWith('company', 'foo');
+ });
+
+ test('hide Company typeahead in company Focus Mode', () => {
+ const query = {
+ focus: 'Acme Foobar',
+ lens: 'Company',
+ };
+ const trends = {
+ tooltip: {
+ title: 'Date Range: 1/1/1900 - 1/1/2000',
+ total: 2900,
+ values: [
+ { colorIndex: 1, name: 'foo', value: 1000 },
+ { colorIndex: 2, name: 'bar', value: 1000 },
+ { colorIndex: 3, name: 'All other', value: 900 },
+ { colorIndex: 4, name: "Eat at Joe's", value: 1000 },
+ ],
+ },
+ };
+ renderComponent(query, trends);
+ expect(screen.getByText('foo')).toBeInTheDocument();
+ expect(screen.queryByPlaceholderText('Enter company name')).toBeNull();
+ });
+});
diff --git a/src/components/Trends/ExternalTooltip/TooltipRow.js b/src/components/Trends/ExternalTooltip/TooltipRow.js
new file mode 100644
index 000000000..a9ebc745c
--- /dev/null
+++ b/src/components/Trends/ExternalTooltip/TooltipRow.js
@@ -0,0 +1,75 @@
+import { useDispatch, useSelector } from 'react-redux';
+import { removeFilter } from '../../../actions/filter';
+import {
+ selectQueryFocus,
+ selectQueryLens,
+ selectQuerySubLens,
+} from '../../../reducers/query/selectors';
+import { sanitizeHtmlId } from '../../../utils';
+import getIcon from '../../iconMap';
+
+export const TooltipRow = ({ value }) => {
+ const dispatch = useDispatch();
+ const trendsFocus = useSelector(selectQueryFocus);
+ const focus = trendsFocus ? 'focus' : '';
+ const lens = useSelector(selectQueryLens);
+ const subLens = useSelector(selectQuerySubLens);
+ const hasCompanyTypeahead = lens === 'Company' && !focus;
+ const elements = [];
+ const lensToUse = focus ? subLens : lens;
+ const plurals = {
+ Product: 'products',
+ product: 'products',
+ issue: 'issues',
+ 'Sub-Issue': 'sub-issues',
+ sub_product: 'sub-products',
+ Company: 'companies',
+ };
+
+ // Other should never be a selectable focus item
+ if (value.name === 'Other') {
+ elements.push(
+
+ All other {plurals[lensToUse]}
+ ,
+ );
+ return elements;
+ }
+
+ if (focus) {
+ elements.push(
+
+ {value.name}
+ ,
+ );
+ return elements;
+ }
+
+ elements.push(
+
+ {value.name}
+ ,
+ );
+
+ // add in the close button for Company and there's no focus yet
+ if (hasCompanyTypeahead) {
+ elements.push(
+ ,
+ );
+ }
+
+ return elements;
+};
diff --git a/src/components/Trends/TrendsPanel.js b/src/components/Trends/TrendsPanel.js
index 156f0cbe9..fee1be08d 100644
--- a/src/components/Trends/TrendsPanel.js
+++ b/src/components/Trends/TrendsPanel.js
@@ -10,7 +10,7 @@ import { changeDateInterval } from '../../actions/filter';
import { ChartToggles } from '../RefineBar/ChartToggles';
import { CompanyTypeahead } from '../Filters/CompanyTypeahead';
import { connect } from 'react-redux';
-import ExternalTooltip from './ExternalTooltip';
+import { ExternalTooltip } from './ExternalTooltip/ExternalTooltip';
import { FilterPanel } from '../Filters/FilterPanel';
import { FilterPanelToggle } from '../Filters/FilterPanelToggle';
import { FocusHeader } from './FocusHeader';
diff --git a/src/components/__tests__/ExternalTooltip.spec.js b/src/components/__tests__/ExternalTooltip.spec.js
deleted file mode 100644
index 61244d454..000000000
--- a/src/components/__tests__/ExternalTooltip.spec.js
+++ /dev/null
@@ -1,208 +0,0 @@
-import * as trendsUtils from '../../utils/trends';
-import ReduxExternalTooltip, {
- ExternalTooltip,
- mapDispatchToProps,
- mapStateToProps,
-} from '../Trends/ExternalTooltip';
-import React from 'react';
-import { Provider } from 'react-redux';
-import configureMockStore from 'redux-mock-store';
-import thunk from 'redux-thunk';
-import renderer from 'react-test-renderer';
-import { shallow } from 'enzyme';
-
-/**
- *
- * @param {object} query - Query object
- * @param {object} tooltip - Tooltip object
- * @returns {void}
- */
-function setupSnapshot(query, tooltip) {
- const middlewares = [thunk];
- const mockStore = configureMockStore(middlewares);
-
- const store = mockStore({
- query,
- trends: {
- chartType: 'area',
- tooltip,
- },
- });
-
- return renderer.create(
-
-
- ,
- );
-}
-
-xdescribe('initial state', () => {
- let query, tooltip;
- beforeEach(() => {
- query = {
- focus: '',
- lens: '',
- };
- tooltip = {
- title: 'Date Range: 1/1/1900 - 1/1/2000',
- total: 2900,
- values: [
- { colorIndex: 1, name: 'foo', value: 1000 },
- { colorIndex: 2, name: 'bar', value: 1000 },
- { colorIndex: 3, name: 'All other', value: 900 },
- { colorIndex: 4, name: "Eat at Joe's", value: 1000 },
- ],
- };
- });
- it('renders without crashing', () => {
- const target = setupSnapshot(query, tooltip);
- const tree = target.toJSON();
- expect(tree).toMatchSnapshot();
- });
-
- it('renders nothing without crashing', () => {
- const target = setupSnapshot(query, false);
- const tree = target.toJSON();
- expect(tree).toMatchSnapshot();
- });
-
- it('renders Company typehead without crashing', () => {
- query.lens = 'Company';
- const target = setupSnapshot(query, tooltip);
- const tree = target.toJSON();
- expect(tree).toMatchSnapshot();
- });
-
- it('renders "Other" without crashing', () => {
- tooltip.values.push({ colorIndex: 5, name: 'Other', value: 900 });
- const target = setupSnapshot(query, tooltip);
- const tree = target.toJSON();
- expect(tree).toMatchSnapshot();
- });
-
- it('renders focus without crashing', () => {
- query.focus = 'foobar';
- const target = setupSnapshot(query, tooltip);
- const tree = target.toJSON();
- expect(tree).toMatchSnapshot();
- });
-});
-
-describe('buttons', () => {
- let cb = null;
- let cbFocus;
- let target = null;
-
- beforeEach(() => {
- cb = jest.fn();
- cbFocus = jest.fn();
- target = shallow(
- ,
- );
- });
-
- it('remove is called the button is clicked', () => {
- const prev = target.find('.tooltip-ul .color__1 .close');
- prev.simulate('click');
- expect(cb).toHaveBeenCalledWith('foo');
- });
-});
-
-describe('mapDispatchToProps', () => {
- it('provides a way to call remove', () => {
- jest.spyOn(trendsUtils, 'scrollToFocus');
- const dispatch = jest.fn();
- mapDispatchToProps(dispatch).remove('Foo');
- expect(dispatch.mock.calls).toEqual([
- [
- {
- filterName: 'company',
- filterValue: 'Foo',
- requery: 'REQUERY_ALWAYS',
- type: 'FILTER_REMOVED',
- },
- ],
- ]);
- expect(trendsUtils.scrollToFocus).not.toHaveBeenCalled();
- });
-});
-
-describe('mapStateToProps', () => {
- let state;
- beforeEach(() => {
- state = {
- query: {
- focus: '',
- lens: 'Overview',
- },
- trends: {
- tooltip: {
- title: 'Date: 1/1/2015',
- total: 100,
- values: [],
- },
- },
- };
- });
- it('maps state and props', () => {
- const actual = mapStateToProps(state);
- expect(actual).toEqual({
- focus: '',
- lens: 'Overview',
- hasCompanyTypeahead: false,
- hasTotal: false,
- tooltip: {
- date: '1/1/2015',
- heading: 'Date:',
- title: 'Date: 1/1/2015',
- total: 100,
- values: [],
- },
- });
- });
-
- it('maps state and props - focus', () => {
- state.query.focus = 'something else';
- const actual = mapStateToProps(state);
- expect(actual.focus).toEqual('focus');
- });
-
- it('handles broken tooltip title', () => {
- state.trends.tooltip.title = 'something else';
- const actual = mapStateToProps(state);
- expect(actual).toEqual({
- focus: '',
- lens: 'Overview',
- hasCompanyTypeahead: false,
- hasTotal: false,
- tooltip: {
- date: '',
- heading: 'something else:',
- title: 'something else',
- total: 100,
- values: [],
- },
- });
- });
-});
diff --git a/src/reducers/trends/selectors.js b/src/reducers/trends/selectors.js
index 537343e68..8c74487a6 100644
--- a/src/reducers/trends/selectors.js
+++ b/src/reducers/trends/selectors.js
@@ -1,4 +1,5 @@
export const selectTrendsChartType = (state) => state.trends.chartType;
+export const selectTrendsTooltip = (state) => state.trends.tooltip;
export const selectTrendsTotal = (state) => state.trends.total;
export const selectTrendsResultsSubProduct = (state) =>
state.trends.results['sub-product'];