@pagefind/component-ui 1.5.0-alpha.3 → 1.5.0-beta.1
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.
- package/components/pagefind-filter-dropdown.ts +23 -12
- package/components/pagefind-input.ts +16 -2
- package/components/pagefind-modal-trigger.ts +6 -0
- package/components/pagefind-modal.ts +0 -1
- package/components/pagefind-results.ts +17 -5
- package/components/pagefind-searchbox.ts +25 -12
- package/css/pagefind-component-ui.css +11 -0
- package/npm_dist/cjs/component-ui.cjs +617 -538
- package/npm_dist/mjs/component-ui.mjs +617 -538
- package/package.json +1 -1
|
@@ -113,6 +113,7 @@ export class PagefindFilterDropdown extends PagefindElement {
|
|
|
113
113
|
this.triggerEl.id = triggerId;
|
|
114
114
|
this.triggerEl.className = "pf-dropdown-trigger";
|
|
115
115
|
if (this.wrapLabels) this.triggerEl.classList.add("wrap");
|
|
116
|
+
this.triggerEl.setAttribute("role", "combobox");
|
|
116
117
|
this.triggerEl.setAttribute("aria-haspopup", "listbox");
|
|
117
118
|
this.triggerEl.setAttribute("aria-expanded", "false");
|
|
118
119
|
this.triggerEl.setAttribute("aria-controls", menuId);
|
|
@@ -151,7 +152,6 @@ export class PagefindFilterDropdown extends PagefindElement {
|
|
|
151
152
|
this.singleSelect ? "false" : "true",
|
|
152
153
|
);
|
|
153
154
|
this.optionsEl.setAttribute("aria-labelledby", triggerId);
|
|
154
|
-
this.optionsEl.setAttribute("tabindex", "-1");
|
|
155
155
|
this.menuEl.appendChild(this.optionsEl);
|
|
156
156
|
|
|
157
157
|
this.wrapperEl.appendChild(this.menuEl);
|
|
@@ -180,12 +180,13 @@ export class PagefindFilterDropdown extends PagefindElement {
|
|
|
180
180
|
this.triggerEl.addEventListener("focus", () =>
|
|
181
181
|
this.instance?.triggerLoad(),
|
|
182
182
|
);
|
|
183
|
-
this.triggerEl.addEventListener("keydown", (e) =>
|
|
184
|
-
this.
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
183
|
+
this.triggerEl.addEventListener("keydown", (e) => {
|
|
184
|
+
if (this.isOpen) {
|
|
185
|
+
this.handleMenuKeydown(e);
|
|
186
|
+
} else {
|
|
187
|
+
this.handleTriggerKeydown(e);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
189
190
|
|
|
190
191
|
this.isRendered = true;
|
|
191
192
|
}
|
|
@@ -214,10 +215,11 @@ export class PagefindFilterDropdown extends PagefindElement {
|
|
|
214
215
|
this.triggerEl.setAttribute("aria-expanded", "true");
|
|
215
216
|
this.triggerEl.classList.add("open");
|
|
216
217
|
|
|
217
|
-
|
|
218
|
-
|
|
218
|
+
// Always apply visual focus when opening if there are options
|
|
219
|
+
if (this.optionElements.length > 0) {
|
|
220
|
+
const targetIndex = this.activeIndex >= 0 ? this.activeIndex : 0;
|
|
221
|
+
this.setActiveIndex(targetIndex);
|
|
219
222
|
}
|
|
220
|
-
this.optionsEl.focus();
|
|
221
223
|
|
|
222
224
|
const navigateText =
|
|
223
225
|
this.instance?.translate("keyboard_navigate") || "navigate";
|
|
@@ -250,7 +252,7 @@ export class PagefindFilterDropdown extends PagefindElement {
|
|
|
250
252
|
this.triggerEl.setAttribute("aria-expanded", "false");
|
|
251
253
|
this.triggerEl.classList.remove("open");
|
|
252
254
|
|
|
253
|
-
this.
|
|
255
|
+
this.triggerEl?.removeAttribute("aria-activedescendant");
|
|
254
256
|
|
|
255
257
|
for (const { el } of this.optionElements) {
|
|
256
258
|
el.classList.remove("pf-dropdown-option-focused");
|
|
@@ -348,7 +350,7 @@ export class PagefindFilterDropdown extends PagefindElement {
|
|
|
348
350
|
const option = this.optionElements[index];
|
|
349
351
|
|
|
350
352
|
option.el.classList.add("pf-dropdown-option-focused");
|
|
351
|
-
this.
|
|
353
|
+
this.triggerEl?.setAttribute("aria-activedescendant", option.el.id);
|
|
352
354
|
|
|
353
355
|
this.scrollToCenter(option.el);
|
|
354
356
|
}
|
|
@@ -524,6 +526,8 @@ export class PagefindFilterDropdown extends PagefindElement {
|
|
|
524
526
|
}
|
|
525
527
|
|
|
526
528
|
toggleOption(value: string): void {
|
|
529
|
+
const wasSelected = this.selectedValues.has(value);
|
|
530
|
+
|
|
527
531
|
if (this.singleSelect) {
|
|
528
532
|
if (this.selectedValues.has(value)) {
|
|
529
533
|
this.selectedValues.clear();
|
|
@@ -540,6 +544,13 @@ export class PagefindFilterDropdown extends PagefindElement {
|
|
|
540
544
|
}
|
|
541
545
|
}
|
|
542
546
|
|
|
547
|
+
// Announce selection change for screen readers
|
|
548
|
+
const isNowSelected = this.selectedValues.has(value);
|
|
549
|
+
if (isNowSelected !== wasSelected) {
|
|
550
|
+
const action = isNowSelected ? "selected" : "deselected";
|
|
551
|
+
this.instance?.announceRaw(`${value} ${action}`);
|
|
552
|
+
}
|
|
553
|
+
|
|
543
554
|
this.updateOptionStates();
|
|
544
555
|
this.updateBadge();
|
|
545
556
|
|
|
@@ -57,11 +57,10 @@ export class PagefindInput extends PagefindElement {
|
|
|
57
57
|
this.removeAttribute("dir");
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
const wrapper = document.createElement("
|
|
60
|
+
const wrapper = document.createElement("search");
|
|
61
61
|
wrapper.className = "pf-input-wrapper";
|
|
62
62
|
wrapper.setAttribute("role", "search");
|
|
63
63
|
wrapper.setAttribute("aria-label", searchLabel);
|
|
64
|
-
wrapper.setAttribute("action", "javascript:void(0);");
|
|
65
64
|
|
|
66
65
|
const label = document.createElement("label");
|
|
67
66
|
label.setAttribute("for", inputId);
|
|
@@ -72,16 +71,31 @@ export class PagefindInput extends PagefindElement {
|
|
|
72
71
|
this.inputEl = document.createElement("input");
|
|
73
72
|
this.inputEl.id = inputId;
|
|
74
73
|
this.inputEl.className = "pf-input";
|
|
74
|
+
this.inputEl.setAttribute("type", "search");
|
|
75
|
+
this.inputEl.setAttribute("autocomplete", "off");
|
|
75
76
|
this.inputEl.setAttribute("autocapitalize", "none");
|
|
76
77
|
this.inputEl.setAttribute("enterkeyhint", "search");
|
|
77
78
|
this.inputEl.setAttribute("placeholder", placeholderText);
|
|
78
79
|
if (this.autofocus) {
|
|
79
80
|
this.inputEl.setAttribute("autofocus", "autofocus");
|
|
80
81
|
}
|
|
82
|
+
|
|
83
|
+
const hintId = this.instance!.generateId("pf-input-hint");
|
|
84
|
+
const hintText =
|
|
85
|
+
this.instance?.translate("input_hint") ||
|
|
86
|
+
"Results will appear as you type";
|
|
87
|
+
const hint = document.createElement("span");
|
|
88
|
+
hint.id = hintId;
|
|
89
|
+
hint.setAttribute("data-pf-sr-hidden", "true");
|
|
90
|
+
hint.textContent = hintText;
|
|
91
|
+
this.inputEl.setAttribute("aria-describedby", hintId);
|
|
92
|
+
|
|
81
93
|
wrapper.appendChild(this.inputEl);
|
|
94
|
+
wrapper.appendChild(hint);
|
|
82
95
|
|
|
83
96
|
this.clearEl = document.createElement("button");
|
|
84
97
|
this.clearEl.className = "pf-input-clear";
|
|
98
|
+
this.clearEl.setAttribute("type", "button");
|
|
85
99
|
this.clearEl.setAttribute("data-pf-suppressed", "true");
|
|
86
100
|
this.clearEl.textContent = clearText;
|
|
87
101
|
wrapper.appendChild(this.clearEl);
|
|
@@ -84,6 +84,12 @@ export class PagefindModalTrigger extends PagefindElement {
|
|
|
84
84
|
this.buttonEl.setAttribute("aria-haspopup", "dialog");
|
|
85
85
|
this.buttonEl.setAttribute("aria-expanded", "false");
|
|
86
86
|
this.buttonEl.setAttribute("aria-label", this.placeholder || "Search");
|
|
87
|
+
this.buttonEl.setAttribute(
|
|
88
|
+
"aria-keyshortcuts",
|
|
89
|
+
this.isMac
|
|
90
|
+
? `Meta+${this.shortcut.toUpperCase()}`
|
|
91
|
+
: `Control+${this.shortcut.toUpperCase()}`,
|
|
92
|
+
);
|
|
87
93
|
|
|
88
94
|
const icon = document.createElement("span");
|
|
89
95
|
icon.className = "pf-trigger-icon";
|
|
@@ -46,7 +46,6 @@ export class PagefindModal extends PagefindElement {
|
|
|
46
46
|
this.dialogEl = document.createElement("dialog");
|
|
47
47
|
this.dialogEl.className = "pf-modal";
|
|
48
48
|
this.dialogEl.id = dialogId;
|
|
49
|
-
this.dialogEl.setAttribute("aria-modal", "true");
|
|
50
49
|
this.dialogEl.setAttribute("aria-label", searchLabel);
|
|
51
50
|
|
|
52
51
|
if (hasChildren && children) {
|
|
@@ -68,7 +68,7 @@ const DEFAULT_RESULT_TEMPLATE = `<li class="pf-result">
|
|
|
68
68
|
{{/if}}
|
|
69
69
|
</li>`;
|
|
70
70
|
|
|
71
|
-
const DEFAULT_PLACEHOLDER_TEMPLATE = `<li class="pf-result">
|
|
71
|
+
const DEFAULT_PLACEHOLDER_TEMPLATE = `<li class="pf-result" aria-hidden="true">
|
|
72
72
|
<div class="pf-result-card">
|
|
73
73
|
<div class="pf-skeleton pf-skeleton-image"></div>
|
|
74
74
|
<div class="pf-result-content">
|
|
@@ -393,6 +393,15 @@ export class PagefindResults extends PagefindElement {
|
|
|
393
393
|
: "many_results";
|
|
394
394
|
const priority = count === 0 ? "assertive" : "polite";
|
|
395
395
|
instance.announce(key, { SEARCH_TERM: term, COUNT: count }, priority);
|
|
396
|
+
} else if (instance.faceted) {
|
|
397
|
+
const key =
|
|
398
|
+
count === 0
|
|
399
|
+
? "total_zero_results"
|
|
400
|
+
: count === 1
|
|
401
|
+
? "total_one_result"
|
|
402
|
+
: "total_many_results";
|
|
403
|
+
const priority = count === 0 ? "assertive" : "polite";
|
|
404
|
+
instance.announce(key, { COUNT: count }, priority);
|
|
396
405
|
}
|
|
397
406
|
|
|
398
407
|
const resultRenderer = this.getResultRenderer();
|
|
@@ -485,14 +494,14 @@ export class PagefindResults extends PagefindElement {
|
|
|
485
494
|
if (index < anchors.length - 1) {
|
|
486
495
|
const next = anchors[index + 1];
|
|
487
496
|
next.focus();
|
|
488
|
-
this.scrollToCenter(next);
|
|
497
|
+
this.scrollToCenter(next, e.repeat);
|
|
489
498
|
}
|
|
490
499
|
} else if (e.key === "ArrowUp") {
|
|
491
500
|
e.preventDefault();
|
|
492
501
|
if (index > 0) {
|
|
493
502
|
const prev = anchors[index - 1];
|
|
494
503
|
prev.focus();
|
|
495
|
-
this.scrollToCenter(prev);
|
|
504
|
+
this.scrollToCenter(prev, e.repeat);
|
|
496
505
|
} else {
|
|
497
506
|
// At first anchor, go back to input
|
|
498
507
|
this.instance?.focusPreviousInput(document.activeElement as Element);
|
|
@@ -549,7 +558,7 @@ export class PagefindResults extends PagefindElement {
|
|
|
549
558
|
});
|
|
550
559
|
}
|
|
551
560
|
|
|
552
|
-
private scrollToCenter(el: HTMLElement): void {
|
|
561
|
+
private scrollToCenter(el: HTMLElement, instant: boolean = false): void {
|
|
553
562
|
const container = this.intersectionEl || nearestScrollParent(el);
|
|
554
563
|
if (!container || !(container instanceof HTMLElement)) return;
|
|
555
564
|
if (container === document.body || container === document.documentElement)
|
|
@@ -560,7 +569,10 @@ export class PagefindResults extends PagefindElement {
|
|
|
560
569
|
const elRelativeTop = elRect.top - containerRect.top + container.scrollTop;
|
|
561
570
|
const targetScroll =
|
|
562
571
|
elRelativeTop - container.clientHeight / 2 + el.offsetHeight / 2;
|
|
563
|
-
container.scrollTo({
|
|
572
|
+
container.scrollTo({
|
|
573
|
+
top: targetScroll,
|
|
574
|
+
behavior: instant ? "instant" : "smooth",
|
|
575
|
+
});
|
|
564
576
|
}
|
|
565
577
|
|
|
566
578
|
clearSelection(): void {
|
|
@@ -174,7 +174,8 @@ export class PagefindSearchbox extends PagefindElement {
|
|
|
174
174
|
containerEl: HTMLElement | null = null;
|
|
175
175
|
inputEl: HTMLInputElement | null = null;
|
|
176
176
|
dropdownEl: HTMLElement | null = null;
|
|
177
|
-
resultsEl:
|
|
177
|
+
resultsEl: HTMLElement | null = null;
|
|
178
|
+
statusEl: HTMLElement | null = null;
|
|
178
179
|
footerEl: HTMLElement | null = null;
|
|
179
180
|
|
|
180
181
|
isOpen: boolean = false;
|
|
@@ -303,13 +304,18 @@ export class PagefindSearchbox extends PagefindElement {
|
|
|
303
304
|
this.removeAttribute("dir");
|
|
304
305
|
}
|
|
305
306
|
|
|
306
|
-
this.resultsEl = document.createElement("
|
|
307
|
+
this.resultsEl = document.createElement("div");
|
|
307
308
|
this.resultsEl.id = resultsId;
|
|
308
309
|
this.resultsEl.className = "pf-searchbox-results";
|
|
309
310
|
this.resultsEl.setAttribute("role", "listbox");
|
|
310
311
|
this.resultsEl.setAttribute("aria-label", resultsLabel);
|
|
311
312
|
this.dropdownEl.appendChild(this.resultsEl);
|
|
312
313
|
|
|
314
|
+
this.statusEl = document.createElement("div");
|
|
315
|
+
this.statusEl.className = "pf-searchbox-status";
|
|
316
|
+
this.statusEl.hidden = true;
|
|
317
|
+
this.dropdownEl.appendChild(this.statusEl);
|
|
318
|
+
|
|
313
319
|
if (this.showKeyboardHints) {
|
|
314
320
|
this.footerEl = document.createElement("div");
|
|
315
321
|
this.footerEl.className = "pf-searchbox-footer";
|
|
@@ -491,33 +497,33 @@ export class PagefindSearchbox extends PagefindElement {
|
|
|
491
497
|
}
|
|
492
498
|
|
|
493
499
|
private showLoadingState(): void {
|
|
494
|
-
if (!this.resultsEl) return;
|
|
500
|
+
if (!this.resultsEl || !this.statusEl) return;
|
|
495
501
|
this.isLoading = true;
|
|
496
502
|
this.resultsEl.innerHTML = "";
|
|
503
|
+
this.resultsEl.setAttribute("aria-busy", "true");
|
|
497
504
|
|
|
498
505
|
const searchingText =
|
|
499
506
|
this.instance?.translate("searching", { SEARCH_TERM: this.searchTerm }) ||
|
|
500
507
|
"Searching...";
|
|
501
508
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
this.resultsEl.appendChild(loadingEl);
|
|
509
|
+
this.statusEl.textContent = searchingText;
|
|
510
|
+
this.statusEl.className = "pf-searchbox-status pf-searchbox-loading";
|
|
511
|
+
this.statusEl.hidden = false;
|
|
506
512
|
}
|
|
507
513
|
|
|
508
514
|
private showEmptyState(): void {
|
|
509
|
-
if (!this.resultsEl) return;
|
|
515
|
+
if (!this.resultsEl || !this.statusEl) return;
|
|
510
516
|
this.resultsEl.innerHTML = "";
|
|
517
|
+
this.resultsEl.removeAttribute("aria-busy");
|
|
511
518
|
|
|
512
519
|
const noResultsText =
|
|
513
520
|
this.instance?.translate("zero_results", {
|
|
514
521
|
SEARCH_TERM: this.searchTerm,
|
|
515
522
|
}) || `No results for "${this.searchTerm}"`;
|
|
516
523
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
this.resultsEl.appendChild(emptyEl);
|
|
524
|
+
this.statusEl.textContent = noResultsText;
|
|
525
|
+
this.statusEl.className = "pf-searchbox-status pf-searchbox-empty";
|
|
526
|
+
this.statusEl.hidden = false;
|
|
521
527
|
|
|
522
528
|
this.instance?.announce(
|
|
523
529
|
"zero_results",
|
|
@@ -613,6 +619,13 @@ export class PagefindSearchbox extends PagefindElement {
|
|
|
613
619
|
private handleResults(searchResult: PagefindSearchResult): void {
|
|
614
620
|
this.isLoading = false;
|
|
615
621
|
|
|
622
|
+
if (this.resultsEl) {
|
|
623
|
+
this.resultsEl.removeAttribute("aria-busy");
|
|
624
|
+
}
|
|
625
|
+
if (this.statusEl) {
|
|
626
|
+
this.statusEl.hidden = true;
|
|
627
|
+
}
|
|
628
|
+
|
|
616
629
|
for (const result of this.results) {
|
|
617
630
|
result.cleanup();
|
|
618
631
|
}
|
|
@@ -290,6 +290,16 @@ pagefind-modal-trigger {
|
|
|
290
290
|
color: var(--pf-text-muted);
|
|
291
291
|
}
|
|
292
292
|
|
|
293
|
+
/* Hide native search clear button - we have our own */
|
|
294
|
+
:is(*, #\#):is(*, #\#):is(*, #\#) .pf-input::-webkit-search-decoration,
|
|
295
|
+
:is(*, #\#):is(*, #\#):is(*, #\#) .pf-input::-webkit-search-cancel-button,
|
|
296
|
+
:is(*, #\#):is(*, #\#):is(*, #\#) .pf-input::-webkit-search-results-button,
|
|
297
|
+
:is(*, #\#):is(*, #\#):is(*, #\#) .pf-input::-webkit-search-results-decoration {
|
|
298
|
+
display: none;
|
|
299
|
+
appearance: none;
|
|
300
|
+
-webkit-appearance: none;
|
|
301
|
+
}
|
|
302
|
+
|
|
293
303
|
:is(*, #\#):is(*, #\#):is(*, #\#) .pf-input-clear {
|
|
294
304
|
position: absolute;
|
|
295
305
|
right: 2px;
|
|
@@ -1242,6 +1252,7 @@ pagefind-modal-trigger {
|
|
|
1242
1252
|
:is(*, #\#):is(*, #\#):is(*, #\#) pagefind-keyboard-hints,
|
|
1243
1253
|
:is(*, #\#):is(*, #\#):is(*, #\#) .pf-keyboard-hints {
|
|
1244
1254
|
display: flex;
|
|
1255
|
+
flex-wrap: wrap;
|
|
1245
1256
|
align-items: center;
|
|
1246
1257
|
gap: 16px;
|
|
1247
1258
|
font-size: 12px;
|