diff --git a/package-lock.json b/package-lock.json
index ccb3c7014..9362d38e2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -35,6 +35,7 @@
"googleapis": "^163.0.0",
"heic-to": "^1.1.14",
"iframe-resizer-react": "1.1.0",
+ "indefinite": "^2.5.2",
"jquery": "^3.7.1",
"keymirror": "~0.1.1",
"lodash-es": "^4.17.21",
@@ -21377,6 +21378,15 @@
"node": ">=0.8.19"
}
},
+ "node_modules/indefinite": {
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/indefinite/-/indefinite-2.5.2.tgz",
+ "integrity": "sha512-J3ELLIk835hmgDMUfNltTCrHz9+CteTnSuXJqvxZT18wo1U2M9/QeeJMw99QdZwPEEr1DE2aBYda101Ojjdw5g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
"node_modules/indent-string": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
diff --git a/package.json b/package.json
index 386fe4db7..e2740c712 100644
--- a/package.json
+++ b/package.json
@@ -140,6 +140,7 @@
"googleapis": "^163.0.0",
"heic-to": "^1.1.14",
"iframe-resizer-react": "1.1.0",
+ "indefinite": "^2.5.2",
"jquery": "^3.7.1",
"keymirror": "~0.1.1",
"lodash-es": "^4.17.21",
diff --git a/src/js/components/Ballot/OfficeInfoModal.jsx b/src/js/components/Ballot/OfficeInfoModal.jsx
new file mode 100644
index 000000000..8295f3691
--- /dev/null
+++ b/src/js/components/Ballot/OfficeInfoModal.jsx
@@ -0,0 +1,416 @@
+/* eslint-disable react/jsx-one-expression-per-line */
+/* eslint-disable react/no-unescaped-entities */
+/* eslint-disable react/jsx-closing-tag-location */
+import React, { useState } from 'react';
+import PropTypes from 'prop-types';
+import styled, { createGlobalStyle } from 'styled-components';
+import indefinite from 'indefinite';
+import { MoreHoriz, LaunchOutlined, ContentCopy } from '@mui/icons-material';
+import Popover from '@mui/material/Popover';
+import DesignTokenColors from '../../common/components/Style/DesignTokenColors';
+import ModalDisplayTemplateA from '../Widgets/ModalDisplayTemplateA';
+import { openSnackbar } from '../../common/components/Widgets/SnackNotifier';
+
+const OfficeInfoModal = ({ isOpen, onClose, officeName }) => {
+ const [anchorEl, setAnchorEl] = useState(null);
+ const open = Boolean(anchorEl);
+ const id = open ? 'office-info-popover' : undefined;
+
+ const handleMenuClick = (event) => {
+ setAnchorEl(event.currentTarget);
+ };
+
+ const handlePopoverClose = () => {
+ setAnchorEl(null);
+ };
+
+ const handleOpenNewTab = () => {
+ window.open(window.location.href, '_blank');
+ handlePopoverClose();
+ };
+
+ const handleCopyLink = () => {
+ navigator.clipboard.writeText(window.location.href).then(() => {
+ openSnackbar({ message: 'Link copied!', severity: 'success' });
+ handlePopoverClose();
+ })
+ .catch((err) => {
+ console.error('Failed to copy link:', err);
+ openSnackbar({ message: 'Failed to copy link', severity: 'error' });
+ handlePopoverClose();
+ });
+ };
+
+ const PlayButton = () => (
+
+
+
+
+ );
+
+ const dialogTitleJSX = (
+
+
+ About the office of
+ {' '}
+
+ {officeName}
+
+
+
+
+
+
+
+
+
+
+ View this ballot section in new tab
+
+
+
+ Copy ballot section page link
+
+
+
+
+
+
+
+ );
+
+ const textFieldJSX = (
+
+
+
+
+
+
+
+
+
+
+
+ Watch video
+ (25 seconds)
+
+
+
+
+
+
+
+ {indefinite(officeName, { articleOnly: true, capitalize: true })}{' '}
+ {officeName} is the chief legal officer of a state,
+ responsible for enforcing laws, protecting citizens' rights, and representing
+ the public interest in legal matters.
+
+
+ They oversee investigations, defend state laws in court, and provide legal
+ guidance to government agencies.
+
+ {/* eslint-disable react/jsx-indent */}
+
+ Because they influence everything from consumer protections to civil rights
+ and election integrity, choosing {indefinite(officeName)} in an election is
+ crucial —it determines who will uphold justice, safeguard democratic
+ processes, and hold powerful entities accountable on behalf of the public.
+
+ {/* eslint-enable react/jsx-indent */}
+
+
+
+
+ );
+
+ return (
+ <>
+
+
+
+
+ >
+ );
+};
+
+OfficeInfoModal.propTypes = {
+ isOpen: PropTypes.bool.isRequired,
+ onClose: PropTypes.func.isRequired,
+ officeName: PropTypes.string.isRequired,
+};
+
+// Global Styles
+const WidenOfficeInfoModal = createGlobalStyle`
+ .MuiDialog-paper:has(#closeModalDisplayTemplateAofficeInfoModal) {
+ max-width: 750px !important;
+ width: 96% !important;
+ }
+`;
+
+const SoftenCorners = createGlobalStyle`
+ .MuiDialog-paper:has(#closeModalDisplayTemplateAofficeInfoModal) {
+ border-radius: 14px !important;
+ }
+`;
+
+const RemoveTitlePadding = createGlobalStyle`
+ .MuiDialogTitle-root:has(#closeModalDisplayTemplateAofficeInfoModal) > div > div {
+ padding-left: 0 !important;
+ width: 100% !important;
+ }
+`;
+
+// Styled Components
+const HeaderRow = styled.div`
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ width: 100%;
+ padding-left: 16px;
+ padding-bottom: 8px;
+`;
+
+const Title = styled.h3`
+ font-size: 18px;
+ font-weight: 400;
+ line-height: 1.3;
+ margin: 0;
+ flex: 1;
+ min-width: 0;
+
+ strong {
+ font-weight: 600;
+ }
+
+ .mobile-break {
+ display: none;
+ }
+
+ @media (max-width: 768px) {
+ font-size: 16px;
+
+ .mobile-break {
+ display: inline;
+ }
+ }
+`;
+
+const HeaderActions = styled.div`
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ flex-shrink: 0;
+ margin-right: 52px !important;
+ margin-top: -4px;
+
+ @media (max-width: 768px) {
+ margin-right: 52px !important;
+ margin-top: -4px;
+ }
+`;
+
+const VerticalLine = styled.div`
+ border-left: 1px solid ${DesignTokenColors.neutral200};
+ height: 24px;
+ align-self: center;
+
+ @media (max-width: 768px) {
+ height: 20px;
+ }
+`;
+
+const TripleDotWrapper = styled.div`
+ color: ${DesignTokenColors.neutral600};
+ display: flex;
+ align-items: center;
+
+ &:hover {
+ color: ${DesignTokenColors.neutral400};
+ cursor: pointer;
+ }
+`;
+
+const TripleDotButton = styled.button`
+ background: transparent;
+ border: 0;
+ cursor: pointer;
+ padding: 4px;
+ display: flex;
+ align-items: center;
+ color: inherit;
+ outline: none;
+
+ &:focus {
+ outline: none;
+ }
+
+ svg {
+ font-size: 20px;
+ }
+`;
+
+const PopoverWrapper = styled.div`
+ padding: 8px 0;
+ min-width: 220px;
+`;
+
+const PopoverOption = styled.div`
+ padding: 10px 16px;
+ font-size: 14px;
+ cursor: pointer;
+ color: ${DesignTokenColors.neutral900};
+ white-space: nowrap;
+
+ &:hover {
+ background: ${DesignTokenColors.neutral100};
+ }
+`;
+
+const ModalBody = styled.div`
+ background: ${DesignTokenColors.whiteUI};
+ padding: 20px;
+`;
+
+const FlexContainer = styled.div`
+ display: flex;
+ gap: 24px;
+ align-items: flex-start;
+
+ @media (max-width: 768px) {
+ flex-direction: column;
+ gap: 20px;
+ }
+`;
+
+const LeftColumn = styled.div`
+ flex: 0 0 200px;
+
+ @media (max-width: 768px) {
+ flex: 1;
+ width: 100%;
+ }
+`;
+
+const RightColumn = styled.div`
+ flex: 1;
+
+ p {
+ margin-bottom: 16px;
+ line-height: 1.6;
+ font-size: 15px;
+ color: ${DesignTokenColors.neutral900};
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+`;
+
+const VideoContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+
+ @media (max-width: 768px) {
+ background: #ebebeb;
+ border-radius: 6px;
+ padding: 12px;
+ flex-direction: row;
+ align-items: center;
+ gap: 14px;
+ }
+`;
+
+const VideoPlaceholder = styled.div`
+ width: 100%;
+ aspect-ratio: 1 / 1;
+ background: #a0a0a0;
+ border-radius: 4px;
+
+ @media (max-width: 768px) {
+ width: 90px;
+ height: 90px;
+ flex-shrink: 0;
+ }
+`;
+
+const VideoControls = styled.div`
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ margin-top: 10px;
+ cursor: pointer;
+
+ @media (max-width: 768px) {
+ margin-top: 0;
+ flex: 1;
+ }
+`;
+
+const PlayButtonWrapper = styled.div`
+ cursor: pointer;
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+
+ &:hover svg rect {
+ fill: #06437D;
+ }
+`;
+
+const VideoTextWrapper = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ min-width: 0;
+ flex: 1;
+`;
+
+const WatchText = styled.span`
+ font-size: 16px;
+ color: #0858A1;
+ font-weight: 400;
+ line-height: 1.2;
+ white-space: nowrap;
+
+ @media (max-width: 768px) {
+ font-size: 14px;
+ }
+`;
+
+const DurationText = styled.span`
+ font-size: 13px;
+ color: #666666;
+ font-weight: 400;
+ line-height: 1.2;
+ white-space: nowrap;
+
+ @media (max-width: 768px) {
+ font-size: 12px;
+ }
+`;
+
+export default OfficeInfoModal;
diff --git a/src/js/components/Ballot/OfficeItemCompressed.jsx b/src/js/components/Ballot/OfficeItemCompressed.jsx
index a1831ab0f..9a7f8d1c4 100644
--- a/src/js/components/Ballot/OfficeItemCompressed.jsx
+++ b/src/js/components/Ballot/OfficeItemCompressed.jsx
@@ -1,8 +1,10 @@
+import { InfoOutlined } from '@mui/icons-material';
import withStyles from '@mui/styles/withStyles';
import withTheme from '@mui/styles/withTheme';
import PropTypes from 'prop-types';
import React, { Component, Suspense } from 'react';
import styled from 'styled-components';
+import DesignTokenColors from '../../common/components/Style/DesignTokenColors';
import OfficeActions from '../../actions/OfficeActions';
import BallotScrollingContainer from './BallotScrollingContainer';
import { BallotScrollingOuterWrapper } from '../../common/components/Style/ScrollingStyles';
@@ -16,6 +18,7 @@ import { sortCandidateList } from '../../utils/positionFunctions';
import { OfficeItemCompressedWrapper, OfficeNameH2 } from '../Style/BallotStyles';
const ShowMoreButtons = React.lazy(() => import(/* webpackChunkName: 'ShowMoreButtons' */ '../Widgets/ShowMoreButtons'));
+const OfficeInfoModal = React.lazy(() => import(/* webpackChunkName: 'OfficeInfoModal' */ './OfficeInfoModal'));
const NUMBER_OF_CANDIDATES_TO_DISPLAY = 5;
// This is related to components/VoterGuide/VoterGuideOfficeItemCompressed
@@ -29,6 +32,8 @@ class OfficeItemCompressed extends Component {
candidateListForDisplay: [],
showAllCandidates: false,
totalNumberOfCandidates: 0,
+ officeInfoModalOpen: false,
+ moreInfoIconHovered: false,
};
}
@@ -87,6 +92,22 @@ class OfficeItemCompressed extends Component {
});
};
+ openOfficeInfoModal = () => {
+ this.setState({ officeInfoModalOpen: true });
+ };
+
+ closeOfficeInfoModal = () => {
+ this.setState({ officeInfoModalOpen: false });
+ };
+
+ handleMoreInfoIconHover = () => {
+ this.setState({ moreInfoIconHovered: true });
+ }
+
+ handleMoreInfoIconLeave = () => {
+ this.setState({ moreInfoIconHovered: false });
+ }
+
goToCandidateLink (candidateWeVoteId) {
const { organizationWeVoteId } = this.props;
const candidateLink = organizationWeVoteId ?
@@ -101,10 +122,11 @@ class OfficeItemCompressed extends Component {
let { ballotItemDisplayName } = this.props;
const { isFirstBallotItem, officeWeVoteId, primaryParty, useHelpDefeatOrHelpWin } = this.props; // classes
- const { candidateListLength, showAllCandidates, totalNumberOfCandidates } = this.state;
+ const { candidateListLength, showAllCandidates, totalNumberOfCandidates, moreInfoIconHovered } = this.state;
ballotItemDisplayName = toTitleCase(ballotItemDisplayName).replace('(Unexpired)', '(Remainder)');
const moreCandidatesToDisplay = candidateListLength > NUMBER_OF_CANDIDATES_TO_DISPLAY;
+
return (
{ballotItemDisplayName}
+
{!!primaryParty && (
{' '}
@@ -144,6 +172,15 @@ class OfficeItemCompressed extends Component {
/>
)}
+ {this.state.officeInfoModalOpen && (
+ Loading...}>
+
+
+ )}
);
}