From d84640d48068b51bb40eb240ad0ec6ba9643b7dc Mon Sep 17 00:00:00 2001
From: Eric Arellano <14852634+Eric-Arellano@users.noreply.github.com>
Date: Sat, 11 Jan 2025 11:19:31 -0500
Subject: [PATCH] Generalize setting up groups in dropdown (#306)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
packages/ct/src/js/dropdownGroups.ts | 13 ++++++
packages/ct/src/js/main.ts | 2 +
packages/primary/src/js/dropdownGroups.ts | 29 ++++++++++++
packages/primary/src/js/main.ts | 2 +
packages/primary/tests/communityMaps.test.ts | 21 ---------
packages/primary/tests/dropdownGroups.test.ts | 35 ++++++++++++++
packages/shared/src/js/bootstrap.ts | 4 +-
packages/shared/src/js/city-ui/dropdown.ts | 46 +++----------------
.../shared/src/js/city-ui/dropdownUtils.ts | 27 ++++++++++-
packages/shared/tests/dropdownUtils.test.ts | 33 ++++++++++++-
10 files changed, 149 insertions(+), 63 deletions(-)
create mode 100644 packages/ct/src/js/dropdownGroups.ts
create mode 100644 packages/primary/src/js/dropdownGroups.ts
delete mode 100644 packages/primary/tests/communityMaps.test.ts
create mode 100644 packages/primary/tests/dropdownGroups.test.ts
diff --git a/packages/ct/src/js/dropdownGroups.ts b/packages/ct/src/js/dropdownGroups.ts
new file mode 100644
index 0000000..c86263c
--- /dev/null
+++ b/packages/ct/src/js/dropdownGroups.ts
@@ -0,0 +1,13 @@
+import { DropdownGroup } from "@prn-parking-lots/shared/src/js/city-ui/dropdownUtils";
+import { CityStatsCollection } from "@prn-parking-lots/shared/src/js/model/types";
+
+export default function createDropdownGroups(
+ data: CityStatsCollection,
+): DropdownGroup[] {
+ return [
+ {
+ label: "Group 1",
+ cities: Object.entries(data).map(([id, { name }]) => ({ id, name })),
+ },
+ ];
+}
diff --git a/packages/ct/src/js/main.ts b/packages/ct/src/js/main.ts
index c382dea..3dd6e0a 100644
--- a/packages/ct/src/js/main.ts
+++ b/packages/ct/src/js/main.ts
@@ -5,6 +5,7 @@ import {
CITY_BOUNDARIES_GEOJSON,
PARKING_LOT_GEOJSON_MODULES,
} from "./data";
+import createDropdownGroups from "./dropdownGroups";
export default async function initApp(): Promise {
await bootstrapApp({
@@ -13,6 +14,7 @@ export default async function initApp(): Promise {
boundaries: CITY_BOUNDARIES_GEOJSON,
parkingLots: PARKING_LOT_GEOJSON_MODULES,
},
+ dropdownGroups: createDropdownGroups(CITY_STATS_DATA),
initialCity: "hartford",
});
}
diff --git a/packages/primary/src/js/dropdownGroups.ts b/packages/primary/src/js/dropdownGroups.ts
new file mode 100644
index 0000000..a20e594
--- /dev/null
+++ b/packages/primary/src/js/dropdownGroups.ts
@@ -0,0 +1,29 @@
+import {
+ DropdownGroup,
+ DropdownChoiceId,
+} from "@prn-parking-lots/shared/src/js/city-ui/dropdownUtils";
+import { CityStatsCollection } from "@prn-parking-lots/shared/src/js/model/types";
+
+export default function createDropdownGroups(
+ data: CityStatsCollection,
+): DropdownGroup[] {
+ const official: DropdownChoiceId[] = [];
+ const community: DropdownChoiceId[] = [];
+ Object.entries(data).forEach(([id, { name, contribution }]) => {
+ if (contribution) {
+ community.push({ id, name });
+ } else {
+ official.push({ id, name });
+ }
+ });
+ return [
+ {
+ label: "Official maps",
+ cities: official,
+ },
+ {
+ label: "Community maps",
+ cities: community,
+ },
+ ];
+}
diff --git a/packages/primary/src/js/main.ts b/packages/primary/src/js/main.ts
index 1116100..b951a47 100644
--- a/packages/primary/src/js/main.ts
+++ b/packages/primary/src/js/main.ts
@@ -5,6 +5,7 @@ import {
CITY_BOUNDARIES_GEOJSON,
PARKING_LOT_GEOJSON_MODULES,
} from "./data";
+import createDropdownGroups from "./dropdownGroups";
export default async function initApp(): Promise {
await bootstrapApp({
@@ -13,6 +14,7 @@ export default async function initApp(): Promise {
boundaries: CITY_BOUNDARIES_GEOJSON,
parkingLots: PARKING_LOT_GEOJSON_MODULES,
},
+ dropdownGroups: createDropdownGroups(CITY_STATS_DATA),
initialCity: "atlanta-ga",
});
}
diff --git a/packages/primary/tests/communityMaps.test.ts b/packages/primary/tests/communityMaps.test.ts
deleted file mode 100644
index e0b7bc2..0000000
--- a/packages/primary/tests/communityMaps.test.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import { expect, test } from "@playwright/test";
-
-test("there are exactly 103 official city maps", async ({ page }) => {
- await page.goto("/");
- await page.waitForSelector(".choices");
- await page.locator(".choices").click();
- await page.waitForSelector(".is-active");
-
- const toggleValues = await page.$eval(
- ".choices__list [role='listbox']",
- (element: HTMLElement) =>
- Array.from(element.children).map(
- (child: Element) => (child as HTMLElement).innerText,
- ),
- );
-
- const expectedCities = 103;
- const labelOffset = 1;
- const communityMapIndex = toggleValues.indexOf("Community maps");
- expect(communityMapIndex).toEqual(expectedCities + labelOffset);
-});
diff --git a/packages/primary/tests/dropdownGroups.test.ts b/packages/primary/tests/dropdownGroups.test.ts
new file mode 100644
index 0000000..d9337a8
--- /dev/null
+++ b/packages/primary/tests/dropdownGroups.test.ts
@@ -0,0 +1,35 @@
+import { expect, test } from "@playwright/test";
+import createDropdownGroups from "../src/js/dropdownGroups";
+
+test("createDropdownGroups", () => {
+ const common = {
+ percentage: "",
+ cityType: "",
+ population: "",
+ urbanizedAreaPopulation: "",
+ parkingScore: null,
+ reforms: "",
+ url: "",
+ };
+ const input = {
+ "city1-ny": {
+ ...common,
+ name: "City 1, NY",
+ },
+ "city2-ny": {
+ ...common,
+ name: "City 2, NY",
+ contribution: "some-email@web.com",
+ },
+ };
+ expect(createDropdownGroups(input)).toEqual([
+ {
+ label: "Official maps",
+ cities: [{ id: "city1-ny", name: "City 1, NY" }],
+ },
+ {
+ label: "Community maps",
+ cities: [{ id: "city2-ny", name: "City 2, NY" }],
+ },
+ ]);
+});
diff --git a/packages/shared/src/js/bootstrap.ts b/packages/shared/src/js/bootstrap.ts
index 7dae5a0..b5a1781 100644
--- a/packages/shared/src/js/bootstrap.ts
+++ b/packages/shared/src/js/bootstrap.ts
@@ -1,5 +1,6 @@
import subscribeScorecard from "./city-ui/scorecard";
import initDropdown from "./city-ui/dropdown";
+import type { DropdownGroup } from "./city-ui/dropdownUtils";
import initAbout from "./layout/about";
import initIcons from "./layout/fontAwesome";
@@ -23,6 +24,7 @@ import { initViewState } from "./state/ViewState";
interface Args {
data: DataSet;
initialCity: CityId;
+ dropdownGroups: DropdownGroup[];
}
export default async function bootstrapApp(args: Args): Promise {
@@ -45,7 +47,7 @@ export default async function bootstrapApp(args: Args): Promise {
args.initialCity,
);
- initDropdown(args.data.stats, viewState);
+ initDropdown(args.dropdownGroups, viewState);
subscribeScorecard(viewState, cityEntries);
subscribeShareLink(viewState);
subscribeSnapToCity(viewState, map, cityEntries);
diff --git a/packages/shared/src/js/city-ui/dropdown.ts b/packages/shared/src/js/city-ui/dropdown.ts
index 811aa30..5819597 100644
--- a/packages/shared/src/js/city-ui/dropdown.ts
+++ b/packages/shared/src/js/city-ui/dropdown.ts
@@ -1,10 +1,9 @@
import ChoicesJS from "choices.js";
-import { DropdownChoice, createChoice } from "./dropdownUtils";
-import type { CityStatsCollection } from "../model/types";
+import { DropdownGroup, convertToChoicesGroups } from "./dropdownUtils";
import { ViewStateObservable } from "../state/ViewState";
-function createDropdown(cityStatsData: CityStatsCollection): ChoicesJS {
+function createDropdown(groups: DropdownGroup[]): ChoicesJS {
const dropdown = new ChoicesJS("#city-dropdown", {
position: "bottom",
allowHTML: false,
@@ -12,57 +11,26 @@ function createDropdown(cityStatsData: CityStatsCollection): ChoicesJS {
searchEnabled: true,
searchResultLimit: 6,
searchFields: ["customProperties.city", "customProperties.context"],
- // Disabling this option allows us to properly handle search groups.
+ // Disabling this option allows us to properly handle groups.
// We already sort entries in the JSON file.
shouldSort: false,
});
-
- const officialCities: DropdownChoice[] = [];
- const communityCities: DropdownChoice[] = [];
- Object.entries(cityStatsData).forEach(([id, { name, contribution }]) => {
- const entry = createChoice(id, name);
- if (contribution) {
- communityCities.push(entry);
- } else {
- officialCities.push(entry);
- }
- });
-
- dropdown.setChoices([
- {
- value: "Official maps",
- label: "Official maps",
- disabled: false,
- choices: officialCities,
- },
- ]);
-
- if (communityCities.length > 0) {
- dropdown.setChoices([
- {
- value: "Community maps",
- label: "Community maps",
- disabled: false,
- choices: communityCities,
- },
- ]);
- }
-
+ dropdown.setChoices(convertToChoicesGroups(groups));
return dropdown;
}
export default function initDropdown(
- cityStatsData: CityStatsCollection,
+ groups: DropdownGroup[],
viewState: ViewStateObservable,
): void {
- const dropdown = createDropdown(cityStatsData);
+ const dropdown = createDropdown(groups);
viewState.subscribe(
({ cityId }) => dropdown.setChoiceByValue(cityId),
"set dropdown to city",
);
- // Bind user-changes in the dropdown to update the state in CitySelectionObservable.
+ // Bind user changes in the dropdown to update the view state.
// Note that `change` only triggers for user-driven changes, not programmatic updates.
const selectElement = dropdown.passedElement.element as HTMLSelectElement;
selectElement.addEventListener("change", () => {
diff --git a/packages/shared/src/js/city-ui/dropdownUtils.ts b/packages/shared/src/js/city-ui/dropdownUtils.ts
index 0d8a340..cbd95e5 100644
--- a/packages/shared/src/js/city-ui/dropdownUtils.ts
+++ b/packages/shared/src/js/city-ui/dropdownUtils.ts
@@ -1,6 +1,8 @@
+import type { Group as ChoicesJSGroup } from "choices.js";
+
import type { CityId } from "../model/types";
-export interface DropdownChoice {
+interface DropdownChoice {
value: string;
label: string;
customProperties: {
@@ -10,6 +12,16 @@ export interface DropdownChoice {
};
}
+export interface DropdownChoiceId {
+ id: CityId;
+ name: string;
+}
+
+export interface DropdownGroup {
+ label: string;
+ cities: Array;
+}
+
export function createChoice(id: CityId, name: string): DropdownChoice {
const [city, context] = name.split(/,\s|\s-\s/);
return {
@@ -21,3 +33,16 @@ export function createChoice(id: CityId, name: string): DropdownChoice {
},
};
}
+
+export function convertToChoicesGroups(
+ groups: DropdownGroup[],
+): ChoicesJSGroup[] {
+ return groups
+ .filter(({ cities }) => cities.length > 0)
+ .map(({ label, cities }) => ({
+ label,
+ value: label,
+ disabled: false,
+ choices: cities.map(({ id, name }) => createChoice(id, name)),
+ }));
+}
diff --git a/packages/shared/tests/dropdownUtils.test.ts b/packages/shared/tests/dropdownUtils.test.ts
index 6f3921a..da89604 100644
--- a/packages/shared/tests/dropdownUtils.test.ts
+++ b/packages/shared/tests/dropdownUtils.test.ts
@@ -1,5 +1,8 @@
import { expect, test } from "@playwright/test";
-import { createChoice } from "../src/js/city-ui/dropdownUtils";
+import {
+ createChoice,
+ convertToChoicesGroups,
+} from "../src/js/city-ui/dropdownUtils";
test.describe("createChoice()", () => {
test("city and state", () => {
@@ -26,3 +29,31 @@ test.describe("createChoice()", () => {
});
});
});
+
+test("convertToChoicesGroups()", () => {
+ const city1 = { id: "city1", name: "City 1" };
+ const city2 = { id: "city2", name: "City 2" };
+ const city3 = { id: "city2", name: "City 3" };
+ const input = [
+ { label: "group 1", cities: [city1] },
+ { label: "hidden", cities: [] },
+ { label: "group 2", cities: [city2, city3] },
+ ];
+ expect(convertToChoicesGroups(input)).toEqual([
+ {
+ value: "group 1",
+ label: "group 1",
+ disabled: false,
+ choices: [createChoice(city1.id, city1.name)],
+ },
+ {
+ value: "group 2",
+ label: "group 2",
+ disabled: false,
+ choices: [
+ createChoice(city2.id, city2.name),
+ createChoice(city3.id, city3.name),
+ ],
+ },
+ ]);
+});