@pagefind/component-ui 1.5.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,702 @@
1
+ import { PagefindElement } from "./base-element";
2
+ import { Instance } from "../core/instance";
3
+ import type { FilterCounts, FilterSelection, PagefindError } from "../types";
4
+
5
+ interface OptionRef {
6
+ el: HTMLElement;
7
+ value: string;
8
+ }
9
+
10
+ type SortOption = "default" | "alphabetical" | "count-desc" | "count-asc";
11
+
12
+ export class PagefindFilterDropdown extends PagefindElement {
13
+ static get observedAttributes(): string[] {
14
+ return ["filter", "label", "single-select", "show-empty", "wrap", "sort", "hide-clear"];
15
+ }
16
+
17
+ isOpen: boolean = false;
18
+ activeIndex: number = -1;
19
+ selectedValues: Set<string> = new Set();
20
+ isRendered: boolean = false;
21
+ filtersLoaded: boolean = false;
22
+
23
+ filterName: string | null = null;
24
+ availableFilters: Record<string, number> = {};
25
+ totalFilters: Record<string, number> = {};
26
+
27
+ singleSelect: boolean = false;
28
+ showEmpty: boolean = false;
29
+ wrapLabels: boolean = false;
30
+ hideClear: boolean = false;
31
+ sortOption: SortOption = "default";
32
+
33
+ wrapperEl: HTMLElement | null = null;
34
+ triggerEl: HTMLButtonElement | null = null;
35
+ menuEl: HTMLElement | null = null;
36
+ optionsEl: HTMLElement | null = null;
37
+ clearEl: HTMLButtonElement | null = null;
38
+ badgeEl: HTMLElement | null = null;
39
+ optionElements: OptionRef[] = [];
40
+
41
+ typeAheadBuffer: string = "";
42
+ typeAheadTimeout: ReturnType<typeof setTimeout> | null = null;
43
+
44
+ constructor() {
45
+ super();
46
+ this._handleClickOutside = this._handleClickOutside.bind(this);
47
+ }
48
+
49
+ init(): void {
50
+ this.filterName = this.getAttribute("filter");
51
+ if (!this.filterName) {
52
+ this.showError({
53
+ message: "filter attribute is required on <pagefind-filter-dropdown>",
54
+ });
55
+ return;
56
+ }
57
+
58
+ this.singleSelect = this.hasAttribute("single-select");
59
+ this.showEmpty = this.hasAttribute("show-empty");
60
+ this.wrapLabels = this.hasAttribute("wrap");
61
+ this.hideClear = this.hasAttribute("hide-clear");
62
+ if (this.hasAttribute("sort")) {
63
+ const sortVal = this.getAttribute("sort") as SortOption;
64
+ if (
65
+ ["default", "alphabetical", "count-desc", "count-asc"].includes(sortVal)
66
+ ) {
67
+ this.sortOption = sortVal;
68
+ }
69
+ }
70
+
71
+ this.render();
72
+ }
73
+
74
+ private sortValues(values: string[]): string[] {
75
+ if (this.sortOption === "default") {
76
+ return values;
77
+ }
78
+
79
+ const sorted = [...values];
80
+ switch (this.sortOption) {
81
+ case "alphabetical":
82
+ sorted.sort((a, b) => a.localeCompare(b));
83
+ break;
84
+ case "count-desc":
85
+ sorted.sort((a, b) => {
86
+ const countA = this.availableFilters[a] ?? this.totalFilters[a] ?? 0;
87
+ const countB = this.availableFilters[b] ?? this.totalFilters[b] ?? 0;
88
+ return countB - countA;
89
+ });
90
+ break;
91
+ case "count-asc":
92
+ sorted.sort((a, b) => {
93
+ const countA = this.availableFilters[a] ?? this.totalFilters[a] ?? 0;
94
+ const countB = this.availableFilters[b] ?? this.totalFilters[b] ?? 0;
95
+ return countA - countB;
96
+ });
97
+ break;
98
+ }
99
+ return sorted;
100
+ }
101
+
102
+ render(): void {
103
+ this.innerHTML = "";
104
+ const id = this.ensureId("pf-dropdown");
105
+ const triggerId = `${id}-trigger`;
106
+ const menuId = `${id}-menu`;
107
+
108
+ this.wrapperEl = document.createElement("div");
109
+ this.wrapperEl.className = "pf-dropdown-wrapper";
110
+
111
+ this.triggerEl = document.createElement("button");
112
+ this.triggerEl.type = "button";
113
+ this.triggerEl.id = triggerId;
114
+ this.triggerEl.className = "pf-dropdown-trigger";
115
+ if (this.wrapLabels) this.triggerEl.classList.add("wrap");
116
+ this.triggerEl.setAttribute("aria-haspopup", "listbox");
117
+ this.triggerEl.setAttribute("aria-expanded", "false");
118
+ this.triggerEl.setAttribute("aria-controls", menuId);
119
+
120
+ const labelSpan = document.createElement("span");
121
+ labelSpan.className = "pf-dropdown-trigger-label";
122
+ if (this.wrapLabels) labelSpan.classList.add("wrap");
123
+ labelSpan.textContent = this.getAttribute("label") || this.filterName || "";
124
+ this.triggerEl.appendChild(labelSpan);
125
+
126
+ this.badgeEl = document.createElement("span");
127
+ this.badgeEl.className = "pf-dropdown-selected-badge";
128
+ this.badgeEl.setAttribute("data-pf-hidden", "true");
129
+ // Count badge is visual only, accessible text set in updateBadge
130
+ this.badgeEl.setAttribute("aria-hidden", "true");
131
+ this.badgeEl.textContent = "0";
132
+ this.triggerEl.appendChild(this.badgeEl);
133
+
134
+ const arrow = document.createElement("span");
135
+ arrow.className = "pf-dropdown-arrow";
136
+ arrow.setAttribute("aria-hidden", "true");
137
+ this.triggerEl.appendChild(arrow);
138
+
139
+ this.wrapperEl.appendChild(this.triggerEl);
140
+
141
+ this.menuEl = document.createElement("div");
142
+ this.menuEl.id = menuId;
143
+ this.menuEl.className = "pf-dropdown-menu";
144
+ this.menuEl.hidden = true;
145
+
146
+ this.optionsEl = document.createElement("div");
147
+ this.optionsEl.className = "pf-dropdown-options";
148
+ this.optionsEl.setAttribute("role", "listbox");
149
+ this.optionsEl.setAttribute(
150
+ "aria-multiselectable",
151
+ this.singleSelect ? "false" : "true",
152
+ );
153
+ this.optionsEl.setAttribute("aria-labelledby", triggerId);
154
+ this.optionsEl.setAttribute("tabindex", "-1");
155
+ this.menuEl.appendChild(this.optionsEl);
156
+
157
+ this.wrapperEl.appendChild(this.menuEl);
158
+
159
+ // Clear button (optional)
160
+ if (!this.hideClear) {
161
+ this.clearEl = document.createElement("button");
162
+ this.clearEl.type = "button";
163
+ this.clearEl.className = "pf-dropdown-clear";
164
+ this.clearEl.setAttribute("aria-disabled", "true");
165
+ this.clearEl.setAttribute(
166
+ "aria-label",
167
+ (this.instance?.translate("clear_search") || "Clear") +
168
+ " " +
169
+ (this.getAttribute("label") || this.filterName || ""),
170
+ );
171
+ this.clearEl.textContent =
172
+ this.instance?.translate("clear_search") || "Clear";
173
+ this.wrapperEl.appendChild(this.clearEl);
174
+ this.clearEl.addEventListener("click", () => this.clearAll());
175
+ }
176
+
177
+ this.appendChild(this.wrapperEl);
178
+
179
+ this.triggerEl.addEventListener("click", () => this.toggle());
180
+ this.triggerEl.addEventListener("focus", () =>
181
+ this.instance?.triggerLoad(),
182
+ );
183
+ this.triggerEl.addEventListener("keydown", (e) =>
184
+ this.handleTriggerKeydown(e),
185
+ );
186
+ this.optionsEl.addEventListener("keydown", (e) =>
187
+ this.handleMenuKeydown(e),
188
+ );
189
+
190
+ this.isRendered = true;
191
+ }
192
+
193
+ toggle(): void {
194
+ if (this.isOpen) {
195
+ this.close();
196
+ } else {
197
+ this.open();
198
+ }
199
+ }
200
+
201
+ open(): void {
202
+ // Trigger Pagefind load when opening
203
+ this.instance?.triggerLoad();
204
+
205
+ if (this.isOpen || !this.menuEl || !this.triggerEl || !this.optionsEl)
206
+ return;
207
+ this.isOpen = true;
208
+
209
+ if (!this.filtersLoaded) {
210
+ this.showLoadingState();
211
+ }
212
+
213
+ this.menuEl.hidden = false;
214
+ this.triggerEl.setAttribute("aria-expanded", "true");
215
+ this.triggerEl.classList.add("open");
216
+
217
+ if (this.activeIndex < 0 && this.optionElements.length > 0) {
218
+ this.setActiveIndex(0);
219
+ }
220
+ this.optionsEl.focus();
221
+
222
+ const navigateText =
223
+ this.instance?.translate("keyboard_navigate") || "navigate";
224
+ const selectText = this.instance?.translate("keyboard_select") || "select";
225
+ const closeText = this.instance?.translate("keyboard_close") || "close";
226
+ this.instance?.registerShortcut(
227
+ { label: "↑↓", description: navigateText },
228
+ this,
229
+ );
230
+ this.instance?.registerShortcut(
231
+ { label: "↵", description: selectText },
232
+ this,
233
+ );
234
+ this.instance?.registerShortcut(
235
+ { label: "esc", description: closeText },
236
+ this,
237
+ );
238
+
239
+ setTimeout(() => {
240
+ document.addEventListener("click", this._handleClickOutside);
241
+ }, 0);
242
+ }
243
+
244
+ close(returnFocus = true): void {
245
+ if (!this.isOpen || !this.menuEl || !this.triggerEl || !this.optionsEl)
246
+ return;
247
+ this.isOpen = false;
248
+
249
+ this.menuEl.hidden = true;
250
+ this.triggerEl.setAttribute("aria-expanded", "false");
251
+ this.triggerEl.classList.remove("open");
252
+
253
+ this.optionsEl.removeAttribute("aria-activedescendant");
254
+
255
+ for (const { el } of this.optionElements) {
256
+ el.classList.remove("pf-dropdown-option-focused");
257
+ }
258
+
259
+ this.instance?.deregisterAllShortcuts(this);
260
+
261
+ document.removeEventListener("click", this._handleClickOutside);
262
+
263
+ if (returnFocus) {
264
+ this.triggerEl.focus();
265
+ }
266
+ }
267
+
268
+ private _handleClickOutside(event: MouseEvent): void {
269
+ if (this.wrapperEl && !this.wrapperEl.contains(event.target as Node)) {
270
+ this.close(false);
271
+ }
272
+ }
273
+
274
+ handleTriggerKeydown(e: KeyboardEvent): void {
275
+ switch (e.key) {
276
+ case "Enter":
277
+ case " ":
278
+ e.preventDefault();
279
+ this.open();
280
+ break;
281
+ case "ArrowDown":
282
+ e.preventDefault();
283
+ this.open();
284
+ this.setActiveIndex(0);
285
+ break;
286
+ case "ArrowUp":
287
+ e.preventDefault();
288
+ this.open();
289
+ this.setActiveIndex(this.optionElements.length - 1);
290
+ break;
291
+ }
292
+ }
293
+
294
+ handleMenuKeydown(e: KeyboardEvent): void {
295
+ switch (e.key) {
296
+ case "ArrowDown":
297
+ e.preventDefault();
298
+ this.moveActiveIndex(1);
299
+ break;
300
+ case "ArrowUp":
301
+ e.preventDefault();
302
+ this.moveActiveIndex(-1);
303
+ break;
304
+ case "Home":
305
+ e.preventDefault();
306
+ this.setActiveIndex(0);
307
+ break;
308
+ case "End":
309
+ e.preventDefault();
310
+ this.setActiveIndex(this.optionElements.length - 1);
311
+ break;
312
+ case "Enter":
313
+ case " ":
314
+ e.preventDefault();
315
+ if (
316
+ this.activeIndex >= 0 &&
317
+ this.activeIndex < this.optionElements.length
318
+ ) {
319
+ const activeOption = this.optionElements[this.activeIndex];
320
+ if (activeOption) {
321
+ this.toggleOption(activeOption.value);
322
+ }
323
+ }
324
+ break;
325
+ case "Escape":
326
+ e.preventDefault();
327
+ this.close();
328
+ break;
329
+ case "Tab":
330
+ this.close(false);
331
+ break;
332
+ default:
333
+ if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
334
+ this.handleTypeAhead(e.key);
335
+ }
336
+ }
337
+ }
338
+
339
+ setActiveIndex(index: number): void {
340
+ if (index < 0 || index >= this.optionElements.length || !this.optionsEl)
341
+ return;
342
+
343
+ for (const { el } of this.optionElements) {
344
+ el.classList.remove("pf-dropdown-option-focused");
345
+ }
346
+
347
+ this.activeIndex = index;
348
+ const option = this.optionElements[index];
349
+
350
+ option.el.classList.add("pf-dropdown-option-focused");
351
+ this.optionsEl.setAttribute("aria-activedescendant", option.el.id);
352
+
353
+ this.scrollToCenter(option.el);
354
+ }
355
+
356
+ private scrollToCenter(el: HTMLElement): void {
357
+ if (!this.optionsEl) return;
358
+ const container = this.optionsEl;
359
+ const elTop = el.offsetTop;
360
+ const elHeight = el.offsetHeight;
361
+ const containerHeight = container.clientHeight;
362
+ const targetScroll = elTop - containerHeight / 2 + elHeight / 2;
363
+ container.scrollTo({ top: targetScroll, behavior: "smooth" });
364
+ }
365
+
366
+ moveActiveIndex(delta: number): void {
367
+ let newIndex = this.activeIndex + delta;
368
+
369
+ if (newIndex < 0) {
370
+ newIndex = this.optionElements.length - 1;
371
+ } else if (newIndex >= this.optionElements.length) {
372
+ newIndex = 0;
373
+ }
374
+
375
+ this.setActiveIndex(newIndex);
376
+ }
377
+
378
+ handleTypeAhead(char: string): void {
379
+ this.typeAheadBuffer += char.toLowerCase();
380
+
381
+ if (this.typeAheadTimeout) {
382
+ clearTimeout(this.typeAheadTimeout);
383
+ }
384
+
385
+ const matchIndex = this.optionElements.findIndex(({ value }) =>
386
+ value.toLowerCase().startsWith(this.typeAheadBuffer),
387
+ );
388
+
389
+ if (matchIndex >= 0) {
390
+ this.setActiveIndex(matchIndex);
391
+ }
392
+
393
+ this.typeAheadTimeout = setTimeout(() => {
394
+ this.typeAheadBuffer = "";
395
+ }, 500);
396
+ }
397
+
398
+ showLoadingState(): void {
399
+ if (!this.optionsEl) return;
400
+ this.optionsEl.innerHTML = "";
401
+ this.optionsEl.setAttribute("aria-busy", "true");
402
+
403
+ const srStatus = document.createElement("div");
404
+ srStatus.setAttribute("data-pf-sr-hidden", "true");
405
+ srStatus.textContent = "Loading filter options...";
406
+ this.optionsEl.appendChild(srStatus);
407
+
408
+ for (let i = 0; i < 3; i++) {
409
+ const skeleton = document.createElement("div");
410
+ skeleton.className = "pf-dropdown-option pf-dropdown-option-loading";
411
+ skeleton.setAttribute("aria-hidden", "true");
412
+
413
+ const checkbox = document.createElement("span");
414
+ checkbox.className = "pf-dropdown-checkbox pf-skeleton";
415
+ skeleton.appendChild(checkbox);
416
+
417
+ const label = document.createElement("span");
418
+ label.className = "pf-dropdown-option-label pf-skeleton";
419
+ label.style.width = `${60 + i * 15}%`;
420
+ label.innerHTML = "&nbsp;";
421
+ skeleton.appendChild(label);
422
+
423
+ this.optionsEl.appendChild(skeleton);
424
+ }
425
+ }
426
+
427
+ updateOptions(): void {
428
+ if (!this.optionsEl) return;
429
+
430
+ this.filtersLoaded = true;
431
+ this.optionsEl.removeAttribute("aria-busy");
432
+ const rawValues = Object.keys(this.totalFilters || {});
433
+ const values = this.sortValues(rawValues);
434
+
435
+ if (rawValues.length === 0) {
436
+ this.optionsEl.innerHTML = "";
437
+ const error = document.createElement("div");
438
+ error.className = "pf-dropdown-error";
439
+ error.setAttribute("role", "alert");
440
+ error.textContent = `No filter "${this.filterName}" found`;
441
+ this.optionsEl.appendChild(error);
442
+ this.optionElements = [];
443
+ return;
444
+ }
445
+
446
+ this.wrapperEl?.removeAttribute("data-pf-hidden");
447
+ this.optionsEl.innerHTML = "";
448
+ this.optionElements = [];
449
+
450
+ const baseId = this.id || this.ensureId("pf-dropdown");
451
+
452
+ values.forEach((value, index) => {
453
+ const availableCount = this.availableFilters?.[value] ?? 0;
454
+ const totalCount = this.totalFilters[value] ?? 0;
455
+ const isSelected = this.selectedValues.has(value);
456
+ const shouldShow = this.showEmpty || availableCount > 0 || isSelected;
457
+
458
+ if (!shouldShow) return;
459
+
460
+ const count = isSelected ? totalCount : availableCount;
461
+ const optionId = `${baseId}-option-${index}`;
462
+
463
+ const option = this.createOption(optionId, value, count, isSelected);
464
+ this.optionsEl!.appendChild(option);
465
+ this.optionElements.push({ el: option, value });
466
+ });
467
+
468
+ // Restore focus state after rebuilding options
469
+ if (this.isOpen && this.optionElements.length > 0) {
470
+ if (this.activeIndex >= this.optionElements.length) {
471
+ this.setActiveIndex(this.optionElements.length - 1);
472
+ } else if (this.activeIndex < 0) {
473
+ this.setActiveIndex(0);
474
+ } else {
475
+ // Re-apply focus to new DOM element at same index
476
+ this.setActiveIndex(this.activeIndex);
477
+ }
478
+ }
479
+
480
+ this.updateBadge();
481
+ }
482
+
483
+ createOption(
484
+ id: string,
485
+ value: string,
486
+ count: number,
487
+ isSelected: boolean,
488
+ ): HTMLElement {
489
+ const option = document.createElement("div");
490
+ option.id = id;
491
+ option.className = "pf-dropdown-option";
492
+ if (this.wrapLabels) option.classList.add("wrap");
493
+ option.setAttribute("role", "option");
494
+ option.setAttribute("aria-selected", String(isSelected));
495
+ option.dataset.value = value;
496
+
497
+ const checkbox = document.createElement("span");
498
+ checkbox.className = "pf-dropdown-checkbox";
499
+ checkbox.setAttribute("aria-hidden", "true");
500
+ option.appendChild(checkbox);
501
+
502
+ const label = document.createElement("span");
503
+ label.className = "pf-dropdown-option-label";
504
+ if (this.wrapLabels) label.classList.add("wrap");
505
+ label.textContent = value;
506
+ option.appendChild(label);
507
+
508
+ // Count is visual only, accessible version in aria-label
509
+ const countSpan = document.createElement("span");
510
+ countSpan.className = "pf-dropdown-option-count";
511
+ countSpan.setAttribute("aria-hidden", "true");
512
+ countSpan.textContent = String(count);
513
+ option.appendChild(countSpan);
514
+
515
+ const resultWord = count === 1 ? "result" : "results";
516
+ option.setAttribute("aria-label", `${value}, ${count} ${resultWord}`);
517
+
518
+ option.addEventListener("click", (e) => {
519
+ e.stopPropagation();
520
+ this.toggleOption(value);
521
+ });
522
+
523
+ return option;
524
+ }
525
+
526
+ toggleOption(value: string): void {
527
+ if (this.singleSelect) {
528
+ if (this.selectedValues.has(value)) {
529
+ this.selectedValues.clear();
530
+ } else {
531
+ this.selectedValues.clear();
532
+ this.selectedValues.add(value);
533
+ }
534
+ this.close();
535
+ } else {
536
+ if (this.selectedValues.has(value)) {
537
+ this.selectedValues.delete(value);
538
+ } else {
539
+ this.selectedValues.add(value);
540
+ }
541
+ }
542
+
543
+ this.updateOptionStates();
544
+ this.updateBadge();
545
+
546
+ this.dispatchFilterChange();
547
+ }
548
+
549
+ clearAll(): void {
550
+ if (this.selectedValues.size === 0) return;
551
+
552
+ this.selectedValues.clear();
553
+ this.updateOptionStates();
554
+ this.updateBadge();
555
+ this.dispatchFilterChange();
556
+ }
557
+
558
+ dispatchFilterChange(): void {
559
+ if (!this.filterName) return;
560
+ const selectedArray = Array.from(this.selectedValues);
561
+
562
+ if (selectedArray.length === 0) {
563
+ this.instance?.triggerFilter(this.filterName, []);
564
+ } else {
565
+ this.instance?.triggerFilter(this.filterName, selectedArray);
566
+ }
567
+ }
568
+
569
+ updateBadge(): void {
570
+ if (!this.badgeEl || !this.triggerEl) return;
571
+ const count = this.selectedValues.size;
572
+ if (count > 0) {
573
+ this.badgeEl.textContent = String(count);
574
+ this.badgeEl.removeAttribute("data-pf-hidden");
575
+
576
+ const label = this.getAttribute("label") || this.filterName || "";
577
+ const filterWord = count === 1 ? "filter" : "filters";
578
+ this.triggerEl.setAttribute(
579
+ "aria-label",
580
+ `${label}, ${count} ${filterWord} selected`,
581
+ );
582
+
583
+ if (this.clearEl) {
584
+ this.clearEl.removeAttribute("aria-disabled");
585
+ }
586
+ } else {
587
+ this.badgeEl.setAttribute("data-pf-hidden", "true");
588
+ this.triggerEl.removeAttribute("aria-label");
589
+
590
+ if (this.clearEl) {
591
+ this.clearEl.setAttribute("aria-disabled", "true");
592
+ }
593
+ }
594
+ }
595
+
596
+ updateOptionStates(): void {
597
+ for (const { el, value } of this.optionElements) {
598
+ const isSelected = this.selectedValues.has(value);
599
+ el.setAttribute("aria-selected", String(isSelected));
600
+ }
601
+ }
602
+
603
+ register(instance: Instance): void {
604
+ if (!this.filterName) return;
605
+
606
+ instance.registerFilter(this);
607
+
608
+ instance.on(
609
+ "filters",
610
+ (filters: unknown) => {
611
+ const f = filters as { available: FilterCounts; total: FilterCounts };
612
+ this.availableFilters = f.available?.[this.filterName!] || {};
613
+ this.totalFilters = f.total?.[this.filterName!] || {};
614
+ if (this.isRendered) {
615
+ this.updateOptions();
616
+ }
617
+ },
618
+ this,
619
+ );
620
+
621
+ instance.on(
622
+ "search",
623
+ (_term: unknown, filters: unknown) => {
624
+ const f = filters as FilterSelection | undefined;
625
+ const externalValues = f?.[this.filterName!] || [];
626
+ this.selectedValues = new Set(externalValues);
627
+ if (this.isRendered) {
628
+ this.updateOptionStates();
629
+ this.updateBadge();
630
+ }
631
+ },
632
+ this,
633
+ );
634
+
635
+ instance.on(
636
+ "error",
637
+ (error: unknown) => {
638
+ const err = error as PagefindError;
639
+ this.showError({
640
+ message: err.message || "Failed to load filters",
641
+ details: err.bundlePath
642
+ ? `Bundle path: ${err.bundlePath}`
643
+ : undefined,
644
+ });
645
+ },
646
+ this,
647
+ );
648
+ }
649
+
650
+ update(): void {
651
+ const newFilterName = this.getAttribute("filter");
652
+ if (newFilterName !== this.filterName) {
653
+ this.filterName = newFilterName;
654
+ this.selectedValues.clear();
655
+ this.updateOptions();
656
+ }
657
+
658
+ this.singleSelect = this.hasAttribute("single-select");
659
+ this.showEmpty = this.hasAttribute("show-empty");
660
+ this.wrapLabels = this.hasAttribute("wrap");
661
+ this.hideClear = this.hasAttribute("hide-clear");
662
+ if (this.hasAttribute("sort")) {
663
+ const sortVal = this.getAttribute("sort") as SortOption;
664
+ if (
665
+ ["default", "alphabetical", "count-desc", "count-asc"].includes(sortVal)
666
+ ) {
667
+ this.sortOption = sortVal;
668
+ }
669
+ } else {
670
+ this.sortOption = "default";
671
+ }
672
+
673
+ if (this.optionsEl) {
674
+ this.optionsEl.setAttribute(
675
+ "aria-multiselectable",
676
+ this.singleSelect ? "false" : "true",
677
+ );
678
+ }
679
+
680
+ const labelSpan = this.triggerEl?.querySelector(
681
+ ".pf-dropdown-trigger-label",
682
+ );
683
+ if (labelSpan) {
684
+ labelSpan.textContent =
685
+ this.getAttribute("label") || this.filterName || "";
686
+ }
687
+
688
+ this.updateOptions();
689
+ }
690
+
691
+ cleanup(): void {
692
+ document.removeEventListener("click", this._handleClickOutside);
693
+ this.instance?.deregisterAllShortcuts(this);
694
+ if (this.typeAheadTimeout) {
695
+ clearTimeout(this.typeAheadTimeout);
696
+ }
697
+ }
698
+ }
699
+
700
+ if (!customElements.get("pagefind-filter-dropdown")) {
701
+ customElements.define("pagefind-filter-dropdown", PagefindFilterDropdown);
702
+ }