diff --git a/sites/public/__tests__/pages/applications/review/demographics.test.tsx b/sites/public/__tests__/pages/applications/review/demographics.test.tsx
index 93aaacfe131..935e676c756 100644
--- a/sites/public/__tests__/pages/applications/review/demographics.test.tsx
+++ b/sites/public/__tests__/pages/applications/review/demographics.test.tsx
@@ -233,6 +233,64 @@ describe("Demographics", () => {
fireEvent.click(screen.getByRole("checkbox", { name: "Other / Multiracial" }))
expect(await screen.findAllByTestId("otherMultiracial")).toHaveLength(2)
})
+
+ describe("'Decline to respond' race exclusivity", () => {
+ const renderDemographics = () => {
+ const conductor = new ApplicationConductor({}, {})
+ conductor.config.featureFlags = [
+ { name: FeatureFlagEnum.enableLimitedHowDidYouHear, active: false } as FeatureFlag,
+ ]
+ conductor.config.raceEthnicityConfiguration = defaultRaceEthnicityConfiguration
+
+ render(
+ {
+ return
+ },
+ syncListing: () => {
+ return
+ },
+ }}
+ >
+
+
+ )
+ }
+
+ it("clears other race options when 'Decline to respond' is checked", () => {
+ renderDemographics()
+ const asian = screen.getByRole("checkbox", { name: "Asian" })
+ const white = screen.getByRole("checkbox", { name: "White" })
+ const decline = screen.getByRole("checkbox", { name: "Decline to respond" })
+
+ fireEvent.click(asian)
+ fireEvent.click(white)
+ expect(asian).toBeChecked()
+ expect(white).toBeChecked()
+
+ fireEvent.click(decline)
+ expect(decline).toBeChecked()
+ expect(asian).not.toBeChecked()
+ expect(white).not.toBeChecked()
+ })
+
+ it("clears 'Decline to respond' when another race option is checked", () => {
+ renderDemographics()
+ const decline = screen.getByRole("checkbox", { name: "Decline to respond" })
+ const white = screen.getByRole("checkbox", { name: "White" })
+
+ fireEvent.click(decline)
+ expect(decline).toBeChecked()
+
+ fireEvent.click(white)
+ expect(white).toBeChecked()
+ expect(decline).not.toBeChecked()
+ })
+ })
})
it("should show limited list of how did you hear fields when enableLimitedHowDidYouHear is on", () => {
diff --git a/sites/public/src/pages/applications/review/demographics.tsx b/sites/public/src/pages/applications/review/demographics.tsx
index 7e371365f00..b86f341cccd 100644
--- a/sites/public/src/pages/applications/review/demographics.tsx
+++ b/sites/public/src/pages/applications/review/demographics.tsx
@@ -20,6 +20,7 @@ import {
limitedHowDidYouHear,
getRaceEthnicityOptions,
genderKeys,
+ setExclusive,
} from "@bloom-housing/shared-helpers"
import FormsLayout from "../../../layouts/forms"
import { isFeatureFlagOn } from "../../../lib/helpers"
@@ -27,6 +28,9 @@ import { useFormConductor } from "../../../lib/hooks"
import { UserStatus } from "../../../lib/constants"
import ApplicationFormLayout from "../../../layouts/application-form"
+// Race option that is mutually exclusive with every other race option.
+const RACE_DECLINE_TO_RESPOND = "declineToRespond"
+
const ApplicationDemographics = () => {
const { profile } = useContext(AuthContext)
const { conductor, application, listing } = useFormConductor("demographics")
@@ -39,7 +43,7 @@ const ApplicationDemographics = () => {
currentPageSection += 1
// eslint-disable-next-line @typescript-eslint/unbound-method
- const { register, handleSubmit, watch } = useForm({
+ const { register, handleSubmit, watch, setValue } = useForm({
defaultValues: {
ethnicity: application.demographics?.ethnicity,
race: application.demographics?.race,
@@ -106,8 +110,45 @@ const ApplicationDemographics = () => {
}))
}
+ const raceKeys = useMemo(
+ () => getRaceEthnicityOptions(conductor.config.raceEthnicityConfiguration),
+ [conductor.config.raceEthnicityConfiguration]
+ )
+
+ // Field names FieldGroup registers for the race group, e.g. "race-white". Because
+ // the race options have subFields, FieldGroup names every input `${name}-${value}`.
+ const allRaceFieldNames = useMemo(
+ () =>
+ raceKeys
+ ? Object.entries(raceKeys).flatMap(([rootKey, subKeys]) => [
+ `race-${rootKey}`,
+ ...subKeys.map((subKey) => `race-${subKey}`),
+ ])
+ : [],
+ [raceKeys]
+ )
+
+ const exclusiveRaceFieldNames = useMemo(
+ () => allRaceFieldNames.filter((fieldName) => fieldName === `race-${RACE_DECLINE_TO_RESPOND}`),
+ [allRaceFieldNames]
+ )
+
+ // Checking "Decline to respond" clears every other race option; checking any
+ // other option clears "Decline to respond". Reuses the shared setExclusive
+ // helper that powers the same behavior in ApplicationMultiselectQuestionStep.
+ const handleRaceExclusiveChange = (
+ changedFieldName: string,
+ isExclusiveOption: boolean,
+ checked: boolean
+ ) => {
+ if (isExclusiveOption && checked) {
+ setExclusive(true, setValue, exclusiveRaceFieldNames, changedFieldName, allRaceFieldNames)
+ } else if (!isExclusiveOption) {
+ setExclusive(false, setValue, exclusiveRaceFieldNames, changedFieldName, allRaceFieldNames)
+ }
+ }
+
const raceOptions = useMemo(() => {
- const raceKeys = getRaceEthnicityOptions(conductor.config.raceEthnicityConfiguration)
if (!raceKeys) return []
return Object.keys(raceKeys).map((rootKey) => ({
id: rootKey,
@@ -116,6 +157,14 @@ const ApplicationDemographics = () => {
additionalText: rootKey.indexOf("other") >= 0,
defaultChecked: isKeyIncluded(rootKey, application.demographics?.race),
defaultText: getCustomValue(rootKey, application.demographics?.race),
+ inputProps: {
+ onChange: (e: React.ChangeEvent) =>
+ handleRaceExclusiveChange(
+ `race-${rootKey}`,
+ rootKey === RACE_DECLINE_TO_RESPOND,
+ e.target.checked
+ ),
+ },
subFields: raceKeys[rootKey].map((subKey) => ({
id: subKey,
label: t(`application.review.demographics.raceOptions.${subKey}`),
@@ -123,12 +172,16 @@ const ApplicationDemographics = () => {
defaultChecked: isKeyIncluded(subKey, application.demographics?.race),
additionalText: subKey.indexOf("other") >= 0,
defaultText: getCustomValue(subKey, application.demographics?.race),
+ inputProps: {
+ onChange: (e: React.ChangeEvent) =>
+ handleRaceExclusiveChange(`race-${subKey}`, false, e.target.checked),
+ },
dataTestId: subKey,
})),
dataTestId: rootKey,
}))
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [register])
+ }, [register, raceKeys])
useEffect(() => {
pushGtmEvent({