From 1bc74c5327dbfe3897d0d6596adf33617d956880 Mon Sep 17 00:00:00 2001 From: Dale John McGrew Date: Sat, 18 Jul 2026 09:50:20 -0700 Subject: [PATCH] Turned on auto rollup of items on the Ballot when candidates are chosen or opposed. Limiting listed candidates to 4, and reinstated the "Show more" feature -- especially useful for races with 30+ candidates. Did a code iteration related to WV-679 so we can use the "Chosen" filters in browser mobile. --- .../Ballot/BallotItemCompressed.jsx | 2 +- .../Ballot/OfficeItemCompressed.jsx | 58 ++++++++++++++++--- src/js/utils/positionFunctions.js | 8 ++- src/js/utilsApi/showBallotDecisionsTabs.js | 8 ++- 4 files changed, 64 insertions(+), 12 deletions(-) diff --git a/src/js/components/Ballot/BallotItemCompressed.jsx b/src/js/components/Ballot/BallotItemCompressed.jsx index 4c958e6b18..91c19d1cb9 100644 --- a/src/js/components/Ballot/BallotItemCompressed.jsx +++ b/src/js/components/Ballot/BallotItemCompressed.jsx @@ -25,7 +25,7 @@ export default class BallotItemCompressed extends PureComponent { ballotItemDisplayName={ballotItemDisplayName} candidateList={candidateList} candidatesToShowForSearchResults={candidatesToShowForSearchResults} - disableAutoRollUp + // disableAutoRollUp foundInSearchWords={foundInSearchWords} isFirstBallotItem={isFirstBallotItem} officeWeVoteId={weVoteId} diff --git a/src/js/components/Ballot/OfficeItemCompressed.jsx b/src/js/components/Ballot/OfficeItemCompressed.jsx index a8b0dc3d79..f6afd1db96 100644 --- a/src/js/components/Ballot/OfficeItemCompressed.jsx +++ b/src/js/components/Ballot/OfficeItemCompressed.jsx @@ -20,7 +20,7 @@ import OfficeHeaderTripleDotMenu from '../BallotItem/OfficeHeaderTripleDotMenu'; const ShowMoreButtons = React.lazy(() => import(/* webpackChunkName: 'ShowMoreButtons' */ '../Widgets/ShowMoreButtons')); const OfficeInfoModal = React.lazy(() => import(/* webpackChunkName: 'OfficeInfoModal' */ './OfficeInfoModal')); -const NUMBER_OF_CANDIDATES_TO_DISPLAY = 5; +const NUMBER_OF_CANDIDATES_TO_DISPLAY = 4; // This is related to components/VoterGuide/VoterGuideOfficeItemCompressed class OfficeItemCompressed extends Component { @@ -30,7 +30,6 @@ class OfficeItemCompressed extends Component { super(props); this.state = { candidateListLength: 0, - candidateListForDisplay: [], showAllCandidates: false, totalNumberOfCandidates: 0, officeInfoModalOpen: false, @@ -63,11 +62,9 @@ class OfficeItemCompressed extends Component { } onCandidateStoreChange () { - const { candidateList, officeWeVoteId } = this.props; + const { officeWeVoteId } = this.props; const totalNumberOfCandidates = officeWeVoteId ? CandidateStore.getNumberOfCandidatesRetrievedByOffice(officeWeVoteId) : 0; - // const sortedCandidateList = sortCandidateList(candidateList || []); this.setState({ - // candidateListForDisplay: sortedCandidateList, totalNumberOfCandidates, }); } @@ -80,9 +77,40 @@ class OfficeItemCompressed extends Component { if (showAllCandidates || disableAutoRollUp) { return sortedCandidateList; } - return sortedCandidateList.slice(0, NUMBER_OF_CANDIDATES_TO_DISPLAY); + const numberOfCandidatesToRender = this.getCandidatesToRenderCount(); + // console.log('numberOfCandidatesToRender: ', numberOfCandidatesToRender); + return sortedCandidateList.slice(0, numberOfCandidatesToRender); } + getCandidatesToRenderCount = () => { + // How many candidates should we render? If voter has chosen or opposed 1+ candidates, only show those + const { candidateList, disableAutoRollUp } = this.props; + if (!candidateList || candidateList.length === 0) { + return 0; + } + // Dale 2026-07-18 I want to leave candidatesToShowForSearchResults in place for the next year in case we need it again. + // let { candidatesToShowForSearchResults } = this.props; + // candidatesToShowForSearchResults = candidatesToShowForSearchResults || []; + const candidatesToShowForSearchResults = []; + const { showAllCandidates } = this.state; + const supportedCandidatesList = candidateList.filter((candidate) => candidatesToShowForSearchResults.includes(candidate.we_vote_id) || (SupportStore.getVoterSupportsByBallotItemWeVoteId(candidate.we_vote_id) && !candidate.withdrawn_from_election)); + const opposedCandidatesList = candidateList.filter((candidate) => candidatesToShowForSearchResults.includes(candidate.we_vote_id) || (SupportStore.getVoterOpposesByBallotItemWeVoteId(candidate.we_vote_id) && !candidate.withdrawn_from_election)); + const supportedAndOpposedCandidatesList = supportedCandidatesList.concat(opposedCandidatesList); + // console.log('OfficeItemCompressed getCandidatesToRenderCount showAllCandidates: ', showAllCandidates, ', disableAutoRollUp:', disableAutoRollUp, ', supportedAndOpposedCandidatesList:', supportedAndOpposedCandidatesList, ', candidateList:', candidateList); + if (showAllCandidates) { + return candidateList.length; + } else if (disableAutoRollUp) { + return NUMBER_OF_CANDIDATES_TO_DISPLAY; + } else if (supportedCandidatesList && supportedCandidatesList.length > 0) { + // Used to be: + // if (supportedAndOpposedCandidatesList && supportedAndOpposedCandidatesList.length > 0) + // But we don't want to roll-up until an option has been chosen/supported + return supportedAndOpposedCandidatesList.length; + } else { + return NUMBER_OF_CANDIDATES_TO_DISPLAY; + } + }; + showAllCandidates = () => { this.setState({ showAllCandidates: true }); }; @@ -125,7 +153,8 @@ class OfficeItemCompressed extends Component { const { hideOfficeHeader, isFirstBallotItem, officeWeVoteId, primaryParty, useHelpDefeatOrHelpWin } = this.props; const { candidateListLength, showAllCandidates, totalNumberOfCandidates, moreInfoIconHovered } = this.state; ballotItemDisplayName = toTitleCase(ballotItemDisplayName).replace('(Unexpired)', '(Remainder)'); - const moreCandidatesToDisplay = candidateListLength > NUMBER_OF_CANDIDATES_TO_DISPLAY; + const candidatesToRenderCount = this.getCandidatesToRenderCount(); + const moreCandidatesToDisplay = candidatesToRenderCount < totalNumberOfCandidates; const officeExplanationExists = false; // TODO: Add logic to check if officeExplanation exists return ( @@ -177,7 +206,7 @@ class OfficeItemCompressed extends Component { goToCandidateLink={(candidateWeVoteId) => this.goToCandidateLink(candidateWeVoteId)} useHelpDefeatOrHelpWin={useHelpDefeatOrHelpWin} /> - {moreCandidatesToDisplay && ( + {(moreCandidatesToDisplay) ? ( }> + ) : ( + <> + {(showAllCandidates && candidateListLength >= totalNumberOfCandidates) && ( + }> + this.showLessCandidates()} + showMoreButtonWasClicked + showLessCustomText="show fewer candidates" + /> + + )} + )} {this.state.officeInfoModalOpen && ( Loading...}> diff --git a/src/js/utils/positionFunctions.js b/src/js/utils/positionFunctions.js index fd1db3aaea..493eaffafc 100644 --- a/src/js/utils/positionFunctions.js +++ b/src/js/utils/positionFunctions.js @@ -253,6 +253,7 @@ export function sortCandidateList (newCandidateList) { let candidateModified; let numberOfOpposePositionsForScore = 0; let numberOfSupportPositionsForScore = 0; + let voterOpposesBallotItem; let voterSupportsBallotItem; // Prepare an array of candidate names that are supported by voter @@ -260,10 +261,11 @@ export function sortCandidateList (newCandidateList) { ballotItemStatSheet = SupportStore.getBallotItemStatSheet(candidate.we_vote_id, candidate.politician_we_vote_id); // console.log('ballotItemStatSheet:', ballotItemStatSheet); if (ballotItemStatSheet) { - ({ numberOfOpposePositionsForScore, numberOfSupportPositionsForScore, voterSupportsBallotItem } = ballotItemStatSheet); + ({ numberOfOpposePositionsForScore, numberOfSupportPositionsForScore, voterOpposesBallotItem, voterSupportsBallotItem } = ballotItemStatSheet); // voterIssuesScoreForCandidate = IssueStore.getIssuesScoreByBallotItemWeVoteId(candidate.we_vote_id); candidateModified = { ...candidate }; candidateModified.voterNetworkScoreForCandidate = Math.abs(numberOfSupportPositionsForScore - numberOfOpposePositionsForScore); + candidateModified.voterOpposesBallotItem = voterOpposesBallotItem; candidateModified.voterSupportsBallotItem = voterSupportsBallotItem; // console.log('candidateModified:', candidateModified); unsortedCandidateListModified.push(candidateModified); @@ -275,8 +277,12 @@ export function sortCandidateList (newCandidateList) { sortedCandidateList = unsortedCandidateListModified; // Start by ordering by twitter_followers_count sortedCandidateList.sort((optionA, optionB) => optionB.twitter_followers_count - optionA.twitter_followers_count); + // Move candidates with the highest supporters count to the top of the list + sortedCandidateList.sort((optionA, optionB) => optionB.supporters_count - optionA.supporters_count); // Move candidates with the highest personalized score to the top of the list sortedCandidateList.sort((optionA, optionB) => optionB.voterNetworkScoreForCandidate - optionA.voterNetworkScoreForCandidate); + // Move candidates opposed by the voter to the top of list + sortedCandidateList.sort((optionA, optionB) => (optionB.voterOpposesBallotItem ? 1 : 0) - (optionA.voterOpposesBallotItem ? 1 : 0)); // Move candidates supported by the voter to the top of list sortedCandidateList.sort((optionA, optionB) => (optionB.voterSupportsBallotItem ? 1 : 0) - (optionA.voterSupportsBallotItem ? 1 : 0)); // Move withdrawn candidates to the bottom of list diff --git a/src/js/utilsApi/showBallotDecisionsTabs.js b/src/js/utilsApi/showBallotDecisionsTabs.js index 11fc1d391c..c92da3dab2 100644 --- a/src/js/utilsApi/showBallotDecisionsTabs.js +++ b/src/js/utilsApi/showBallotDecisionsTabs.js @@ -2,10 +2,14 @@ import { isWebApp } from '../common/utils/isCordovaOrWebApp'; import BallotStore from '../stores/BallotStore'; // eslint-disable-line import/no-cycle export default function showBallotDecisionsTabs () { + // Dale 2026-07-18 I would like showBallotDecisionsTabs to not be based on mobile vs. desktop, but only based on if ballot choices have been made. + // WV-679 is the ticket where the UX team is reworking the design for in all screen sizes. + // src/js/common/utils/isMobileScreenSize.js should be used to avoid problems, 500 is an imperfect criteria - const isMobileScreenSizeForShowBallotDecisionsTabs = window.innerWidth < 500; + // const isMobileScreenSizeForShowBallotDecisionsTabs = window.innerWidth < 500; + // console.log('window.innerWidth:', window.innerWidth, ', isMobileScreenSizeForShowBallotDecisionsTabs:', isMobileScreenSizeForShowBallotDecisionsTabs); return (BallotStore.ballotLength !== BallotStore.ballotRemainingChoicesLength) && (BallotStore.ballotRemainingChoicesLength > 0) && - !isMobileScreenSizeForShowBallotDecisionsTabs && + // !isMobileScreenSizeForShowBallotDecisionsTabs && isWebApp(); // November 2025: Disabled for Cordova until the feature is finished and released to production }