diff --git a/changelog.d/1740-layer-finder.md b/changelog.d/1740-layer-finder.md new file mode 100644 index 000000000..6079c6f44 --- /dev/null +++ b/changelog.d/1740-layer-finder.md @@ -0,0 +1,3 @@ +- Add a search and a filter (On / Off and category) to the Data Layers panel, + matching layer names in the UI language, and a section index down its side + that jumps to each group (manjunath22466, samehkhamis, #1740). diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 6b814f4d6..7bc9c7a07 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -723,6 +723,27 @@ and touch screens, while the rails are hidden (clean view, recording), and it follows the rails' sides in a right-to-left language. On a phone, Visual is in the More sheet with the dock's Visual Presets, as before. +## Data Layers finder and section index — November 15, 2026 + +The Data Layers header has a search button and a filter button +(`src/ui/views/panels/LayerFinder.vue`). Search opens a field under the +header that matches each layer's name on screen and its English name; Escape +closes it and leaves the panel open. The filter is a small popover with two +groups, Status (All layers, On, Off: whether the layer is enabled) and +Category (the list's groups, kept by their key, so a language change keeps +the choice). The arrows, Home and End move through it and Escape closes it. +Rows and groups that are left out hide; chips under the header show the +active filters and clear them, and "No layers match" says when nothing is +left. Neither the search nor the filter is saved or put in share links. A +voice request that points at a hidden layer clears the finder first +(`gev:layer-finder-clear`). + +A column of ticks down the list's inline end +(`src/ui/views/panels/LayerSectionIndex.vue`) has one tick per group still +shown; hovering or focusing a tick names its group beside it, on whichever +side has room, and clicking scrolls the list there. The tick for the group in +view is lit. Phones do not show it. + ## Cockpit side panels — November 14, 2026 On the desktop, the cockpit's side panels match the main page's diff --git a/index.html b/index.html index 24bc148b7..2341db1c7 100644 --- a/index.html +++ b/index.html @@ -26,7 +26,7 @@ with the sources: src/materialSymbolsSubset.test.mjs fails when a source names a glyph this list is missing, because a missing glyph does not draw a box — the ligature never forms and the element renders the literal word. --> - +
diff --git a/scripts/format-scope.json b/scripts/format-scope.json index 8026811ea..7f3542bae 100644 --- a/scripts/format-scope.json +++ b/scripts/format-scope.json @@ -1462,5 +1462,8 @@ "src/layers/vessels/placement.test.mjs", "src/ui/styles/visual-panel.css", "src/ui/styles/scope-off-shadows.css", - "src/ui/styles/cockpit-panels.css" + "src/ui/styles/cockpit-panels.css", + "src/ui/styles/layer-section-index.css", + "src/ui/styles/layer-finder.css", + "src/ui/layerFinder.test.mjs" ] diff --git a/scripts/package-boundaries.json b/scripts/package-boundaries.json index a48c717ea..cd6d60a34 100644 --- a/scripts/package-boundaries.json +++ b/scripts/package-boundaries.json @@ -113,6 +113,8 @@ "src/ui/views/panels/DataLibraryCard.vue", "src/ui/views/panels/DataLibraryDialog.vue", "src/ui/views/panels/DataLibraryBrowse.vue", + "src/ui/views/panels/LayerFinder.vue", + "src/ui/views/panels/LayerSectionIndex.vue", "src/ui/libraryBrowser.js", "src/ui/localSdrPresentation.js", "src/ui/unitLabels.js", diff --git a/src/i18n/locales/ar.json b/src/i18n/locales/ar.json index 894076638..5fe57db79 100644 --- a/src/i18n/locales/ar.json +++ b/src/i18n/locales/ar.json @@ -3977,5 +3977,15 @@ "Scope opacity": "عتامة المنظار", "How dark the scope shades the view outside the keyhole": "مدى تعتيم المنظار للعرض خارج الفتحة", "Reset to default": "إعادة التعيين إلى الافتراضي", - "Reset Visual to its defaults: Normal look, HUD, scope, labels and effects": "إعادة المرئيات إلى إعداداتها الافتراضية: المظهر العادي وHUD والمنظار والتسميات والمؤثرات" + "Reset Visual to its defaults: Normal look, HUD, scope, labels and effects": "إعادة المرئيات إلى إعداداتها الافتراضية: المظهر العادي وHUD والمنظار والتسميات والمؤثرات", + "All layers": "كل الطبقات", + "On": "تشغيل", + "Off": "إيقاف", + "Search layers": "البحث في الطبقات", + "Filter layers": "تصفية الطبقات", + "Status": "الحالة", + "Search layers…": "ابحث في الطبقات…", + "Layer groups": "مجموعات الطبقات", + "Jump to {name}": "الانتقال إلى {name}", + "No layers match": "لا توجد طبقات مطابقة" } diff --git a/src/i18n/locales/de.json b/src/i18n/locales/de.json index 408ddbc3a..9964801bb 100644 --- a/src/i18n/locales/de.json +++ b/src/i18n/locales/de.json @@ -3629,5 +3629,15 @@ "Scope opacity": "Scope-Deckkraft", "How dark the scope shades the view outside the keyhole": "Wie stark der Scope die Ansicht außerhalb des Schlüssellochs abdunkelt", "Reset to default": "Auf Standard zurücksetzen", - "Reset Visual to its defaults: Normal look, HUD, scope, labels and effects": "Darstellung auf die Standardwerte zurücksetzen: normaler Look, HUD, Scope, Beschriftungen und Effekte" + "Reset Visual to its defaults: Normal look, HUD, scope, labels and effects": "Darstellung auf die Standardwerte zurücksetzen: normaler Look, HUD, Scope, Beschriftungen und Effekte", + "All layers": "Alle Ebenen", + "On": "An", + "Off": "Aus", + "Search layers": "Ebenen suchen", + "Filter layers": "Ebenen filtern", + "Status": "Status", + "Search layers…": "Ebenen suchen…", + "Layer groups": "Ebenengruppen", + "Jump to {name}": "Zu {name} springen", + "No layers match": "Keine passenden Ebenen" } diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json index a7d626a28..cc72601c9 100644 --- a/src/i18n/locales/es.json +++ b/src/i18n/locales/es.json @@ -3716,5 +3716,15 @@ "Scope opacity": "Opacidad de la mirilla", "How dark the scope shades the view outside the keyhole": "Cuánto oscurece la mirilla la vista fuera del ojo de cerradura", "Reset to default": "Restablecer valores predeterminados", - "Reset Visual to its defaults: Normal look, HUD, scope, labels and effects": "Restablecer Visual a sus valores predeterminados: aspecto Normal, HUD, mirilla, etiquetas y efectos" + "Reset Visual to its defaults: Normal look, HUD, scope, labels and effects": "Restablecer Visual a sus valores predeterminados: aspecto Normal, HUD, mirilla, etiquetas y efectos", + "All layers": "Todas las capas", + "On": "Activadas", + "Off": "Desactivadas", + "Search layers": "Buscar capas", + "Filter layers": "Filtrar capas", + "Status": "Estado", + "Search layers…": "Buscar capas…", + "Layer groups": "Grupos de capas", + "Jump to {name}": "Ir a {name}", + "No layers match": "Ninguna capa coincide" } diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json index d580354a3..0e417867e 100644 --- a/src/i18n/locales/fr.json +++ b/src/i18n/locales/fr.json @@ -3716,5 +3716,15 @@ "Scope opacity": "Opacité de la lunette", "How dark the scope shades the view outside the keyhole": "À quel point la lunette assombrit la vue hors de l'ouverture", "Reset to default": "Rétablir les valeurs par défaut", - "Reset Visual to its defaults: Normal look, HUD, scope, labels and effects": "Rétablir les valeurs par défaut de Visuel : aspect Normal, HUD, lunette, libellés et effets" + "Reset Visual to its defaults: Normal look, HUD, scope, labels and effects": "Rétablir les valeurs par défaut de Visuel : aspect Normal, HUD, lunette, libellés et effets", + "All layers": "Toutes les couches", + "On": "Actives", + "Off": "Inactives", + "Search layers": "Rechercher des couches", + "Filter layers": "Filtrer les couches", + "Status": "État", + "Search layers…": "Rechercher des couches…", + "Layer groups": "Groupes de couches", + "Jump to {name}": "Aller à {name}", + "No layers match": "Aucune couche ne correspond" } diff --git a/src/i18n/locales/he.json b/src/i18n/locales/he.json index 661cf2fef..16dbedb45 100644 --- a/src/i18n/locales/he.json +++ b/src/i18n/locales/he.json @@ -3716,5 +3716,15 @@ "Scope opacity": "אטימות הסקופ", "How dark the scope shades the view outside the keyhole": "עד כמה הסקופ מכהה את התצוגה מחוץ לחור המנעול", "Reset to default": "איפוס לברירת המחדל", - "Reset Visual to its defaults: Normal look, HUD, scope, labels and effects": "איפוס החזותי לברירות המחדל: מראה רגיל, HUD, סקופ, תוויות ואפקטים" + "Reset Visual to its defaults: Normal look, HUD, scope, labels and effects": "איפוס החזותי לברירות המחדל: מראה רגיל, HUD, סקופ, תוויות ואפקטים", + "All layers": "כל השכבות", + "On": "פעילות", + "Off": "כבויות", + "Search layers": "חיפוש שכבות", + "Filter layers": "סינון שכבות", + "Status": "מצב", + "Search layers…": "חיפוש שכבות…", + "Layer groups": "קבוצות שכבות", + "Jump to {name}": "מעבר אל {name}", + "No layers match": "אין שכבות תואמות" } diff --git a/src/i18n/locales/it.json b/src/i18n/locales/it.json index cbd65beb7..e3fe590e5 100644 --- a/src/i18n/locales/it.json +++ b/src/i18n/locales/it.json @@ -3716,5 +3716,15 @@ "Scope opacity": "Opacità dello scope", "How dark the scope shades the view outside the keyhole": "Quanto lo scope scurisce la vista fuori dal foro", "Reset to default": "Ripristina predefiniti", - "Reset Visual to its defaults: Normal look, HUD, scope, labels and effects": "Ripristina Visuale ai valori predefiniti: aspetto Normale, HUD, scope, etichette ed effetti" + "Reset Visual to its defaults: Normal look, HUD, scope, labels and effects": "Ripristina Visuale ai valori predefiniti: aspetto Normale, HUD, scope, etichette ed effetti", + "All layers": "Tutti i livelli", + "On": "Attivi", + "Off": "Disattivati", + "Search layers": "Cerca livelli", + "Filter layers": "Filtra livelli", + "Status": "Stato", + "Search layers…": "Cerca livelli…", + "Layer groups": "Gruppi di livelli", + "Jump to {name}": "Vai a {name}", + "No layers match": "Nessun livello corrisponde" } diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 24fa10f79..0a7d60ba9 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -3542,5 +3542,15 @@ "Scope opacity": "スコープの不透明度", "How dark the scope shades the view outside the keyhole": "キーホール外のビューをスコープがどれだけ暗くするか", "Reset to default": "既定に戻す", - "Reset Visual to its defaults: Normal look, HUD, scope, labels and effects": "ビジュアルを既定に戻す:標準の見た目、HUD、スコープ、ラベル、エフェクト" + "Reset Visual to its defaults: Normal look, HUD, scope, labels and effects": "ビジュアルを既定に戻す:標準の見た目、HUD、スコープ、ラベル、エフェクト", + "All layers": "すべてのレイヤー", + "On": "オン", + "Off": "オフ", + "Search layers": "レイヤーを検索", + "Filter layers": "レイヤーを絞り込む", + "Status": "状態", + "Search layers…": "レイヤーを検索…", + "Layer groups": "レイヤーのグループ", + "Jump to {name}": "{name} へ移動", + "No layers match": "一致するレイヤーはありません" } diff --git a/src/i18n/locales/pt.json b/src/i18n/locales/pt.json index 86b9f3aca..2e3b0d016 100644 --- a/src/i18n/locales/pt.json +++ b/src/i18n/locales/pt.json @@ -3716,5 +3716,15 @@ "Scope opacity": "Opacidade da mira", "How dark the scope shades the view outside the keyhole": "Quanto a mira escurece a vista fora do visor", "Reset to default": "Redefinir para o padrão", - "Reset Visual to its defaults: Normal look, HUD, scope, labels and effects": "Redefinir o Visual para o padrão: aparência Normal, HUD, mira, rótulos e efeitos" + "Reset Visual to its defaults: Normal look, HUD, scope, labels and effects": "Redefinir o Visual para o padrão: aparência Normal, HUD, mira, rótulos e efeitos", + "All layers": "Todas as camadas", + "On": "Ativadas", + "Off": "Desativadas", + "Search layers": "Pesquisar camadas", + "Filter layers": "Filtrar camadas", + "Status": "Estado", + "Search layers…": "Pesquisar camadas…", + "Layer groups": "Grupos de camadas", + "Jump to {name}": "Ir para {name}", + "No layers match": "Nenhuma camada corresponde" } diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json index ac149c01f..5553d847e 100644 --- a/src/i18n/locales/ru.json +++ b/src/i18n/locales/ru.json @@ -3803,5 +3803,15 @@ "Scope opacity": "Непрозрачность прицела", "How dark the scope shades the view outside the keyhole": "Насколько прицел затемняет вид за пределами маски", "Reset to default": "Сбросить по умолчанию", - "Reset Visual to its defaults: Normal look, HUD, scope, labels and effects": "Сбросить «Визуал» к настройкам по умолчанию: обычный вид, HUD, прицел, подписи и эффекты" + "Reset Visual to its defaults: Normal look, HUD, scope, labels and effects": "Сбросить «Визуал» к настройкам по умолчанию: обычный вид, HUD, прицел, подписи и эффекты", + "All layers": "Все слои", + "On": "Включены", + "Off": "Выключены", + "Search layers": "Поиск слоёв", + "Filter layers": "Фильтр слоёв", + "Status": "Состояние", + "Search layers…": "Поиск слоёв…", + "Layer groups": "Группы слоёв", + "Jump to {name}": "Перейти к «{name}»", + "No layers match": "Нет подходящих слоёв" } diff --git a/src/i18n/locales/sv.json b/src/i18n/locales/sv.json index d6acc93a0..05f004f07 100644 --- a/src/i18n/locales/sv.json +++ b/src/i18n/locales/sv.json @@ -3629,5 +3629,15 @@ "Scope opacity": "Scope-opacitet", "How dark the scope shades the view outside the keyhole": "Hur mycket scopet mörkar vyn utanför nyckelhålet", "Reset to default": "Återställ till standard", - "Reset Visual to its defaults: Normal look, HUD, scope, labels and effects": "Återställ Visuellt till standard: Normal stil, HUD, scope, etiketter och effekter" + "Reset Visual to its defaults: Normal look, HUD, scope, labels and effects": "Återställ Visuellt till standard: Normal stil, HUD, scope, etiketter och effekter", + "All layers": "Alla lager", + "On": "På", + "Off": "Av", + "Search layers": "Sök lager", + "Filter layers": "Filtrera lager", + "Status": "Status", + "Search layers…": "Sök lager…", + "Layer groups": "Lagergrupper", + "Jump to {name}": "Gå till {name}", + "No layers match": "Inga lager matchar" } diff --git a/src/i18n/locales/uk.json b/src/i18n/locales/uk.json index 6f49092f4..d841eb0b9 100644 --- a/src/i18n/locales/uk.json +++ b/src/i18n/locales/uk.json @@ -3803,5 +3803,15 @@ "Scope opacity": "Непрозорість прицілу", "How dark the scope shades the view outside the keyhole": "Наскільки приціл затемнює вигляд за межами прицілу", "Reset to default": "Скинути до типових", - "Reset Visual to its defaults: Normal look, HUD, scope, labels and effects": "Скинути «Візуал» до типових налаштувань: звичайний вигляд, HUD, приціл, підписи та ефекти" + "Reset Visual to its defaults: Normal look, HUD, scope, labels and effects": "Скинути «Візуал» до типових налаштувань: звичайний вигляд, HUD, приціл, підписи та ефекти", + "All layers": "Усі шари", + "On": "Увімкнені", + "Off": "Вимкнені", + "Search layers": "Пошук шарів", + "Filter layers": "Фільтр шарів", + "Status": "Стан", + "Search layers…": "Пошук шарів…", + "Layer groups": "Групи шарів", + "Jump to {name}": "Перейти до «{name}»", + "No layers match": "Немає відповідних шарів" } diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index 56a5aa354..26109d574 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -3542,5 +3542,15 @@ "Scope opacity": "视域不透明度", "How dark the scope shades the view outside the keyhole": "视域遮罩使视域之外的画面变暗的程度", "Reset to default": "恢复默认", - "Reset Visual to its defaults: Normal look, HUD, scope, labels and effects": "将视觉恢复为默认设置:常规外观、HUD、视域、标签和效果" + "Reset Visual to its defaults: Normal look, HUD, scope, labels and effects": "将视觉恢复为默认设置:常规外观、HUD、视域、标签和效果", + "All layers": "全部图层", + "On": "已开启", + "Off": "已关闭", + "Search layers": "搜索图层", + "Filter layers": "筛选图层", + "Status": "状态", + "Search layers…": "搜索图层…", + "Layer groups": "图层分组", + "Jump to {name}": "跳转到{name}", + "No layers match": "没有匹配的图层" } diff --git a/src/i18n/locales/zh-Hant.json b/src/i18n/locales/zh-Hant.json index 6f177a2cf..f119fd487 100644 --- a/src/i18n/locales/zh-Hant.json +++ b/src/i18n/locales/zh-Hant.json @@ -3542,5 +3542,15 @@ "Scope opacity": "視域不透明度", "How dark the scope shades the view outside the keyhole": "視域遮罩使視域之外的畫面變暗的程度", "Reset to default": "還原預設", - "Reset Visual to its defaults: Normal look, HUD, scope, labels and effects": "將視覺還原為預設設定:常規外觀、HUD、視域、標籤和效果" + "Reset Visual to its defaults: Normal look, HUD, scope, labels and effects": "將視覺還原為預設設定:常規外觀、HUD、視域、標籤和效果", + "All layers": "全部圖層", + "On": "已開啟", + "Off": "已關閉", + "Search layers": "搜尋圖層", + "Filter layers": "篩選圖層", + "Status": "狀態", + "Search layers…": "搜尋圖層…", + "Layer groups": "圖層分組", + "Jump to {name}": "跳至{name}", + "No layers match": "沒有符合的圖層" } diff --git a/src/i18n/same-as-english.json b/src/i18n/same-as-english.json index b8f50bfe7..1f91cdf68 100644 --- a/src/i18n/same-as-english.json +++ b/src/i18n/same-as-english.json @@ -251,7 +251,8 @@ "Regional", "OPT-IN", "{minutes} min", - "1 min/s" + "1 min/s", + "Status" ], "es": [ "1 min/s", @@ -1275,7 +1276,8 @@ "Period", "ICAO: {id}", "NORAD: {id}", - "MMSI: {id}" + "MMSI: {id}", + "Status" ], "uk": [ "1900–1945", diff --git a/src/ui/cockpitInput.js b/src/ui/cockpitInput.js index 9574068ce..658501195 100644 --- a/src/ui/cockpitInput.js +++ b/src/ui/cockpitInput.js @@ -248,6 +248,9 @@ export function onKeyDown(event) { if (event.target?.closest?.('.cesium-credit-lightbox')) return; // An open Display or Radio popover takes Escape itself. if (this.isUtilityDisclosureOpen?.()) return; + // So do the Data Layers search field and filter menu. + if (event.target?.closest?.('.layer-finder__field, .layer-finder__menu')) + return; if (this.context?.contains(event.target) && !this.contextCollapsed) { event.preventDefault(); event.stopImmediatePropagation(); diff --git a/src/ui/layerFinder.test.mjs b/src/ui/layerFinder.test.mjs new file mode 100644 index 000000000..8cd61ff1a --- /dev/null +++ b/src/ui/layerFinder.test.mjs @@ -0,0 +1,200 @@ +// src/ui/layerFinder.test.mjs — the Data Layers finder and section index work +// on the rows LayerRows renders: search, the On/Off and category filter, +// Escape, the clear request voice sends, and the group ticks. +import '../testSupport/registerDom.mjs'; +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { createApp, h, nextTick } from 'vue'; +import LayerFinder, { + LAYER_FINDER_CLEAR_EVENT, +} from './views/panels/LayerFinder.vue'; +import LayerSectionIndex from './views/panels/LayerSectionIndex.vue'; + +const ROWS = [ + { group: 'movement', heading: 'Movement' }, + { id: 'flights', name: 'Live Flights', english: 'Live Flights', on: true }, + { id: 'ships', name: 'Ships', english: 'Ships', on: false }, + { group: 'weather', heading: 'Weather' }, + { id: 'radar', name: 'Radar météo', english: 'Weather Radar', on: false }, +]; + +const flush = async () => { + await nextTick(); + await new Promise((resolve) => setTimeout(resolve, 0)); + await nextTick(); +}; + +function fixture(t) { + document.body.replaceChildren(); + // Frames run at once, so the observers' batching does not hold a test up. + globalThis.requestAnimationFrame = (fn) => setTimeout(fn, 0); + globalThis.cancelAnimationFrame = (id) => clearTimeout(id); + const panel = document.body.appendChild(document.createElement('section')); + panel.id = 'data-panel'; + const header = panel.appendChild(document.createElement('div')); + const bar = panel.appendChild(document.createElement('div')); + const list = panel.appendChild(document.createElement('div')); + list.id = 'data-toggles'; + for (const row of ROWS) { + if (row.heading) { + const heading = list.appendChild(document.createElement('h3')); + heading.className = 'data-layer-group-heading'; + heading.dataset.group = row.group; + heading.textContent = row.heading; + continue; + } + const el = list.appendChild(document.createElement('div')); + el.className = 'data-toggle-row'; + el.dataset.layerId = row.id; + el.dataset.layerName = row.english; + el.innerHTML = `${row.name}`; + } + const index = panel.appendChild(document.createElement('div')); + const apps = [ + createApp(() => h(LayerFinder, { part: 'buttons' })), + createApp(() => h(LayerFinder, { part: 'bar' })), + createApp(LayerSectionIndex), + ]; + apps[0].mount(header); + apps[1].mount(bar); + apps[2].mount(index); + t.after(async () => { + document.dispatchEvent(new CustomEvent(LAYER_FINDER_CLEAR_EVENT)); + for (const app of apps) app.unmount(); + }); + const shown = () => + [...list.querySelectorAll('.data-toggle-row:not(.is-filtered)')].map( + (row) => row.dataset.layerId, + ); + const search = async (text) => { + if (!document.getElementById('layer-finder-input')) { + document.getElementById('layer-finder-search-btn').click(); + await flush(); + } + const input = document.getElementById('layer-finder-input'); + input.value = text; + input.dispatchEvent(new Event('input', { bubbles: true })); + await flush(); + }; + const openFilter = async () => { + document.querySelector('[aria-controls="layer-finder-menu"]').click(); + await flush(); + return document.getElementById('layer-finder-menu'); + }; + return { list, panel, shown, search, openFilter }; +} + +test('search finds a layer by the name on screen or its English name', async (t) => { + const { shown, search, list } = fixture(t); + await search('météo'); + assert.deepEqual(shown(), ['radar']); + await search('weather radar'); + assert.deepEqual(shown(), ['radar']); + // The Movement heading has no rows left, so it is hidden too. + assert.equal( + list + .querySelector('[data-group="movement"]') + .classList.contains('is-filtered'), + true, + ); + await search('nothing like it'); + assert.deepEqual(shown(), []); + assert.match( + document.querySelector('.layer-finder__empty')?.textContent || '', + /No layers match/, + ); +}); + +test('On and Off follow whether the layer is enabled', async (t) => { + const { shown, openFilter } = fixture(t); + const menu = await openFilter(); + const on = [...menu.querySelectorAll('button')].find((b) => + b.textContent.trim().endsWith('On'), + ); + on.click(); + await flush(); + assert.deepEqual(shown(), ['flights']); + assert.equal(on.getAttribute('aria-pressed'), 'true'); +}); + +test('a chosen category survives its heading being translated', async (t) => { + const { shown, openFilter, list } = fixture(t); + const menu = await openFilter(); + [...menu.querySelectorAll('button')] + .find((b) => b.textContent.trim().endsWith('Weather')) + .click(); + await flush(); + assert.deepEqual(shown(), ['radar']); + list.querySelector('[data-group="weather"]').textContent = 'Météo'; + await flush(); + assert.deepEqual(shown(), ['radar']); + assert.match( + document.querySelector('.layer-finder__chip').textContent, + /Météo/, + ); +}); + +test('Escape in the search field closes it without reaching the panel', async (t) => { + const { search, panel } = fixture(t); + let reached = false; + panel.addEventListener('keydown', () => (reached = true)); + await search('ships'); + document + .getElementById('layer-finder-input') + .dispatchEvent( + new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }), + ); + await flush(); + assert.equal(reached, false); + assert.equal(document.getElementById('layer-finder-input'), null); + assert.equal(document.activeElement?.id, 'layer-finder-search-btn'); +}); + +test('the filter menu takes focus, moves with the arrows and closes on Escape', async (t) => { + const { openFilter } = fixture(t); + const menu = await openFilter(); + const buttons = [...menu.querySelectorAll('button')]; + assert.equal(document.activeElement, buttons[0]); + menu.dispatchEvent( + new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }), + ); + assert.equal(document.activeElement, buttons[1]); + menu.dispatchEvent( + new KeyboardEvent('keydown', { key: 'End', bubbles: true }), + ); + assert.equal(document.activeElement, buttons.at(-1)); + menu.dispatchEvent( + new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }), + ); + await flush(); + assert.equal(document.getElementById('layer-finder-menu'), null); + assert.equal( + document.activeElement, + document.querySelector('[aria-controls="layer-finder-menu"]'), + ); +}); + +test('a clear request shows every row again', async (t) => { + const { shown, search } = fixture(t); + await search('ships'); + assert.deepEqual(shown(), ['ships']); + document.dispatchEvent(new CustomEvent(LAYER_FINDER_CLEAR_EVENT)); + await flush(); + assert.deepEqual(shown(), ['flights', 'ships', 'radar']); +}); + +test('the section index has a tick per group and drops filtered groups', async (t) => { + const { search } = fixture(t); + await flush(); + assert.equal( + document.querySelectorAll('.layer-section-index__tick').length, + 2, + ); + await search('ships'); + await flush(); + // One group left: no index. + assert.equal( + document.querySelectorAll('.layer-section-index__tick').length, + 0, + ); +}); diff --git a/src/ui/layerPanel/LayerRows.vue b/src/ui/layerPanel/LayerRows.vue index ee0490913..9358ad2b8 100644 --- a/src/ui/layerPanel/LayerRows.vue +++ b/src/ui/layerPanel/LayerRows.vue @@ -22,9 +22,11 @@ defineProps({