From 5fb2f7c39f6faf4fc3cab3ccd1205b71ac0775a2 Mon Sep 17 00:00:00 2001 From: James Lowenthal Date: Sun, 11 Feb 2024 10:08:44 -0500 Subject: [PATCH 1/4] Add choices to package --- package-lock.json | 51 +++++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + 2 files changed, 52 insertions(+) diff --git a/package-lock.json b/package-lock.json index 98d0aac..dc58730 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@fortawesome/fontawesome-svg-core": "^6.4.0", "@fortawesome/free-solid-svg-icons": "^6.4.0", "@parcel/resolver-glob": "^2.10.3", + "choices.js": "^10.2.0", "leaflet": "~1.9.3" }, "devDependencies": { @@ -214,6 +215,22 @@ "node": ">=4" } }, + "node_modules/@babel/runtime": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz", + "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/runtime/node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -2734,6 +2751,16 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/choices.js": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/choices.js/-/choices.js-10.2.0.tgz", + "integrity": "sha512-8PKy6wq7BMjNwDTZwr3+Zry6G2+opJaAJDDA/j3yxvqSCnvkKe7ZIFfIyOhoc7htIWFhsfzF9tJpGUATcpUtPg==", + "dependencies": { + "deepmerge": "^4.2.2", + "fuse.js": "^6.6.2", + "redux": "^4.2.0" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -3054,6 +3081,14 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/define-data-property": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.2.tgz", @@ -3822,6 +3857,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/fuse.js": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-6.6.2.tgz", + "integrity": "sha512-cJaJkxCCxC8qIIcPBF9yGxY0W/tVZS3uEISDxhYIdtk8OL93pe+6Zj7LjCqVV4dzbqcriOZ+kQ/NE4RXZHsIGA==", + "engines": { + "node": ">=10" + } + }, "node_modules/get-intrinsic": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", @@ -5545,6 +5588,14 @@ "node": ">=8.10.0" } }, + "node_modules/redux": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "dependencies": { + "@babel/runtime": "^7.9.2" + } + }, "node_modules/regenerator-runtime": { "version": "0.13.11", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", diff --git a/package.json b/package.json index 14ed569..4c8191d 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@fortawesome/fontawesome-svg-core": "^6.4.0", "@fortawesome/free-solid-svg-icons": "^6.4.0", "@parcel/resolver-glob": "^2.10.3", + "choices.js": "^10.2.0", "leaflet": "~1.9.3" }, "devDependencies": { From 164b5429e8814d57f8755f77f08ba89c5d090f48 Mon Sep 17 00:00:00 2001 From: James Lowenthal Date: Sun, 11 Feb 2024 10:08:57 -0500 Subject: [PATCH 2/4] Use choices.js single select --- index.html | 7 ++++++- src/css/_header.scss | 28 ++++++++++++++++++++++++- src/js/dropdown.js | 24 ++++++++++++++++++++++ src/js/setUpSite.js | 23 +++++---------------- tests/app/setUpSite.test.js | 41 +++++++++++++++++-------------------- 5 files changed, 81 insertions(+), 42 deletions(-) create mode 100644 src/js/dropdown.js diff --git a/index.html b/index.html index cfe9d74..018fb91 100644 --- a/index.html +++ b/index.html @@ -76,7 +76,12 @@ Parking Lot Map
- +
diff --git a/src/css/_header.scss b/src/css/_header.scss index 02faa66..204c2d9 100644 --- a/src/css/_header.scss +++ b/src/css/_header.scss @@ -21,6 +21,8 @@ header { .header-about-icon { margin-right: 0.75em; } + + margin-left: auto; } @media only screen and (min-width: 48em) { @@ -39,14 +41,38 @@ header { .site-title { flex: 1; } + + #city-dropdown { + display: flex; + align-items: center; + gap: 10px; + height: 40px; + margin-bottom: 6px; + } } #city-dropdown label { font-weight: normal; } -#city-choice { +.choices__inner { border: 2px solid rgba(0, 0, 0, 0.2); border-radius: 10px; color: black; + height: 40px; + + @media only screen and (min-width: 285px) { + min-width: 285px; + } +} + +.choices__item--selectable { + font-size: 14px; + height: 40px; +} + +.choices__list--dropdown, +.choices__list[aria-expanded] { + z-index: 1001; + color: black; } diff --git a/src/js/dropdown.js b/src/js/dropdown.js new file mode 100644 index 0000000..204bade --- /dev/null +++ b/src/js/dropdown.js @@ -0,0 +1,24 @@ +import Choices from "choices.js"; +import "choices.js/public/assets/styles/choices.css"; +import scoreCardsData from "../../data/score-cards.json"; + +export const DROPDOWN = new Choices("#city-choice", { + allowHTML: false, + itemSelectText: "Select", + searchEnabled: true, +}); + +const setUpDropdown = (initialCityId, fallBackCityId) => { + const cities = Object.entries(scoreCardsData).map(([id, { Name }]) => ({ + value: id, + label: Name, + })); + DROPDOWN.setChoices(cities); + if (Object.keys(scoreCardsData).includes(initialCityId)) { + DROPDOWN.setChoiceByValue(initialCityId); + } else { + DROPDOWN.setChoiceByValue(fallBackCityId); + } +}; + +export default setUpDropdown; diff --git a/src/js/setUpSite.js b/src/js/setUpSite.js index 1b8e9f8..abcaadb 100644 --- a/src/js/setUpSite.js +++ b/src/js/setUpSite.js @@ -7,6 +7,7 @@ import setUpIcons from "./fontAwesome"; import scoreCardsData from "../../data/score-cards.json"; import setUpAbout from "./about"; import setUpShareUrlClickListener from "./share"; +import setUpDropdown, { DROPDOWN } from "./dropdown"; const parkingLots = import("../../data/parking-lots/*"); // eslint-disable-line @@ -48,21 +49,6 @@ const STYLES = { }, }; -const addCitiesToToggle = (initialCityId, fallbackCityId) => { - const cityToggleElement = document.getElementById("city-choice"); - let validInitialId = false; - Object.entries(scoreCardsData).forEach(([id, { Name }]) => { - if (id === initialCityId) { - validInitialId = true; - } - const option = document.createElement("option"); - option.value = id; - option.textContent = Name; - cityToggleElement.appendChild(option); - }); - cityToggleElement.value = validInitialId ? initialCityId : fallbackCityId; -}; - /** * Create the initial map object. * @@ -138,7 +124,8 @@ const loadParkingLot = async (cityId, parkingLayer) => { .getLayers() .find((city) => city.feature.properties.id === cityId); if (!alreadyLoaded) { - parkingLayer.addData(await parkingLots[`${cityId}.geojson`]()); + const lots = await parkingLots; + parkingLayer.addData(await lots[`${cityId}.geojson`]()); parkingLayer.bringToBack(); // Ensures city boundary is on top") } }; @@ -198,7 +185,7 @@ const setUpAutoScorecard = async (map, cities, parkingLayer) => { } }); if (centralCity) { - document.getElementById("city-choice").value = centralCity; + DROPDOWN.setChoiceByValue(centralCity); setScorecard(centralCity, cities[centralCity]); } }); @@ -280,7 +267,7 @@ const setUpSite = async () => { setUpIcons(); const initialCityId = extractCityIdFromUrl(window.location.href); - addCitiesToToggle(initialCityId, "atlanta-ga"); + setUpDropdown(initialCityId, "atlanta-ga"); setUpAbout(); const map = createMap(); diff --git a/tests/app/setUpSite.test.js b/tests/app/setUpSite.test.js index 81aac62..1cc93e1 100644 --- a/tests/app/setUpSite.test.js +++ b/tests/app/setUpSite.test.js @@ -19,18 +19,14 @@ test("every city is in the toggle", async ({ page }) => { const data = JSON.parse(rawData); const expectedCities = Object.values(data).map((scoreCard) => scoreCard.Name); - await page.goto(""); - - // Wait a second to make sure the site is fully loaded. - await page.waitForTimeout(1000); + await page.goto("/"); + await page.waitForSelector(".choices"); - const toggleValues = await page.evaluate(() => { - const select = document.querySelector("#city-choice"); - return Array.from(select.querySelectorAll("option")).map((opt) => - opt.textContent.trim() - ); - }); + const toggleValues = await page.$$eval(".choices__item--choice", (elements) => + Array.from(elements.map((opt) => opt.textContent.trim())) + ); + toggleValues.sort(); expectedCities.sort(); expect(toggleValues).toEqual(expectedCities); }); @@ -47,11 +43,12 @@ test("correctly load the city score card", async ({ page }) => { route.continue(); }); - await page.goto(""); + await page.goto("/"); expect(albanyLoaded).toBe(false); + await page.waitForSelector(".choices"); - const selectElement = await page.$("#city-choice"); - await selectElement.selectOption("albany-ny"); + await page.click(".choices"); + await page.click('.choices__item--choice >> text="Albany, NY"'); await page.waitForFunction(() => { const titleElement = document.querySelector( ".leaflet-popup-content .title" @@ -93,10 +90,8 @@ test.describe("the share feature", () => { const context = await browser.newContext(); await context.grantPermissions(["clipboard-read", "clipboard-write"]); const page = await context.newPage(); - await page.goto(""); - - // Wait a second to make sure the site is fully loaded. - await page.waitForTimeout(1000); + await page.goto("/"); + await page.waitForSelector(".url-copy-button > a"); await page.click(".url-copy-button > a"); const firstCityClipboardText = await page.evaluate(() => @@ -106,8 +101,9 @@ test.describe("the share feature", () => { // Check that the share button works when changing the city, too. // This is a regression test. - const selectElement = await page.$("#city-choice"); - await selectElement.selectOption("anchorage-ak"); + await page.waitForSelector(".choices"); + await page.click(".choices"); + await page.click('.choices__item--choice >> text="Anchorage, AK"'); await page.waitForFunction(() => { const titleElement = document.querySelector( ".leaflet-popup-content .title" @@ -129,7 +125,7 @@ test.describe("the share feature", () => { await page.goto("#parking-reform-map=fort-worth-tx"); // Wait a second to make sure the site is fully loaded. - await page.waitForTimeout(1000); + await page.waitForSelector(".leaflet-popup-content .title"); const [scoreCardTitle, cityToggleValue] = await page.evaluate(() => { const title = document.querySelector( @@ -234,8 +230,7 @@ test.describe("auto-focus city", () => { test("scorecard pulls up city closest to center", async ({ page }) => { await page.goto(""); - // Wait a second to make sure the site is fully loaded. - await page.waitForTimeout(1000); + await page.waitForSelector(".leaflet-control-zoom-out"); // Zoom out. await page @@ -244,6 +239,8 @@ test("scorecard pulls up city closest to center", async ({ page }) => { // Drag map to Birmingham await dragMap(page, 300); + + await page.waitForSelector(".choices"); const [scoreCardTitle, cityToggleValue] = await page.evaluate(() => { const title = document.querySelector( ".leaflet-popup-content .title" From 6f7bbc71477e174d2d9de10f4bfa4f17e1e6ec86 Mon Sep 17 00:00:00 2001 From: James Lowenthal Date: Sun, 11 Feb 2024 17:19:46 -0500 Subject: [PATCH 3/4] Revert unnecessary change --- src/js/setUpSite.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/js/setUpSite.js b/src/js/setUpSite.js index abcaadb..c89d449 100644 --- a/src/js/setUpSite.js +++ b/src/js/setUpSite.js @@ -124,8 +124,7 @@ const loadParkingLot = async (cityId, parkingLayer) => { .getLayers() .find((city) => city.feature.properties.id === cityId); if (!alreadyLoaded) { - const lots = await parkingLots; - parkingLayer.addData(await lots[`${cityId}.geojson`]()); + parkingLayer.addData(await parkingLots[`${cityId}.geojson`]()); parkingLayer.bringToBack(); // Ensures city boundary is on top") } }; From 7c7649a8493e82119e8e8b912dc7dca58af45840 Mon Sep 17 00:00:00 2001 From: James Lowenthal Date: Sun, 11 Feb 2024 17:19:55 -0500 Subject: [PATCH 4/4] Use em where easy --- src/css/_header.scss | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/css/_header.scss b/src/css/_header.scss index 204c2d9..69a46bf 100644 --- a/src/css/_header.scss +++ b/src/css/_header.scss @@ -45,7 +45,7 @@ header { #city-dropdown { display: flex; align-items: center; - gap: 10px; + gap: 0.7em; height: 40px; margin-bottom: 6px; } @@ -61,8 +61,8 @@ header { color: black; height: 40px; - @media only screen and (min-width: 285px) { - min-width: 285px; + @media only screen and (min-width: 20em) { + min-width: 20em; } }