From c6f738609f819bfc98b3c20a7878a499550b1c2e Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Fri, 22 Dec 2023 18:50:22 +0100 Subject: [PATCH] A11y: improve documentation of hotkeys, keyboard navigation acts more like an aria-grid --- langs/en.json | 1 + public/css/index-tailwind-output.css | 10 +++ src/Models/ThemeViewState.ts | 2 +- src/UI/Base/Hotkeys.ts | 91 ++++++++++++-------- src/UI/BigComponents/ReverseGeocoding.svelte | 5 +- src/UI/Map/MapLibreAdaptor.ts | 14 ++- src/UI/ThemeViewGUI.svelte | 5 +- 7 files changed, 85 insertions(+), 43 deletions(-) diff --git a/langs/en.json b/langs/en.json index a58063266..f5ba8ef11 100644 --- a/langs/en.json +++ b/langs/en.json @@ -477,6 +477,7 @@ "selectMapnik": "Set the background layer to OpenStreetMap-carto", "selectOsmbasedmap": "Set the background layer to on OpenStreetMap-based map (or disable the background raster layer)", "selectSearch": "Select the search bar to search locations", + "shakePhone": "Shaking your phone", "title": "Hotkeys" }, "image": { diff --git a/public/css/index-tailwind-output.css b/public/css/index-tailwind-output.css index 11a428aa6..0f157c68c 100644 --- a/public/css/index-tailwind-output.css +++ b/public/css/index-tailwind-output.css @@ -1431,6 +1431,11 @@ video { row-gap: 0.25rem; } +.gap-x-4 { + -webkit-column-gap: 1rem; + column-gap: 1rem; +} + .gap-x-0\.5 { -webkit-column-gap: 0.125rem; column-gap: 0.125rem; @@ -1690,6 +1695,11 @@ video { border-color: rgb(219 234 254 / var(--tw-border-opacity)); } +.border-red-500 { + --tw-border-opacity: 1; + border-color: rgb(239 68 68 / var(--tw-border-opacity)); +} + .border-gray-300 { --tw-border-opacity: 1; border-color: rgb(209 213 219 / var(--tw-border-opacity)); diff --git a/src/Models/ThemeViewState.ts b/src/Models/ThemeViewState.ts index 4f2c2ad66..5db88f912 100644 --- a/src/Models/ThemeViewState.ts +++ b/src/Models/ThemeViewState.ts @@ -83,7 +83,7 @@ export default class ThemeViewState implements SpecialVisualizationState { readonly osmConnection: OsmConnection readonly selectedElement: UIEventSource readonly selectedElementAndLayer: Store<{ feature: Feature; layer: LayerConfig }> - readonly mapProperties: MapProperties & ExportableMap + readonly mapProperties: MapLibreAdaptor & MapProperties & ExportableMap readonly osmObjectDownloader: OsmObjectDownloader readonly dataIsLoading: Store diff --git a/src/UI/Base/Hotkeys.ts b/src/UI/Base/Hotkeys.ts index ec4068e1b..e00f9a09f 100644 --- a/src/UI/Base/Hotkeys.ts +++ b/src/UI/Base/Hotkeys.ts @@ -10,17 +10,13 @@ import { FixedUiElement } from "./FixedUiElement" import Translations from "../i18n/Translations" export default class Hotkeys { - private static readonly _docs: UIEventSource< + public static readonly _docs: UIEventSource< { key: { ctrl?: string; shift?: string; alt?: string; nomod?: string; onUp?: boolean } documentation: string | Translation + alsoTriggeredBy: Translation[] }[] - > = new UIEventSource< - { - key: { ctrl?: string; shift?: string; alt?: string; nomod?: string; onUp?: boolean } - documentation: string | Translation - }[] - >([]) + > = new UIEventSource([]) /** * Register a hotkey @@ -48,7 +44,7 @@ export default class Hotkeys { }, documentation: string | Translation, action: () => void | false, - alsoTriggeredOn?: Translation[] + alsoTriggeredBy?: Translation[] ) { const type = key["onUp"] ? "keyup" : "keypress" let keycode: string = key["ctrl"] ?? key["shift"] ?? key["alt"] ?? key["nomod"] @@ -59,7 +55,7 @@ export default class Hotkeys { } } - this._docs.data.push({ key, documentation }) + this._docs.data.push({ key, documentation, alsoTriggeredBy }) this._docs.ping() if (Utils.runningFromConsole) { return @@ -109,37 +105,56 @@ export default class Hotkeys { } static generateDocumentation(): BaseUIElement { - let byKey: [string, string | Translation][] = Hotkeys._docs.data - .map(({ key, documentation }) => { - const modifiers = Object.keys(key).filter((k) => k !== "nomod" && k !== "onUp") - let keycode: string = key["ctrl"] ?? key["shift"] ?? key["alt"] ?? key["nomod"] - if (keycode.length == 1) { - keycode = keycode.toUpperCase() + return new VariableUiElement( + Hotkeys._docs.mapD((docs) => { + let byKey: [string, string | Translation, Translation[] | undefined][] = docs + .map(({ key, documentation, alsoTriggeredBy }) => { + const modifiers = Object.keys(key).filter( + (k) => k !== "nomod" && k !== "onUp" + ) + let keycode: string = + key["ctrl"] ?? key["shift"] ?? key["alt"] ?? key["nomod"] + if (keycode.length == 1) { + keycode = keycode.toUpperCase() + } + if (keycode === " ") { + keycode = "Spacebar" + } + modifiers.push(keycode) + return <[string, string | Translation, Translation[] | undefined]>[ + modifiers.join("+"), + documentation, + alsoTriggeredBy, + ] + }) + .sort() + byKey = Utils.NoNull(byKey) + for (let i = byKey.length - 1; i > 0; i--) { + if (byKey[i - 1][0] === byKey[i][0]) { + byKey.splice(i, 1) + } } - if (keycode === " ") { - keycode = "Spacebar" - } - modifiers.push(keycode) - return <[string, string | Translation]>[modifiers.join("+"), documentation] + const t = Translations.t.hotkeyDocumentation + return new Combine([ + new Title(t.title, 1), + t.intro, + new Table( + [t.key, t.action], + byKey.map(([key, doc, alsoTriggeredBy]) => { + let keyEl: BaseUIElement = new FixedUiElement(key).SetClass( + "literal-code w-fit h-fit" + ) + if (alsoTriggeredBy?.length > 0) { + keyEl = new Combine([keyEl, ...alsoTriggeredBy]).SetClass( + "flex gap-x-4 items-center" + ) + } + return [keyEl, doc] + }) + ), + ]) }) - .sort() - byKey = Utils.NoNull(byKey) - for (let i = byKey.length - 1; i > 0; i--) { - if (byKey[i - 1][0] === byKey[i][0]) { - byKey.splice(i, 1) - } - } - const t = Translations.t.hotkeyDocumentation - return new Combine([ - new Title(t.title, 1), - t.intro, - new Table( - [t.key, t.action], - byKey.map(([key, doc]) => { - return [new FixedUiElement(key).SetClass("literal-code"), doc] - }) - ), - ]) + ) } static generateDocumentationDynamic(): BaseUIElement { diff --git a/src/UI/BigComponents/ReverseGeocoding.svelte b/src/UI/BigComponents/ReverseGeocoding.svelte index 98a902c65..77877bdc2 100644 --- a/src/UI/BigComponents/ReverseGeocoding.svelte +++ b/src/UI/BigComponents/ReverseGeocoding.svelte @@ -44,7 +44,10 @@ Translations.t.hotkeyDocumentation.queryCurrentLocation, () => { displayLocation() - } + }, + [ + Translations.t.hotkeyDocumentation.shakePhone + ] ) Motion.singleton.startListening() diff --git a/src/UI/Map/MapLibreAdaptor.ts b/src/UI/Map/MapLibreAdaptor.ts index 44c7a74f2..c97dd5d9f 100644 --- a/src/UI/Map/MapLibreAdaptor.ts +++ b/src/UI/Map/MapLibreAdaptor.ts @@ -511,7 +511,19 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap { await Utils.waitFor(250) } } - + public installCustomKeyboardHandler(viewport: Store) { + viewport.mapD( + (viewport) => { + const map = this._maplibreMap.data + if (!map) { + return + } + const oldKeyboard = map.keyboard + oldKeyboard._panStep = viewport.getBoundingClientRect().width + }, + [this._maplibreMap] + ) + } private removeCurrentLayer(map: MLMap): void { if (this._currentRasterLayer) { // hide the previous layer diff --git a/src/UI/ThemeViewGUI.svelte b/src/UI/ThemeViewGUI.svelte index d255a41ea..6f8436044 100644 --- a/src/UI/ThemeViewGUI.svelte +++ b/src/UI/ThemeViewGUI.svelte @@ -66,6 +66,7 @@ import FilterPanel from "./BigComponents/FilterPanel.svelte" import PrivacyPolicy from "./BigComponents/PrivacyPolicy.svelte" import { BBox } from "../Logic/BBox" + import { MapLibreAdaptor } from "./Map/MapLibreAdaptor.js" export let state: ThemeViewState let layout = state.layout @@ -100,7 +101,7 @@ let visualFeedback = state.visualFeedback let viewport: UIEventSource = new UIEventSource(undefined) let mapproperties: MapProperties = state.mapProperties - + state.mapProperties.installCustomKeyboardHandler(viewport) function updateViewport() { const rect = viewport.data?.getBoundingClientRect() if (!rect) { @@ -159,7 +160,7 @@
-
+
{/if}