From af4d9bb2bf5aadabf4358e091649947416c08f45 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Fri, 15 Dec 2023 01:46:01 +0100 Subject: [PATCH] A11y: screen navigation improvements, see #1181 --- langs/en.json | 16 ++ langs/layers/en.json | 12 ++ public/css/index-tailwind-output.css | 8 +- src/Logic/State/UserRelatedState.ts | 2 +- src/Models/MapProperties.ts | 14 +- src/Models/ThemeViewState.ts | 66 +++--- src/UI/Base/FloatOver.svelte | 8 +- src/UI/Base/Hotkeys.ts | 54 +++-- src/UI/Base/LoginToggle.svelte | 12 +- src/UI/Base/ModalRight.svelte | 45 ++-- src/UI/Base/TabbedGroup.svelte | 2 +- src/UI/BigComponents/Geosearch.svelte | 5 +- .../BigComponents/SelectedElementView.svelte | 2 +- src/UI/BigComponents/Summary.svelte | 15 +- .../BigComponents/VisualFeedbackPanel.svelte | 41 ++++ src/UI/Image/LinkableImage.svelte | 46 ++-- src/UI/Image/NearbyImagesCollapsed.svelte | 5 +- src/UI/InputElement/Helpers/SlopeInput.svelte | 1 - src/UI/Map/MapLibreAdaptor.ts | 79 +++++-- src/UI/Map/MaplibreMap.svelte | 8 +- .../TagRendering/TagRenderingEditable.svelte | 92 ++++---- .../TagRendering/TagRenderingQuestion.svelte | 201 +++++++++--------- src/UI/ThemeViewGUI.svelte | 64 +++--- src/index.css | 4 - theme.html | 6 +- 25 files changed, 483 insertions(+), 325 deletions(-) create mode 100644 src/UI/BigComponents/VisualFeedbackPanel.svelte diff --git a/langs/en.json b/langs/en.json index df4b0575d..2a7634cfd 100644 --- a/langs/en.json +++ b/langs/en.json @@ -336,6 +336,7 @@ "searchShort": "Search…", "searching": "Searching…" }, + "searchAnswer": "Search an option…", "share": "Share", "sharescreen": { "copiedToClipboard": "Link copied to clipboard", @@ -382,6 +383,20 @@ "uploadingChanges": "Uploading changes…", "useSearch": "Use the search above to see presets", "useSearchForMore": "Use the search function to search within {total} more values…", + "visualFeedback": { + "closestFeaturesAre": "Closest features are:", + "east": "Moving east", + "in": "Zooming in", + "islocked": "View locked to your GPS-location, moving disabled. Press the geolocation button to unlock.", + "locked": "View is now locked to your GPS-location, moving disabled.", + "navigation": "Use arrow keys to move the map, press space to select the closest feature", + "noCloseFeatures": "No features in view", + "north": "Moving north", + "out": "Zooming out", + "south": "Moving south", + "unlocked": "Moving enabled.", + "west": "Moving west" + }, "waitingForGeopermission": "Waiting for your permission to use the geolocation…", "waitingForLocation": "Searching your current location…", "weekdays": { @@ -449,6 +464,7 @@ "dontDelete": "Cancel", "isDeleted": "Deleted", "nearby": { + "close": "Collapse panel with nearby images", "link": "This picture shows the object", "noNearbyImages": "No nearby images were found", "seeNearby": "Browse and link nearby pictures", diff --git a/langs/layers/en.json b/langs/layers/en.json index 2c24b45eb..64b16a457 100644 --- a/langs/layers/en.json +++ b/langs/layers/en.json @@ -3400,6 +3400,18 @@ "question": "How wide is the gap between the cycleway and the road?", "render": "The buffer besides this cycleway is {cycleway:buffer} m" }, + "incline": { + "mappings": { + "0": { + "then": "There is (probably) no incline here" + }, + "1": { + "then": "This road has a slope" + } + }, + "question": "Does {title()} have an incline?", + "render": "This road has an slope of {incline}" + }, "is lit?": { "mappings": { "0": { diff --git a/public/css/index-tailwind-output.css b/public/css/index-tailwind-output.css index ee3b5cd6f..1e3f381c1 100644 --- a/public/css/index-tailwind-output.css +++ b/public/css/index-tailwind-output.css @@ -1342,6 +1342,10 @@ video { resize: both; } +.list-none { + list-style-type: none; +} + .appearance-none { -webkit-appearance: none; appearance: none; @@ -2229,10 +2233,6 @@ body { font-family: "Helvetica Neue", Arial, sans-serif; } -.focusable { - /* Not a 'real' class, but rather an indication to FloatOver and ModalRight to, when they open, grab the focus */ -} - svg, img { box-sizing: content-box; diff --git a/src/Logic/State/UserRelatedState.ts b/src/Logic/State/UserRelatedState.ts index 618ca31af..5fe99c68c 100644 --- a/src/Logic/State/UserRelatedState.ts +++ b/src/Logic/State/UserRelatedState.ts @@ -267,7 +267,7 @@ export default class UserRelatedState { /** * Initialize the 'amended preferences'. - * This is inherently a dirty and chaotic method, as it shoves many properties into this EventSourcd + * This is inherently a dirty and chaotic method, as it shoves many properties into this EventSource * */ private initAmendedPrefs( layout?: LayoutConfig, diff --git a/src/Models/MapProperties.ts b/src/Models/MapProperties.ts index 6137b22a7..8e905155a 100644 --- a/src/Models/MapProperties.ts +++ b/src/Models/MapProperties.ts @@ -1,7 +1,11 @@ import { Store, UIEventSource } from "../Logic/UIEventSource" import { BBox } from "../Logic/BBox" import { RasterLayerPolygon } from "./RasterLayers" - +import { B } from "vitest/dist/types-aac763a5" +export interface KeyNavigationEvent { + date: Date + key: "north" | "east" | "south" | "west" | "in" | "out" | "islocked" | "locked" | "unlocked" +} export interface MapProperties { readonly location: UIEventSource<{ lon: number; lat: number }> readonly zoom: UIEventSource @@ -14,7 +18,13 @@ export interface MapProperties { readonly allowRotating: UIEventSource readonly lastClickLocation: Store<{ lon: number; lat: number }> readonly allowZooming: UIEventSource - readonly lastKeyNavigation: UIEventSource + + /** + * Triggered when the user navigated by using the keyboard. + * The callback might return 'true' if it wants to be unregistered + * @param f + */ + onKeyNavigationEvent(f: (event: KeyNavigationEvent) => void | boolean): () => void } export interface ExportableMap { diff --git a/src/Models/ThemeViewState.ts b/src/Models/ThemeViewState.ts index 1513e3a39..9fdf64618 100644 --- a/src/Models/ThemeViewState.ts +++ b/src/Models/ThemeViewState.ts @@ -128,6 +128,11 @@ export default class ThemeViewState implements SpecialVisualizationState { * All 'level'-tags that are available with the current features */ readonly floors: Store + /** + * If true, the user interface will toggle some extra aids for people using screenreaders and keyboard navigation + * Triggered by navigating the map with arrows or by pressing 'space' or 'enter' + */ + public readonly visualFeedback: UIEventSource = new UIEventSource(false) private readonly newPointDialog: FilteredLayer constructor(layout: LayoutConfig) { @@ -372,6 +377,7 @@ export default class ThemeViewState implements SpecialVisualizationState { public focusOnMap() { if (this.map.data) { this.map.data.getCanvas().focus() + console.log("Focused on map") return } this.map.addCallbackAndRunD((map) => { @@ -437,6 +443,13 @@ export default class ThemeViewState implements SpecialVisualizationState { * Various small methods that need to be called */ private miscSetup() { + this.mapProperties.onKeyNavigationEvent((keyEvent) => { + if (["north", "east", "south", "west"].indexOf(keyEvent.key) >= 0) { + this.visualFeedback.setData(true) + return true // Our job is done, unregister + } + }) + this.userRelatedState.markLayoutAsVisited(this.layout) this.selectedElement.addCallbackAndRunD((feature) => { @@ -460,7 +473,7 @@ export default class ThemeViewState implements SpecialVisualizationState { * @private */ private selectClosestAtCenter(i: number = 0) { - this.mapProperties.lastKeyNavigation.setData(Date.now() / 1000) + this.visualFeedback.setData(true) const toSelect = this.closestFeatures.features.data[i] if (!toSelect) { return @@ -495,35 +508,30 @@ export default class ThemeViewState implements SpecialVisualizationState { } ) - this.mapProperties.lastKeyNavigation.addCallbackAndRunD((_) => { - Hotkeys.RegisterHotkey( - { - nomod: " ", - onUp: true, - }, - Translations.t.hotkeyDocumentation.selectItem, - () => this.selectClosestAtCenter(0) - ) - Hotkeys.RegisterHotkey( - { - nomod: "Spacebar", - onUp: true, - }, - Translations.t.hotkeyDocumentation.selectItem, - () => this.selectClosestAtCenter(0) - ) - for (let i = 1; i < 9; i++) { - Hotkeys.RegisterHotkey( - { - nomod: "" + i, - onUp: true, - }, - Translations.t.hotkeyDocumentation.selectItem, - () => this.selectClosestAtCenter(i - 1) - ) + Hotkeys.RegisterHotkey( + { + nomod: " ", + onUp: true, + }, + Translations.t.hotkeyDocumentation.selectItem, + () => { + if (this.selectedElement.data !== undefined) { + return false + } + this.selectClosestAtCenter(0) } - return true // unregister - }) + ) + + for (let i = 1; i < 9; i++) { + Hotkeys.RegisterHotkey( + { + nomod: "" + i, + onUp: true, + }, + Translations.t.hotkeyDocumentation.selectItem, + () => this.selectClosestAtCenter(i - 1) + ) + } this.featureSwitches.featureSwitchBackgroundSelection.addCallbackAndRun((enable) => { if (!enable) { diff --git a/src/UI/Base/FloatOver.svelte b/src/UI/Base/FloatOver.svelte index 0ad3a8bc6..94b5e0159 100644 --- a/src/UI/Base/FloatOver.svelte +++ b/src/UI/Base/FloatOver.svelte @@ -11,12 +11,6 @@ export let extraClasses = "p-4 md:p-6"; - let mainContent: HTMLElement; - onMount(() => { - requestAnimationFrame(() => { - Utils.focusOnFocusableChild(mainContent); - }); - }); @@ -31,7 +25,7 @@ use:trapFocus style="z-index: 21" > -
{}}> +
{}}>
diff --git a/src/UI/Base/Hotkeys.ts b/src/UI/Base/Hotkeys.ts index 379e97f68..1359163b5 100644 --- a/src/UI/Base/Hotkeys.ts +++ b/src/UI/Base/Hotkeys.ts @@ -22,16 +22,13 @@ export default class Hotkeys { }[] >([]) - private static textElementSelected(event: KeyboardEvent): boolean { - if (event.ctrlKey || event.altKey) { - // This is an event with a modifier-key, lets not ignore it - return false - } - if (event.key === "Escape") { - return false // Another not-printable character that should not be ignored - } - return ["input", "textarea"].includes(document?.activeElement?.tagName?.toLowerCase()) - } + /** + * Register a hotkey + * @param key + * @param documentation + * @param action the function to run. It might return 'false', indicating that it didn't do anything and gives control back to the default flow + * @constructor + */ public static RegisterHotkey( key: ( | { @@ -50,7 +47,7 @@ export default class Hotkeys { onUp?: boolean }, documentation: string | Translation, - action: () => void + action: () => void | false ) { const type = key["onUp"] ? "keyup" : "keypress" let keycode: string = key["ctrl"] ?? key["shift"] ?? key["alt"] ?? key["nomod"] @@ -69,8 +66,9 @@ export default class Hotkeys { if (key["ctrl"] !== undefined) { document.addEventListener("keydown", function (event) { if (event.ctrlKey && event.key === keycode) { - action() - event.preventDefault() + if (action() !== false) { + event.preventDefault() + } } }) } else if (key["shift"] !== undefined) { @@ -80,15 +78,17 @@ export default class Hotkeys { return } if (event.shiftKey && event.key === keycode) { - action() - event.preventDefault() + if (action() !== false) { + event.preventDefault() + } } }) } else if (key["alt"] !== undefined) { document.addEventListener(type, function (event) { if (event.altKey && event.key === keycode) { - action() - event.preventDefault() + if (action() !== false) { + event.preventDefault() + } } }) } else if (key["nomod"] !== undefined) { @@ -98,8 +98,10 @@ export default class Hotkeys { return } if (event.key === keycode) { - action() - event.preventDefault() + const result = action() + if (result !== false) { + event.preventDefault() + } } }) } @@ -113,6 +115,9 @@ export default class Hotkeys { if (keycode.length == 1) { keycode = keycode.toUpperCase() } + if (keycode === " ") { + keycode = "Spacebar" + } modifiers.push(keycode) return <[string, string | Translation]>[modifiers.join("+"), documentation] }) @@ -139,4 +144,15 @@ export default class Hotkeys { static generateDocumentationDynamic(): BaseUIElement { return new VariableUiElement(Hotkeys._docs.map((_) => Hotkeys.generateDocumentation())) } + + private static textElementSelected(event: KeyboardEvent): boolean { + if (event.ctrlKey || event.altKey) { + // This is an event with a modifier-key, lets not ignore it + return false + } + if (event.key === "Escape") { + return false // Another not-printable character that should not be ignored + } + return ["input", "textarea"].includes(document?.activeElement?.tagName?.toLowerCase()) + } } diff --git a/src/UI/Base/LoginToggle.svelte b/src/UI/Base/LoginToggle.svelte index dab8f8711..12d0e4ea9 100644 --- a/src/UI/Base/LoginToggle.svelte +++ b/src/UI/Base/LoginToggle.svelte @@ -1,10 +1,10 @@