From d6bfc28c45d6eadab96ce50488c6fdb4482e2c56 Mon Sep 17 00:00:00 2001
From: igdmdimitrov
Date: Fri, 26 Jun 2026 18:07:41 +0300
Subject: [PATCH 1/5] chore(*): ux improvements on advanced search when
fetching files is taking longer
---
.../pages/components/search-advanced.astro | 35 ++++
.../SearchAdvanced/SearchAdvanced.astro | 153 ++++++++++++++++--
.../SearchAdvanced/SearchAdvanced.scss | 42 ++++-
3 files changed, 220 insertions(+), 10 deletions(-)
diff --git a/playground/src/pages/components/search-advanced.astro b/playground/src/pages/components/search-advanced.astro
index 470b886..6b83d0e 100644
--- a/playground/src/pages/components/search-advanced.astro
+++ b/playground/src/pages/components/search-advanced.astro
@@ -13,6 +13,7 @@ const headings = [
{ depth: 2, slug: 'index-format', text: 'Index format' },
{ depth: 2, slug: 'tabs', text: 'Tabs' },
{ depth: 2, slug: 'scope-selector', text: 'Scope selector' },
+ { depth: 2, slug: 'two-phase-loading', text: 'Two-phase loading' },
{ depth: 2, slug: 'keyboard', text: 'Keyboard shortcut' },
{ depth: 2, slug: 'usage', text: 'Usage' },
];
@@ -82,6 +83,40 @@ const headings = [
"Current package" option is hidden automatically.
+ Two-phase loading
+
+ When the scope selector is set to All packages and the index is a
+ manifest listing many per-package/version files, the component fetches them in two
+ sequential phases to keep the search feel responsive:
+
+
+ -
+ Phase 1 — only the highest-version file for each package is fetched
+ in parallel. As soon as all phase-1 files arrive the first results are rendered and
+ the query is ready to use.
+
+ -
+ Phase 2 — the remaining (older-version) files are fetched in parallel
+ in the background. The UI stays in a pending state: results are visually
+ greyed out, tab interactions are disabled, and a centered Searching… overlay
+ is displayed over the tabs. A 1 000 ms interval re-renders results as files accumulate,
+ so the counts update progressively rather than jumping at the end.
+
+
+
+ Once every file has loaded the pending state is cleared, interactions are restored, and
+ the final result set (powered by pickHighestVersionWinners()) is shown.
+ Subsequent searches against the same scope reuse the already-fetched index — no
+ additional network requests are made.
+
+
+ The two-phase path is triggered automatically whenever
+ latestPagesPromise is non-null and
+ allIndexLoadingComplete is false at the time a search
+ runs. For single-file indexes or the current-package scope the standard single-phase
+ path is used instead.
+
+
Keyboard shortcut
The trigger button supports Ctrl K
diff --git a/src/components/SearchAdvanced/SearchAdvanced.astro b/src/components/SearchAdvanced/SearchAdvanced.astro
index 594dc2a..4b7cce0 100644
--- a/src/components/SearchAdvanced/SearchAdvanced.astro
+++ b/src/components/SearchAdvanced/SearchAdvanced.astro
@@ -277,6 +277,13 @@ const resolvedIndexUrl = indexUrl ?? `${basePath}api-search-index/manifest.json`
private allTabLimit: number = 10;
private showScope: boolean = false;
private kindCodeToCategory: Map = new Map();
+ // Two-phase all-index loading: latestPagesPromise resolves with only the
+ // highest-version file per package (fast first result), allIndexPromise
+ // resolves when every version file is loaded (complete result).
+ private latestPagesPromise: Promise | null = null;
+ private allIndexLoadingComplete = false;
+ private allPagesAccumulator: IndexedPage[] = [];
+ private updateInterval: ReturnType | null = null;
connectedCallback() {
this.basePath = this.dataset.base || '/';
@@ -506,6 +513,7 @@ const resolvedIndexUrl = indexUrl ?? `${basePath}api-search-index/manifest.json`
}
private closeModal() {
+ this.clearUpdateInterval();
this.dialog.hide();
}
@@ -531,24 +539,42 @@ const resolvedIndexUrl = indexUrl ?? `${basePath}api-search-index/manifest.json`
}
private async fetchIndex(): Promise {
+ // Create latestPagesPromise SYNCHRONOUSLY before the first await so that
+ // runSearch() can detect two-phase loading is in progress the moment
+ // fetchIndex() is called (e.g. immediately after scope changes to 'all').
+ let latestResolve!: (pages: IndexedPage[]) => void;
+ if (!this.latestPagesPromise) {
+ this.latestPagesPromise = new Promise(res => { latestResolve = res; });
+ }
+
let data: unknown;
try {
const r = await fetch(this.indexUrl);
- if (!r.ok) return [];
+ if (!r.ok) { latestResolve([]); return []; }
data = await r.json();
} catch {
+ latestResolve([]);
return [];
}
// Single-file format: { pages: [...] }
if (Array.isArray((data as any)?.pages)) {
- return (data as any).pages as IndexedPage[];
+ this.allIndexLoadingComplete = true;
+ const pages = (data as any).pages as IndexedPage[];
+ latestResolve(pages);
+ return pages;
}
// Manifest format: { files: ["pkg/version.json", ...] }
if (Array.isArray((data as any)?.files)) {
const baseUrl = this.indexUrl.replace(/[^/]*$/, '');
- const fetches: Promise[] = ((data as any).files as string[]).map(async (file: string) => {
+ const files: string[] = (data as any).files as string[];
+ this.allPagesAccumulator = [];
+
+ const latestFileSet = new Set(pickLatestFiles(files));
+ const otherFiles = files.filter(f => !latestFileSet.has(f));
+
+ const fetchFile = async (file: string): Promise => {
try {
const res = await fetch(`${baseUrl}${file}`);
if (!res.ok) return [];
@@ -557,11 +583,27 @@ const resolvedIndexUrl = indexUrl ?? `${basePath}api-search-index/manifest.json`
} catch {
return [];
}
- });
- const results = await Promise.all(fetches);
- return results.flat();
+ };
+
+ // Phase 1: fetch latest-version files in parallel, await all before
+ // starting phase 2. latestPagesPromise resolves when this completes.
+ const phase1 = await Promise.all(Array.from(latestFileSet).map(fetchFile));
+ const latestPages = phase1.flat();
+ this.allPagesAccumulator.push(...latestPages);
+ latestResolve(latestPages);
+
+ // Phase 2: fetch remaining files in parallel; accumulate pages as each
+ // arrives so the 1000ms interval in runSearch can re-render partial results.
+ await Promise.all(otherFiles.map(async (file) => {
+ const pages = await fetchFile(file);
+ this.allPagesAccumulator.push(...pages);
+ }));
+
+ this.allIndexLoadingComplete = true;
+ return this.allPagesAccumulator;
}
+ latestResolve([]);
return [];
}
@@ -596,16 +638,73 @@ const resolvedIndexUrl = indexUrl ?? `${basePath}api-search-index/manifest.json`
return;
}
- this.statusEl.textContent = 'Searching\u2026';
+ // Two-phase search for "all packages" scope when the full index is not
+ // yet loaded: show latest-version results immediately (greyed out /
+ // non-interactive), then update every 1000ms as phase 2 files arrive.
+ const useLatestFirst =
+ this.scope === 'all' &&
+ this.latestPagesPromise !== null &&
+ !this.allIndexLoadingComplete;
+
+ if (useLatestFirst) {
+ // Immediately activate the pending overlay so the CSS ::after "Searching…"
+ // text appears centered over the igc-tabs area rather than in the status
+ // element above the tabs. renderTabs(true) will maintain this once phase
+ // 1 results are ready.
+ this.statusEl.textContent = '';
+ this.tabsEl.toggleAttribute('data-search-pending', true);
+ const contentElEarly = this.tabsEl.closest('.igd-search-content');
+ if (contentElEarly) contentElEarly.toggleAttribute('data-search-pending', true);
+ } else {
+ this.statusEl.textContent = 'Searching\u2026';
+ }
+
+ if (useLatestFirst) {
+ let latestPages: IndexedPage[];
+ try {
+ latestPages = await this.latestPagesPromise!;
+ } catch {
+ latestPages = [];
+ }
+ if (q !== this.currentQuery) return;
+
+ if (latestPages.length > 0) {
+ const buckets = this.classify(latestPages, q);
+ this.currentMatches = buckets;
+ if (this.currentTab !== 'all' && (buckets.get(this.currentTab)?.length ?? 0) === 0) {
+ this.currentTab = 'all';
+ }
+ this.renderTabs(true);
+ this.renderResults();
+ }
+
+ // Update results every 1000ms while phase 2 files are still arriving.
+ this.clearUpdateInterval();
+ this.updateInterval = setInterval(() => {
+ if (q !== this.currentQuery) { this.clearUpdateInterval(); return; }
+ if (this.allPagesAccumulator.length === 0) return;
+ const buckets = this.classify(this.allPagesAccumulator, q);
+ this.currentMatches = buckets;
+ if (this.currentTab !== 'all' && (buckets.get(this.currentTab)?.length ?? 0) === 0) {
+ this.currentTab = 'all';
+ }
+ // Keep pending=true while still loading; the final render below clears it.
+ this.renderTabs(!this.allIndexLoadingComplete);
+ this.renderResults();
+ if (this.allIndexLoadingComplete) this.clearUpdateInterval();
+ }, 1000);
+ }
let pages: IndexedPage[];
try {
pages = await this.loadIndex();
} catch (err) {
+ this.clearUpdateInterval();
this.statusEl.textContent = `Search unavailable: ${(err as Error).message}`;
return;
}
+ this.clearUpdateInterval();
if (q !== this.currentQuery) return;
const buckets = this.classify(pages, q);
@@ -613,17 +712,27 @@ const resolvedIndexUrl = indexUrl ?? `${basePath}api-search-index/manifest.json`
if (this.currentTab !== 'all' && (buckets.get(this.currentTab)?.length ?? 0) === 0) {
this.currentTab = 'all';
}
- this.renderTabs();
+ this.renderTabs(false);
this.renderResults();
}
+ private clearUpdateInterval() {
+ if (this.updateInterval !== null) {
+ clearInterval(this.updateInterval);
+ this.updateInterval = null;
+ }
+ }
+
private clearResults() {
+ this.clearUpdateInterval();
this.statusEl.textContent = '';
this.tabsEl.hidden = true;
this.tabsEl.innerHTML = '';
this.flatListEl.hidden = true;
this.flatListEl.innerHTML = '';
this.currentMatches = new Map();
+ const contentEl = this.tabsEl.closest('.igd-search-content');
+ if (contentEl) contentEl.removeAttribute('data-search-pending');
}
private async runBrowse() {
@@ -845,7 +954,7 @@ const resolvedIndexUrl = indexUrl ?? `${basePath}api-search-index/manifest.json`
return buckets;
}
- private renderTabs() {
+ private renderTabs(pending = false) {
const totalCount = Array.from(this.currentMatches.values()).reduce((s, l) => s + l.length, 0);
// No-categories fallback: hide tabs, render a plain list
@@ -915,6 +1024,12 @@ const resolvedIndexUrl = indexUrl ?? `${basePath}api-search-index/manifest.json`
this.tabsEl.innerHTML = tabsHtml.join('');
this.tabsEl.hidden = false;
+ this.tabsEl.toggleAttribute('data-search-pending', pending);
+ const contentEl = this.tabsEl.closest('.igd-search-content');
+ if (contentEl) contentEl.toggleAttribute('data-search-pending', pending);
+ // Hide the status bar while pending — the CSS overlay on the content
+ // wrapper shows "Searching…" over the greyed results instead.
+ if (pending) this.statusEl.textContent = '';
// Wire "Show more" buttons — select the target tab (its content is already rendered)
this.tabsEl.querySelectorAll('.igd-search-more-btn').forEach((btn) => {
@@ -991,6 +1106,26 @@ const resolvedIndexUrl = indexUrl ?? `${basePath}api-search-index/manifest.json`
return Array.from(winners.values());
}
+ /**
+ * From a manifest file list ("pkg/version.json" entries), return only the
+ * highest-version file per package. These are the files fetched in the fast
+ * first phase of the two-phase all-packages search.
+ */
+ function pickLatestFiles(files: string[]): string[] {
+ const best = new Map();
+ for (const file of files) {
+ const slash = file.indexOf('/');
+ if (slash < 0) continue;
+ const pkg = file.slice(0, slash);
+ const version = file.slice(slash + 1).replace(/\.json$/, '');
+ const existing = best.get(pkg);
+ if (!existing || compareVersions(version, existing.version) > 0) {
+ best.set(pkg, { version, file });
+ }
+ }
+ return Array.from(best.values()).map(e => e.file);
+ }
+
function highlight(text: string, q: string): string {
if (!q) return escapeHtml(text);
const i = text.toLowerCase().indexOf(q);
diff --git a/src/components/SearchAdvanced/SearchAdvanced.scss b/src/components/SearchAdvanced/SearchAdvanced.scss
index b59035a..a928288 100644
--- a/src/components/SearchAdvanced/SearchAdvanced.scss
+++ b/src/components/SearchAdvanced/SearchAdvanced.scss
@@ -154,5 +154,45 @@
letter-spacing: 0;
}
}
-}
+ // Pending state: results from latest-version-only first phase are shown
+ // greyed out and non-interactive while remaining version files load.
+ .igd-search-content {
+ position: relative;
+
+ &[data-search-pending]::after {
+ content: 'Searching\2026';
+ position: absolute;
+ inset: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: light-dark(var(--ig-gray-600), var(--ig-gray-300));
+ font-size: #{rem(14px)};
+ font-weight: 500;
+ pointer-events: none;
+ z-index: 1;
+ }
+ }
+
+ .igd-search-tabs[data-search-pending] {
+ pointer-events: none;
+
+ &::part(selected-indicator),
+ &::part(start-scroll-button),
+ &::part(end-scroll-button) {
+ opacity: 0.25;
+ }
+
+ igc-tab::part(content),
+ igc-tab::part(panel) {
+ opacity: 0.25;
+ }
+
+ .igd-search-results,
+ .igd-search-category-header,
+ .igd-search-tab-count {
+ opacity: 0.25;
+ }
+ }
+}
From 8cc98be4dd357f7df9ccfa9c262c87e4d6f1931d Mon Sep 17 00:00:00 2001
From: igdmdimitrov <49060557+igdmdimitrov@users.noreply.github.com>
Date: Fri, 26 Jun 2026 18:38:18 +0300
Subject: [PATCH 2/5] Apply suggestions from code review
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
---
src/components/SearchAdvanced/SearchAdvanced.astro | 13 +++++++++----
src/components/SearchAdvanced/SearchAdvanced.scss | 14 ++++++++++++++
2 files changed, 23 insertions(+), 4 deletions(-)
diff --git a/src/components/SearchAdvanced/SearchAdvanced.astro b/src/components/SearchAdvanced/SearchAdvanced.astro
index 4b7cce0..e8e177e 100644
--- a/src/components/SearchAdvanced/SearchAdvanced.astro
+++ b/src/components/SearchAdvanced/SearchAdvanced.astro
@@ -651,7 +651,7 @@ const resolvedIndexUrl = indexUrl ?? `${basePath}api-search-index/manifest.json`
// text appears centered over the igc-tabs area rather than in the status
// element above the tabs. renderTabs(true) will maintain this once phase
// 1 results are ready.
- this.statusEl.textContent = '';
+ this.statusEl.textContent = 'Searching\u2026';
this.tabsEl.toggleAttribute('data-search-pending', true);
const contentElEarly = this.tabsEl.closest('.igd-search-content');
if (contentElEarly) contentElEarly.toggleAttribute('data-search-pending', true);
@@ -693,6 +693,7 @@ const resolvedIndexUrl = indexUrl ?? `${basePath}api-search-index/manifest.json`
this.renderResults();
if (this.allIndexLoadingComplete) this.clearUpdateInterval();
}, 1000);
+ this.dialog.addEventListener('igcClosed', () => this.clearUpdateInterval(), { once: true });
}
let pages: IndexedPage[];
@@ -700,6 +701,9 @@ const resolvedIndexUrl = indexUrl ?? `${basePath}api-search-index/manifest.json`
pages = await this.loadIndex();
} catch (err) {
this.clearUpdateInterval();
+ this.tabsEl.toggleAttribute('data-search-pending', false);
+ const contentEl = this.tabsEl.closest('.igd-search-content');
+ if (contentEl) contentEl.removeAttribute('data-search-pending');
this.statusEl.textContent = `Search unavailable: ${(err as Error).message}`;
return;
}
@@ -731,6 +735,7 @@ const resolvedIndexUrl = indexUrl ?? `${basePath}api-search-index/manifest.json`
this.flatListEl.hidden = true;
this.flatListEl.innerHTML = '';
this.currentMatches = new Map();
+ this.tabsEl.removeAttribute('data-search-pending');
const contentEl = this.tabsEl.closest('.igd-search-content');
if (contentEl) contentEl.removeAttribute('data-search-pending');
}
@@ -1027,9 +1032,9 @@ const resolvedIndexUrl = indexUrl ?? `${basePath}api-search-index/manifest.json`
this.tabsEl.toggleAttribute('data-search-pending', pending);
const contentEl = this.tabsEl.closest('.igd-search-content');
if (contentEl) contentEl.toggleAttribute('data-search-pending', pending);
- // Hide the status bar while pending — the CSS overlay on the content
- // wrapper shows "Searching…" over the greyed results instead.
- if (pending) this.statusEl.textContent = '';
+ // Keep the aria-live status text during pending so screen readers still
+ // get a "Searching…" announcement; visually hide it via CSS if desired.
+ if (pending) this.statusEl.textContent = 'Searching\u2026';
// Wire "Show more" buttons — select the target tab (its content is already rendered)
this.tabsEl.querySelectorAll('.igd-search-more-btn').forEach((btn) => {
diff --git a/src/components/SearchAdvanced/SearchAdvanced.scss b/src/components/SearchAdvanced/SearchAdvanced.scss
index a928288..c5695d5 100644
--- a/src/components/SearchAdvanced/SearchAdvanced.scss
+++ b/src/components/SearchAdvanced/SearchAdvanced.scss
@@ -173,6 +173,20 @@
pointer-events: none;
z-index: 1;
}
+
+ // Keep the aria-live status element in the accessibility tree while pending,
+ // but visually hide it so the overlay is the only visible "Searching…".
+ &[data-search-pending] .igd-search-status {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border: 0;
+ }
}
.igd-search-tabs[data-search-pending] {
From db9632a3036f01800e40c52b04c4f00dba71dd20 Mon Sep 17 00:00:00 2001
From: igdmdimitrov
Date: Fri, 26 Jun 2026 18:41:33 +0300
Subject: [PATCH 3/5] chore(*): clear data-search-pending attribute
---
src/components/SearchAdvanced/SearchAdvanced.astro | 3 +++
1 file changed, 3 insertions(+)
diff --git a/src/components/SearchAdvanced/SearchAdvanced.astro b/src/components/SearchAdvanced/SearchAdvanced.astro
index e8e177e..226d09d 100644
--- a/src/components/SearchAdvanced/SearchAdvanced.astro
+++ b/src/components/SearchAdvanced/SearchAdvanced.astro
@@ -1024,6 +1024,9 @@ const resolvedIndexUrl = indexUrl ?? `${basePath}api-search-index/manifest.json`
if (totalCount === 0) {
this.tabsEl.hidden = true;
this.tabsEl.innerHTML = '';
+ this.tabsEl.removeAttribute('data-search-pending');
+ const contentEl = this.tabsEl.closest('.igd-search-content');
+ if (contentEl) contentEl.removeAttribute('data-search-pending');
return;
}
From 7fba08e56f70ce2f9d55cbd42597b79a28cab4f3 Mon Sep 17 00:00:00 2001
From: igdmdimitrov <49060557+igdmdimitrov@users.noreply.github.com>
Date: Mon, 29 Jun 2026 16:14:05 +0300
Subject: [PATCH 4/5] Apply suggestions from code review
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
---
.../SearchAdvanced/SearchAdvanced.astro | 16 ++++++++++++----
1 file changed, 12 insertions(+), 4 deletions(-)
diff --git a/src/components/SearchAdvanced/SearchAdvanced.astro b/src/components/SearchAdvanced/SearchAdvanced.astro
index 226d09d..9a8d099 100644
--- a/src/components/SearchAdvanced/SearchAdvanced.astro
+++ b/src/components/SearchAdvanced/SearchAdvanced.astro
@@ -550,9 +550,10 @@ const resolvedIndexUrl = indexUrl ?? `${basePath}api-search-index/manifest.json`
let data: unknown;
try {
const r = await fetch(this.indexUrl);
- if (!r.ok) { latestResolve([]); return []; }
+ if (!r.ok) { this.allIndexLoadingComplete = true; latestResolve([]); return []; }
data = await r.json();
} catch {
+ this.allIndexLoadingComplete = true;
latestResolve([]);
return [];
}
@@ -603,6 +604,7 @@ const resolvedIndexUrl = indexUrl ?? `${basePath}api-search-index/manifest.json`
return this.allPagesAccumulator;
}
+ this.allIndexLoadingComplete = true;
latestResolve([]);
return [];
}
@@ -680,18 +682,23 @@ const resolvedIndexUrl = indexUrl ?? `${basePath}api-search-index/manifest.json`
// Update results every 1000ms while phase 2 files are still arriving.
this.clearUpdateInterval();
+ let lastLen = 0;
this.updateInterval = setInterval(() => {
if (q !== this.currentQuery) { this.clearUpdateInterval(); return; }
- if (this.allPagesAccumulator.length === 0) return;
+ const len = this.allPagesAccumulator.length;
+ const complete = this.allIndexLoadingComplete;
+ if (len === 0) { if (complete) this.clearUpdateInterval(); return; }
+ if (len === lastLen && !complete) return;
+ lastLen = len;
const buckets = this.classify(this.allPagesAccumulator, q);
this.currentMatches = buckets;
if (this.currentTab !== 'all' && (buckets.get(this.currentTab)?.length ?? 0) === 0) {
this.currentTab = 'all';
}
// Keep pending=true while still loading; the final render below clears it.
- this.renderTabs(!this.allIndexLoadingComplete);
+ this.renderTabs(!complete);
this.renderResults();
- if (this.allIndexLoadingComplete) this.clearUpdateInterval();
+ if (complete) this.clearUpdateInterval();
}, 1000);
this.dialog.addEventListener('igcClosed', () => this.clearUpdateInterval(), { once: true });
}
@@ -1033,6 +1040,7 @@ const resolvedIndexUrl = indexUrl ?? `${basePath}api-search-index/manifest.json`
this.tabsEl.innerHTML = tabsHtml.join('');
this.tabsEl.hidden = false;
this.tabsEl.toggleAttribute('data-search-pending', pending);
+ this.tabsEl.toggleAttribute('inert', pending);
const contentEl = this.tabsEl.closest('.igd-search-content');
if (contentEl) contentEl.toggleAttribute('data-search-pending', pending);
// Keep the aria-live status text during pending so screen readers still
From 0bb731b411fddd0f2587ebfc06241a23712dc868 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 29 Jun 2026 13:15:52 +0000
Subject: [PATCH 5/5] Fix pending state not cleared on short query (< 3 chars)
---
src/components/SearchAdvanced/SearchAdvanced.astro | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/src/components/SearchAdvanced/SearchAdvanced.astro b/src/components/SearchAdvanced/SearchAdvanced.astro
index 9a8d099..d435b0a 100644
--- a/src/components/SearchAdvanced/SearchAdvanced.astro
+++ b/src/components/SearchAdvanced/SearchAdvanced.astro
@@ -632,6 +632,10 @@ const resolvedIndexUrl = indexUrl ?? `${basePath}api-search-index/manifest.json`
// Avoid searching on very short queries — a single or two-character query matches
// virtually everything and causes the classify + render path to freeze the main thread.
if (q.length < 3) {
+ this.clearUpdateInterval();
+ this.tabsEl.removeAttribute('data-search-pending');
+ const contentElShort = this.tabsEl.closest('.igd-search-content');
+ if (contentElShort) contentElShort.removeAttribute('data-search-pending');
this.statusEl.textContent = 'Type at least 3 characters to search\u2026';
this.tabsEl.hidden = true;
this.tabsEl.innerHTML = '';