Skip to content

Commit

Permalink
basti/serverSearch (#42)
Browse files Browse the repository at this point in the history
Based on top of #41 as i need icons :( 

This adds server search. You can either search a city or country. If
only one country is left, we auto expand it, so less clicks needed.


https://github.com/user-attachments/assets/9f63e76e-5d23-4134-95c3-145e7c6d6185
  • Loading branch information
strseb authored Aug 30, 2024
1 parent e332f15 commit 7c2c70f
Show file tree
Hide file tree
Showing 4 changed files with 172 additions and 8 deletions.
4 changes: 4 additions & 0 deletions src/_locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -115,5 +115,9 @@
"altTextOpenSettingsPage": {
"message": "Open Settings Page",
"description": "Alt text for the button that opens the Extensions Settings page"
},
"searchServer": {
"message": "Search Server",
"description": "Placeholder for the Input field Where users can search the Serverlist"
}
}
71 changes: 69 additions & 2 deletions src/components/serverlist.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@ import {
LitElement,
classMap,
styleMap,
createRef,
ref,
} from "../vendor/lit-all.min.js";
import { resetSizing } from "./styles.js";

import { tr } from "../shared/i18n.js";

import { ServerCity } from "../background/vpncontroller/states.js";

/**
Expand Down Expand Up @@ -92,18 +96,51 @@ export class ServerList extends LitElement {
detail: { city, country },
});
}
filterInput = createRef();

render() {
return countrylistHolder(
const filteredList = filterList(
this.serverList,
this.#getCountryListItem.bind(this)
this.filterInput.value?.value
);
let countryListProvider = this.#getCountryListItem.bind(this);
if (filteredList.length == 1) {
// Nit: If we only have one, use a countryListItem provider that
// forces the list to be open :)
countryListProvider = (serverCountry) => {
return countryListItem(
serverCountry,
true,
() => {},
this.#getCityItem.bind(this)
);
};
}

return html`
<input
type="text"
class="search"
placeholder="${tr("searchServer")}"
${ref(this.filterInput)}
@change=${() => this.requestUpdate()}
@input=${() => this.requestUpdate()}
/>
${countrylistHolder(filteredList, countryListProvider)}
`;
}

static styles = css`
${resetSizing}
:host {
display: flex;
flex-direction: column;
align-items: center;
}
#moz-vpn-server-list-panel {
width: 100%;
block-size: var(--panelSize);
max-block-size: var(--panelSize);
min-block-size: var(--panelSize);
Expand Down Expand Up @@ -232,6 +269,20 @@ export class ServerList extends LitElement {
color: var(--text-color-primary);
padding-inline-start: 18px;
}
input.search {
margin-bottom: 16px;
padding: 10px 20px;
padding-left: 30px;
color: var(--text-color-invert);
width: calc(max(50%, 300px));
background-image: url("../../assets/img/search-icon.svg");
background-position: 2.5px 6px;
background-repeat: no-repeat;
border: 2px solid var(--border-color);
border-radius: 5px;
color: black;
}
`;
}
customElements.define("server-list", ServerList);
Expand Down Expand Up @@ -342,3 +393,19 @@ export const countrylistHolder = (
</div>
`;
};

/**
*
* @param {Array<ServerCountry>} serverCountryList - The input List
* @param {String} filterString - A String to filter by
* @returns {Array<ServerCountry>} - The Filtered List
*/
export const filterList = (serverCountryList, filterString = "") => {
const target = filterString.toLowerCase();
return serverCountryList.filter((c) => {
if (c.name.toLowerCase().includes(target)) {
return true;
}
return c.cities.some((cty) => cty.name.toLowerCase().includes(target));
});
};
12 changes: 8 additions & 4 deletions src/shared/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,14 @@
* @returns The Message translated for the Users Language.
*/
export const tr = (id, ...arg) => {
const candidate = browser.i18n.getMessage(id, arg);
if (!candidate || candidate.length === 0) {
console.error(`Missing Translation Message for ${id}`);
try {
const candidate = browser.i18n.getMessage(id, arg);
if (!candidate || candidate.length === 0) {
console.error(`Missing Translation Message for ${id}`);
return id;
}
return candidate;
} catch (error) {
return id;
}
return candidate;
};
93 changes: 91 additions & 2 deletions tests/jest/components/serverlist.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,14 @@ const testCountry = (() => {
out.name = "Testville";
return out;
})();
const testServerList = [testCountry];
const testCountry2 = (() => {
let out = new ServerCountry();
out.cities = [testCity, otherTestCity];
out.code = "oh-no";
out.name = "OtherVille";
return out;
})();
const testServerList = [testCountry, testCountry2];
//#endregion

describe("Serverlist Templates", () => {
Expand Down Expand Up @@ -194,6 +201,27 @@ describe("Serverlist Templates", () => {
// Make sure once adopted to the dom it has rendered into a shadowdom
expect(element.shadowRoot).not.toBeNull();
});
test("Serverlist Expands Items on Click", async () => {
/** @type {ServerList} */
const element = document.createElement("server-list");
document.body.append(element);
element.serverList = testServerList;
await element.requestUpdate();
// Make sure importing the Module registers the custom element
expect(customElements.get("server-list")).toBe(ServerList);
//
const country = element.shadowRoot.querySelector(".server-list-item");
// Should not be opened by default
expect(country.classList.contains("opened")).toBe(false);
// After clicking on it, it should be opened
country.click();
await element.requestUpdate();
expect(country.classList.contains("opened")).toBe(true);
// Clicking it again should close it.
country.click();
await element.requestUpdate();
expect(country.classList.contains("opened")).toBe(false);
});

test("Serverlist element emits a signal if the active city changes", async () => {
/** @type {ServerList} */
Expand All @@ -207,7 +235,7 @@ describe("Serverlist Templates", () => {
document.body.append(element);
// Wait for lit to render to the dom
await element.requestUpdate();
const button = element.shadowRoot.querySelector("input");
const button = element.shadowRoot.querySelector("input[type='radio']");
expect(button.dataset.cityName).not.toBeNull();
button.click();

Expand All @@ -217,4 +245,65 @@ describe("Serverlist Templates", () => {
]);
expect(cityName).toBe(button.dataset.cityName);
});
test("Serverlist element *NOT* emits a signal if the same city is selected", async () => {
/** @type {ServerList} */
const element = document.createElement("server-list");
const newCityEmitted = new Promise((_, err) => {
element.addEventListener("selectedCityChanged", (e) => {
err(e.detail.city.name);
});
});
element.serverList = testServerList;
// Set the active city to the one we will click
element.selectedCity = testServerList[0].cities[0];
document.body.append(element);
// Wait for lit to render to the dom
await element.requestUpdate();
const button = element.shadowRoot.querySelector("input[type='radio']");
expect(button.dataset.cityName).not.toBeNull();
button.click();

const isOk = await Promise.race([
newCityEmitted,
// Queue a microtask to make sure all events have been processed
new Promise((res) => queueMicrotask(() => res(true))),
]);
expect(isOk).toBe(true);
});

test("Serverlist can filter by text input", async () => {
/** @type {ServerList} */
const element = document.createElement("server-list");

element.serverList = testServerList;
document.body.append(element);
// Wait for lit to render to the dom
await element.requestUpdate();
expect(
element.shadowRoot.querySelectorAll(".server-list-item").length
).toBe(2);
const queryInput = element.shadowRoot.querySelector("input[type='text']");
queryInput.value = "Other";
await element.requestUpdate();
expect(
element.shadowRoot.querySelectorAll(".server-list-item").length
).toBe(1);
});
test("Serverlist will auto open if only one Element is present", async () => {
/** @type {ServerList} */
const element = document.createElement("server-list");

element.serverList = testServerList;
document.body.append(element);
// Wait for lit to render to the dom
await element.requestUpdate();
expect(
element.shadowRoot.querySelectorAll(".server-list-item").length
).toBe(2);
const queryInput = element.shadowRoot.querySelector("input[type='text']");
queryInput.value = "Other";
await element.requestUpdate();
// We should now have one .opened City
expect(element.shadowRoot.querySelectorAll(".opened").length).toBe(1);
});
});

0 comments on commit 7c2c70f

Please sign in to comment.