Skip to content

Commit 3157b97

Browse files
committed
Disable datalist suggestions on mobile
datalist is broken on mobile Firefox (invisible) and Chrome (transparent background glitch). Create datalist dynamically via JS only when the primary pointer is fine (mouse/trackpad).
1 parent eb32b11 commit 3157b97

File tree

2 files changed

+72
-65
lines changed

2 files changed

+72
-65
lines changed

internal/ui/packagelist/templates.templ

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,14 +57,12 @@ templ PackagesContent(list *packages.PackagePopularityList, query string, curren
5757
name="query"
5858
placeholder="Search packages"
5959
value={ query }
60-
list="package-suggestions"
6160
/>
6261
if compare != "" {
6362
<input type="hidden" name="compare" value={ compare }/>
6463
}
6564
<button class="btn btn-primary d-none d-sm-block" type="submit">Search</button>
6665
</div>
67-
<datalist id="package-suggestions"></datalist>
6866
</form>
6967
if len(list.PackagePopularities) > 0 {
7068
@PackageTable(list.PackagePopularities, query, currentOffset, limit, compare)

src/package-search-suggest.ts

Lines changed: 72 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,77 +1,86 @@
1-
const DEBOUNCE_MS = 200;
2-
const MIN_QUERY_LENGTH = 2;
3-
const SUGGESTION_LIMIT = 10;
1+
if (matchMedia("(pointer: coarse)").matches) {
2+
// datalist is broken on mobile browsers — skip suggestions
3+
} else {
4+
const DEBOUNCE_MS = 200;
5+
const MIN_QUERY_LENGTH = 2;
6+
const SUGGESTION_LIMIT = 10;
47

5-
const input = document.querySelector<HTMLInputElement>("#package-search");
6-
const datalist = document.querySelector<HTMLDataListElement>(
7-
"#package-suggestions",
8-
);
8+
const input = document.querySelector<HTMLInputElement>("#package-search");
99

10-
if (input && datalist) {
11-
let controller: AbortController | null = null;
12-
let timer: ReturnType<typeof setTimeout>;
13-
let previousValue = input.value;
10+
if (input) {
11+
const datalist = document.createElement("datalist");
12+
datalist.id = "package-suggestions";
13+
input.after(datalist);
14+
input.setAttribute("list", "package-suggestions");
1415

15-
const isSuggestion = (query: string): boolean =>
16-
datalist.querySelector(`option[value="${CSS.escape(query)}"]`) !== null;
16+
let controller: AbortController | null = null;
17+
let timer: ReturnType<typeof setTimeout>;
18+
let previousValue = input.value;
1719

18-
const isSelection = (query: string): boolean => {
19-
const lengthDiff = query.length - previousValue.length;
20-
return lengthDiff > 1 && isSuggestion(query);
21-
};
20+
const isSuggestion = (query: string): boolean =>
21+
datalist.querySelector(`option[value="${CSS.escape(query)}"]`) !==
22+
null;
2223

23-
const fetchSuggestions = async (
24-
query: string,
25-
signal: AbortSignal,
26-
): Promise<string[]> => {
27-
const res = await fetch(
28-
`/api/packages?query=${encodeURIComponent(query)}&limit=${SUGGESTION_LIMIT}`,
29-
{ signal },
30-
);
31-
const data = await res.json();
32-
return data.packagePopularities.map((p: { name: string }) => p.name);
33-
};
24+
const isSelection = (query: string): boolean => {
25+
const lengthDiff = query.length - previousValue.length;
26+
return lengthDiff > 1 && isSuggestion(query);
27+
};
3428

35-
const updateDatalist = (names: string[]): void => {
36-
datalist.replaceChildren(
37-
...names.map((name) => {
38-
const option = document.createElement("option");
39-
option.value = name;
40-
return option;
41-
}),
42-
);
43-
};
29+
const fetchSuggestions = async (
30+
query: string,
31+
signal: AbortSignal,
32+
): Promise<string[]> => {
33+
const res = await fetch(
34+
`/api/packages?query=${encodeURIComponent(query)}&limit=${SUGGESTION_LIMIT}`,
35+
{ signal },
36+
);
37+
const data = await res.json();
38+
return data.packagePopularities.map(
39+
(p: { name: string }) => p.name,
40+
);
41+
};
4442

45-
input.addEventListener("input", () => {
46-
clearTimeout(timer);
47-
const query = input.value.trim();
43+
const updateDatalist = (names: string[]): void => {
44+
datalist.replaceChildren(
45+
...names.map((name) => {
46+
const option = document.createElement("option");
47+
option.value = name;
48+
return option;
49+
}),
50+
);
51+
};
4852

49-
if (isSelection(query)) {
50-
input.form?.submit();
51-
return;
52-
}
53+
input.addEventListener("input", () => {
54+
clearTimeout(timer);
55+
const query = input.value.trim();
5356

54-
previousValue = query;
57+
if (isSelection(query)) {
58+
input.form?.submit();
59+
return;
60+
}
61+
62+
previousValue = query;
5563

56-
if (query.length < MIN_QUERY_LENGTH) {
57-
updateDatalist([]);
58-
return;
59-
}
64+
if (query.length < MIN_QUERY_LENGTH) {
65+
updateDatalist([]);
66+
return;
67+
}
6068

61-
timer = setTimeout(async () => {
62-
controller?.abort();
63-
controller = new AbortController();
69+
timer = setTimeout(async () => {
70+
controller?.abort();
71+
controller = new AbortController();
6472

65-
try {
66-
updateDatalist(
67-
await fetchSuggestions(query, controller.signal),
68-
);
69-
} catch (e) {
70-
if (e instanceof DOMException && e.name === "AbortError") {
71-
return;
73+
try {
74+
updateDatalist(
75+
await fetchSuggestions(query, controller.signal),
76+
);
77+
} catch (e) {
78+
if (e instanceof DOMException && e.name === "AbortError") {
79+
return;
80+
}
81+
throw e;
7282
}
73-
throw e;
74-
}
75-
}, DEBOUNCE_MS);
76-
});
83+
}, DEBOUNCE_MS);
84+
});
85+
}
7786
}

0 commit comments

Comments
 (0)