1+ ---
2+ interface Props {
3+ isMobile? : boolean ;
4+ options? : string [];
5+ }
6+
7+ const {
8+ isMobile,
9+ options
10+ } = Astro .props ;
11+
12+ const suffix = isMobile ? ' -mobile' : ' ' ;
13+ ---
14+
15+ <script >
16+ // use a custom element to make init easy
17+ class SearchableDropdown extends HTMLElement {
18+ private input: HTMLInputElement;
19+ private list: HTMLElement;
20+ private noResults: HTMLElement;
21+ private items: HTMLElement[];
22+
23+ constructor() {
24+ super();
25+ this.input = this.querySelector('.dropdown-input')!;
26+ this.list = this.querySelector('.dropdown-list')!;
27+ this.noResults = this.querySelector('.no-results')!;
28+ this.items = Array.from(this.querySelectorAll('.dropdown-item'));
29+ }
30+
31+ // when element added
32+ connectedCallback() {
33+ this.input.addEventListener('input', (e) => this.handleInput(e));
34+ this.input.addEventListener('focus', () => this.showDropdown());
35+
36+ this.items.forEach(item => {
37+ item.addEventListener('click', () => this.selectItem(item.dataset.value!));
38+ });
39+ document.addEventListener('click', this.handleOutsideClick);
40+ }
41+
42+ // when element removed
43+ disconnectedCallback() {
44+ document.removeEventListener('click', this.handleOutsideClick);
45+ }
46+
47+ private handleOutsideClick = (e: MouseEvent) => {
48+ if (!this.contains(e.target as Node)) {
49+ this.hideDropdown();
50+ }
51+ }
52+
53+ private handleInput(e: Event) {
54+ const searchTerm = (e.target as HTMLInputElement).value.toLowerCase();
55+ let visibleCount = 0;
56+
57+ this.items.forEach(item => {
58+ const value = item.dataset.value?.toLowerCase() || '';
59+ const isMatch = value.includes(searchTerm);
60+ item.style.display = isMatch ? 'block' : 'none';
61+ if (isMatch) visibleCount++;
62+ });
63+
64+ const hasInput = searchTerm.length > 0;
65+ this.list.style.display = visibleCount > 0 ? 'block' : 'none';
66+ this.noResults.style.display = (visibleCount === 0 && hasInput) ? 'block' : 'none';
67+ }
68+
69+ private showDropdown() {
70+ const hasVisibleItems = this.items.some(item => item.style.display !== 'none');
71+ if (hasVisibleItems) this.list.style.display = 'block';
72+ }
73+
74+ private hideDropdown() {
75+ this.list.style.display = 'none';
76+ this.noResults.style.display = 'none';
77+ }
78+
79+ private selectItem(value: string) {
80+ this.input.value = value;
81+ this.hideDropdown();
82+ this.input.dispatchEvent(new Event('input', { bubbles: true }));
83+ }
84+ }
85+
86+ // Register the custom element
87+ customElements.define('searchable-dropdown', SearchableDropdown);
88+ </script >
89+
90+ <searchable-dropdown data-suffix ={ suffix } >
91+ <input
92+ type =" text"
93+ id ={ ` return-type-filter-input${suffix } ` }
94+ placeholder =" e.g., number, text, player"
95+ class =" dropdown-input"
96+ autocomplete =" off"
97+ />
98+
99+ <ul class =" dropdown-list" style =" display: none;" >
100+ { options ?.map ((option ) => (
101+ <li class = " dropdown-item" data-value = { option } >
102+ { option }
103+ </li >
104+ ))}
105+ </ul >
106+
107+ <div class =" no-results" style =" display: none;" >
108+ No matches found
109+ </div >
110+ </div >
111+
112+ <style >
113+ searchable-dropdown {
114+ position: relative;
115+ width: 16rem;
116+ }
117+
118+ .dropdown-input {
119+ width: 100%;
120+ padding: 0.5rem 0.75rem;
121+ border: 1px solid #d1d5db;
122+ border-radius: 0.375rem;
123+ outline: none;
124+ }
125+
126+ .dropdown-input:focus {
127+ ring: 2px;
128+ ring-color: #3b82f6;
129+ border-color: #3b82f6;
130+ }
131+
132+ .dropdown-list {
133+ position: absolute;
134+ z-index: 10;
135+ width: 100%;
136+ margin-top: 0.25rem;
137+ background-color: white;
138+ border: 1px solid #d1d5db;
139+ border-radius: 0.375rem;
140+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
141+ max-height: 15rem;
142+ overflow-y: auto;
143+ list-style: none;
144+ padding: 0;
145+ margin: 0.25rem 0 0 0;
146+ }
147+
148+ .dropdown-item {
149+ padding: 0.5rem 0.75rem;
150+ cursor: pointer;
151+ transition: background-color 0.15s;
152+ }
153+
154+ .dropdown-item:hover {
155+ background-color: #eff6ff;
156+ }
157+
158+ .no-results {
159+ position: absolute;
160+ z-index: 10;
161+ width: 100%;
162+ margin-top: 0.25rem;
163+ background-color: white;
164+ border: 1px solid #d1d5db;
165+ border-radius: 0.375rem;
166+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
167+ padding: 0.75rem;
168+ color: #6b7280;
169+ font-size: 0.875rem;
170+ }
171+ </style >
172+
0 commit comments