diff --git a/packages/frontend/src/components/DragAndDrop/SortableItem.tsx b/packages/frontend/src/components/DragAndDrop/SortableItem.tsx index fd4535b5..a8418e92 100644 --- a/packages/frontend/src/components/DragAndDrop/SortableItem.tsx +++ b/packages/frontend/src/components/DragAndDrop/SortableItem.tsx @@ -62,12 +62,14 @@ export function SortableItem({ children, id }: PropsWithChildren) { interface DragHandleProps { style?: CSSProperties; disabled?: boolean; + ariaLabel?: string; + testId?: string; } -export function DragHandle({ style, disabled }: DragHandleProps) { +export function DragHandle({ style, disabled, ariaLabel }: DragHandleProps) { const { attributes, listeners, ref } = useContext(SortableItemContext); return ( - + ); diff --git a/packages/frontend/src/components/Election/ElectionHome.tsx b/packages/frontend/src/components/Election/ElectionHome.tsx index 89ae2144..fe3c72b4 100644 --- a/packages/frontend/src/components/Election/ElectionHome.tsx +++ b/packages/frontend/src/components/Election/ElectionHome.tsx @@ -71,7 +71,7 @@ const ElectionHome = () => { voterAuth.has_voted == false && voterAuth.authorized_voter && !voterAuth.required && - )) } diff --git a/packages/frontend/src/components/Election/Voting/GenericBallotView/ColumnHeadings.tsx b/packages/frontend/src/components/Election/Voting/GenericBallotView/ColumnHeadings.tsx index e582e90c..70c64abd 100644 --- a/packages/frontend/src/components/Election/Voting/GenericBallotView/ColumnHeadings.tsx +++ b/packages/frontend/src/components/Election/Voting/GenericBallotView/ColumnHeadings.tsx @@ -10,7 +10,7 @@ interface ColumnHeadingsProps { } export default function ColumnHeadings({ columnIndex, columnTitle, gridArea, starHeadings, fontSX }: ColumnHeadingsProps) { return ( - + {starHeadings && - { if(pages.length == 0){ return

No races created for election

} + const isOnLastPage = currentPage === pages.length - 1 + const noScores = pages.every(page => page.candidates.every(candidate => candidate.score === null)) + const thereAreWarnings = pages.some(page => page.warnings) + const submitButtonDisabled = !isOnLastPage || (isPending || noScores || thereAreWarnings) return ( @@ -255,8 +259,9 @@ const VotePage = () => { diff --git a/packages/frontend/src/components/ElectionForm/Candidates/AddCandidate.tsx b/packages/frontend/src/components/ElectionForm/Candidates/AddCandidate.tsx index 380cf48d..61f895af 100644 --- a/packages/frontend/src/components/ElectionForm/Candidates/AddCandidate.tsx +++ b/packages/frontend/src/components/ElectionForm/Candidates/AddCandidate.tsx @@ -307,8 +307,17 @@ const CandidateDialog = ({ onEditCandidate, candidate, index, onSave, open, hand ) } - -export const CandidateForm = ({ onEditCandidate, candidate, index, onDeleteCandidate, disabled, inputRef, onKeyDown}) => { +interface CandidateFormProps { + onEditCandidate: (newCandidate: Candidate) => void, + candidate: Candidate, + index: number, + onDeleteCandidate: () => void, + disabled: boolean, + inputRef: (el: React.MutableRefObject) => React.MutableRefObject, + onKeyDown: (e: React.KeyboardEvent) => void, + electionState: string +} +export const CandidateForm = ({ onEditCandidate, candidate, index, onDeleteCandidate, disabled, inputRef, onKeyDown, electionState}: CandidateFormProps) => { const [open, setOpen] = React.useState(false); const handleOpen = () => setOpen(true); @@ -316,18 +325,18 @@ export const CandidateForm = ({ onEditCandidate, candidate, index, onDeleteCandi const flags = useFeatureFlags(); const onSave = () => { handleClose() } return ( - + - + onEditCandidate({ ...candidate, candidate_name: e.target.value })} inputRef={inputRef} onKeyDown={onKeyDown} + disabled={electionState !== 'draft'} /> @@ -348,7 +358,8 @@ export const CandidateForm = ({ onEditCandidate, candidate, index, onDeleteCandi
} @@ -360,7 +371,7 @@ export const CandidateForm = ({ onEditCandidate, candidate, index, onDeleteCandi ) } -const AddCandidate = ({ onAddNewCandidate }) => { +const AddCandidate = ({ onAddNewCandidate, index }) => { const handleEnter = (e) => { saveNewCandidate() diff --git a/packages/frontend/src/components/ElectionForm/CreateElectionDialog.tsx b/packages/frontend/src/components/ElectionForm/CreateElectionDialog.tsx index 789464ba..76261db2 100644 --- a/packages/frontend/src/components/ElectionForm/CreateElectionDialog.tsx +++ b/packages/frontend/src/components/ElectionForm/CreateElectionDialog.tsx @@ -282,7 +282,7 @@ export default () => { setErrors={setErrors} showLabel={false} /> - + diff --git a/packages/frontend/src/components/ElectionForm/Details/ElectionDetailsForm.tsx b/packages/frontend/src/components/ElectionForm/Details/ElectionDetailsForm.tsx index 87506a59..facd914c 100644 --- a/packages/frontend/src/components/ElectionForm/Details/ElectionDetailsForm.tsx +++ b/packages/frontend/src/components/ElectionForm/Details/ElectionDetailsForm.tsx @@ -17,8 +17,8 @@ export const ElectionTitleField = ({termType, value, onUpdateValue, errors, setE inputProps={{ pattern: "[a-z]{3,15}" }} error={errors.title !== ''} required - id="election-name" - name="name" + id="election-title" + name="election-title" // TODO: This bolding method only works for the text fields, if we like it we should figure out a way to add it to other fields as well // inputProps={getStyle('title')} label={showLabel? t('election_details.title') : ""} @@ -71,7 +71,7 @@ export default function ElectionDetailsForm({editedElection, applyUpdate, errors - {t('election_details.time_zone')} + {t('election_details.time_zone')} { @@ -176,6 +179,8 @@ export default function ElectionDetailsForm({editedElection, applyUpdate, errors {/* datetime-local is formatted according to the OS locale, I don't think there's a way to override it*/} { diff --git a/packages/frontend/src/components/ElectionForm/Details/ElectionDetailsInlineForm.tsx b/packages/frontend/src/components/ElectionForm/Details/ElectionDetailsInlineForm.tsx index 878db329..3b27a2cd 100644 --- a/packages/frontend/src/components/ElectionForm/Details/ElectionDetailsInlineForm.tsx +++ b/packages/frontend/src/components/ElectionForm/Details/ElectionDetailsInlineForm.tsx @@ -65,7 +65,8 @@ export default function ElectionDetailsInlineForm() { diff --git a/packages/frontend/src/components/ElectionForm/ElectionSettings.tsx b/packages/frontend/src/components/ElectionForm/ElectionSettings.tsx index 89bfa89d..fffd88e7 100644 --- a/packages/frontend/src/components/ElectionForm/ElectionSettings.tsx +++ b/packages/frontend/src/components/ElectionForm/ElectionSettings.tsx @@ -82,13 +82,14 @@ export default function ElectionSettings() { - @@ -111,6 +112,7 @@ export default function ElectionSettings() { applySettingsUpdate((settings) => { settings.max_rankings = Number(e.target.value) })} @@ -142,7 +144,7 @@ export default function ElectionSettings() { {t('keyword.save')} - + } ) } diff --git a/packages/frontend/src/components/ElectionForm/Races/AddRace.tsx b/packages/frontend/src/components/ElectionForm/Races/AddRace.tsx index 485c4b5e..4d320f5e 100644 --- a/packages/frontend/src/components/ElectionForm/Races/AddRace.tsx +++ b/packages/frontend/src/components/ElectionForm/Races/AddRace.tsx @@ -28,6 +28,8 @@ export default function AddRace() { }}> Add - + {open && - + } ) } diff --git a/packages/frontend/src/components/ElectionForm/Races/Race.tsx b/packages/frontend/src/components/ElectionForm/Races/Race.tsx index 79a54e30..43217a7f 100644 --- a/packages/frontend/src/components/ElectionForm/Races/Race.tsx +++ b/packages/frontend/src/components/ElectionForm/Races/Race.tsx @@ -54,7 +54,9 @@ export default function Race({ race, race_index }) { {election.state === 'draft' ? : } diff --git a/packages/frontend/src/components/ElectionForm/Races/RaceForm.tsx b/packages/frontend/src/components/ElectionForm/Races/RaceForm.tsx index 1f0c4a2d..a10655ec 100644 --- a/packages/frontend/src/components/ElectionForm/Races/RaceForm.tsx +++ b/packages/frontend/src/components/ElectionForm/Races/RaceForm.tsx @@ -38,7 +38,7 @@ export default function RaceForm({ race_index, editedRace, errors, setErrors, ap ) const confirm = useConfirm(); const inputRefs = useRef([]); - const ephemeralCandidates = useMemo(() => + const ephemeralCandidates:Candidate[] = useMemo(() => [...editedRace.candidates, { candidate_id: uuidv4(), candidate_name: '' }], [editedRace.candidates] ); @@ -80,8 +80,8 @@ export default function RaceForm({ race_index, editedRace, errors, setErrors, ap } }, [confirm, editedRace.candidates.length, applyRaceUpdate, setErrors]); // Handle tab and shift+tab to move focus between candidates - const handleKeyDown = useCallback((event, index) => { - + const handleKeyDown = useCallback((event: React.KeyboardEvent, index: number) => { + const target = event.target as HTMLInputElement; if (event.key === 'Tab' && event.shiftKey) { // Move focus to the previous candidate event.preventDefault(); @@ -95,7 +95,7 @@ export default function RaceForm({ race_index, editedRace, errors, setErrors, ap if (nextIndex < ephemeralCandidates.length && inputRefs.current[nextIndex]) { inputRefs.current[nextIndex].focus(); } - } else if (event.key === 'Backspace' && event.target.value === '' && index > 0) { + } else if (event.key === 'Backspace' && target.value === '' && index > 0) { // Move focus to the previous candidate when backspacing on an empty candidate event.preventDefault(); inputRefs.current[index - 1].focus(); @@ -113,6 +113,7 @@ export default function RaceForm({ race_index, editedRace, errors, setErrors, ap - setActiveStep(0)}>How many winners? + setActiveStep(0)}>How many winners? - } label={t('edit_race.single_winner')} sx={{ mb: 0, pb: 0 }} /> - } label={t('edit_race.bloc_multi_winner')} sx={{ mb: 0, pb: 0 }} /> - } label={t('edit_race.proportional_multi_winner')} sx={{ mb: 0, pb: 0 }} /> + } label={t('edit_race.single_winner')} sx={{ mb: 0, pb: 0 }} /> + } label={t('edit_race.bloc_multi_winner')} sx={{ mb: 0, pb: 0 }} /> + } label={t('edit_race.proportional_multi_winner')} sx={{ mb: 0, pb: 0 }} /> - setActiveStep(1)}>Voting Method + setActiveStep(1)}>Voting Method applyRaceUpdate(race => { race.voting_method = e.target.value })} + > - } label="STAR" sx={{ mb: 0, pb: 0 }} /> + } label="STAR" sx={{ mb: 0, pb: 0 }} /> Score candidates 0-5 {flags.isSet('METHOD_STAR_PR') && <> - } label="Proportional STAR" /> + } label="Proportional STAR" /> Score candidates 0-5 } {flags.isSet('METHOD_RANKED_ROBIN') && <> - } label="Ranked Robin" /> + } label="Ranked Robin" /> Rank candidates in order of preference } {flags.isSet('METHOD_APPROVAL') && <> - } label="Approval" /> + } label="Approval" /> Mark all candidates you approve of @@ -283,11 +286,11 @@ export default function RaceForm({ race_index, editedRace, errors, setErrors, ap sx={{ width: '100%', ml: -1 }}> {!showsAllMethods && - { setShowsAllMethods(true) }}> + { setShowsAllMethods(true) }}> } {showsAllMethods && - { setShowsAllMethods(false) }}> + { setShowsAllMethods(false) }}> } @@ -311,20 +314,20 @@ export default function RaceForm({ race_index, editedRace, errors, setErrors, ap These voting methods do not guarantee every voter an equally powerful vote if there are more than two candidates. - } label="Plurality" /> + } label="Plurality" /> Mark one candidate only. Not recommended with more than 2 candidates. {flags.isSet('METHOD_RANKED_CHOICE') && <> - } label="Ranked Choice" /> + } label="Ranked Choice" /> Rank candidates in order of preference, only recommended for educational purposes } {flags.isSet('METHOD_RANKED_CHOICE') && <> - } label="STV" /> + } label="STV" /> Proportaionl Version of RCV @@ -349,7 +352,7 @@ export default function RaceForm({ race_index, editedRace, errors, setErrors, ap { ( @@ -360,9 +363,10 @@ export default function RaceForm({ race_index, editedRace, errors, setErrors, ap candidate={candidate} index={index} onDeleteCandidate={() => onDeleteCandidate(index)} - disabled={ephemeralCandidates.length - 1 === index} - inputRef={el => inputRefs.current[index] = el} - onKeyDown={event => handleKeyDown(event, index)}/> + disabled={ephemeralCandidates.length - 1 === index || election.state !== 'draft'} + inputRef={(el:React.MutableRefObject) => inputRefs.current[index] = el} + onKeyDown={(event: React.KeyboardEvent) => handleKeyDown(event, index)} + electionState={election.state}/> )} /> diff --git a/packages/frontend/src/components/styles.tsx b/packages/frontend/src/components/styles.tsx index 8f2b0bc8..818ae228 100644 --- a/packages/frontend/src/components/styles.tsx +++ b/packages/frontend/src/components/styles.tsx @@ -45,6 +45,7 @@ export const Tip = (props: {name: TipName, electionTermType: TermType | undefine export const StyledButton = (props) => (