forked from MapComplete/MapComplete
		
	A11y: improve documentation of hotkeys, keyboard navigation acts more like an aria-grid
This commit is contained in:
		
							parent
							
								
									6da72b80ef
								
							
						
					
					
						commit
						c6f738609f
					
				
					 7 changed files with 85 additions and 43 deletions
				
			
		|  | @ -477,6 +477,7 @@ | ||||||
|         "selectMapnik": "Set the background layer to OpenStreetMap-carto", |         "selectMapnik": "Set the background layer to OpenStreetMap-carto", | ||||||
|         "selectOsmbasedmap": "Set the background layer to on OpenStreetMap-based map (or disable the background raster layer)", |         "selectOsmbasedmap": "Set the background layer to on OpenStreetMap-based map (or disable the background raster layer)", | ||||||
|         "selectSearch": "Select the search bar to search locations", |         "selectSearch": "Select the search bar to search locations", | ||||||
|  |         "shakePhone": "Shaking your phone", | ||||||
|         "title": "Hotkeys" |         "title": "Hotkeys" | ||||||
|     }, |     }, | ||||||
|     "image": { |     "image": { | ||||||
|  |  | ||||||
|  | @ -1431,6 +1431,11 @@ video { | ||||||
|   row-gap: 0.25rem; |   row-gap: 0.25rem; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .gap-x-4 { | ||||||
|  |   -webkit-column-gap: 1rem; | ||||||
|  |           column-gap: 1rem; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .gap-x-0\.5 { | .gap-x-0\.5 { | ||||||
|   -webkit-column-gap: 0.125rem; |   -webkit-column-gap: 0.125rem; | ||||||
|           column-gap: 0.125rem; |           column-gap: 0.125rem; | ||||||
|  | @ -1690,6 +1695,11 @@ video { | ||||||
|   border-color: rgb(219 234 254 / var(--tw-border-opacity)); |   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 { | .border-gray-300 { | ||||||
|   --tw-border-opacity: 1; |   --tw-border-opacity: 1; | ||||||
|   border-color: rgb(209 213 219 / var(--tw-border-opacity)); |   border-color: rgb(209 213 219 / var(--tw-border-opacity)); | ||||||
|  |  | ||||||
|  | @ -83,7 +83,7 @@ export default class ThemeViewState implements SpecialVisualizationState { | ||||||
|     readonly osmConnection: OsmConnection |     readonly osmConnection: OsmConnection | ||||||
|     readonly selectedElement: UIEventSource<Feature> |     readonly selectedElement: UIEventSource<Feature> | ||||||
|     readonly selectedElementAndLayer: Store<{ feature: Feature; layer: LayerConfig }> |     readonly selectedElementAndLayer: Store<{ feature: Feature; layer: LayerConfig }> | ||||||
|     readonly mapProperties: MapProperties & ExportableMap |     readonly mapProperties: MapLibreAdaptor & MapProperties & ExportableMap | ||||||
|     readonly osmObjectDownloader: OsmObjectDownloader |     readonly osmObjectDownloader: OsmObjectDownloader | ||||||
| 
 | 
 | ||||||
|     readonly dataIsLoading: Store<boolean> |     readonly dataIsLoading: Store<boolean> | ||||||
|  |  | ||||||
|  | @ -10,17 +10,13 @@ import { FixedUiElement } from "./FixedUiElement" | ||||||
| import Translations from "../i18n/Translations" | import Translations from "../i18n/Translations" | ||||||
| 
 | 
 | ||||||
| export default class Hotkeys { | 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 } |             key: { ctrl?: string; shift?: string; alt?: string; nomod?: string; onUp?: boolean } | ||||||
|             documentation: string | Translation |             documentation: string | Translation | ||||||
|  |             alsoTriggeredBy: Translation[] | ||||||
|         }[] |         }[] | ||||||
|     > = new UIEventSource< |     > = new UIEventSource([]) | ||||||
|         { |  | ||||||
|             key: { ctrl?: string; shift?: string; alt?: string; nomod?: string; onUp?: boolean } |  | ||||||
|             documentation: string | Translation |  | ||||||
|         }[] |  | ||||||
|     >([]) |  | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Register a hotkey |      * Register a hotkey | ||||||
|  | @ -48,7 +44,7 @@ export default class Hotkeys { | ||||||
|         }, |         }, | ||||||
|         documentation: string | Translation, |         documentation: string | Translation, | ||||||
|         action: () => void | false, |         action: () => void | false, | ||||||
|         alsoTriggeredOn?: Translation[] |         alsoTriggeredBy?: Translation[] | ||||||
|     ) { |     ) { | ||||||
|         const type = key["onUp"] ? "keyup" : "keypress" |         const type = key["onUp"] ? "keyup" : "keypress" | ||||||
|         let keycode: string = key["ctrl"] ?? key["shift"] ?? key["alt"] ?? key["nomod"] |         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() |         this._docs.ping() | ||||||
|         if (Utils.runningFromConsole) { |         if (Utils.runningFromConsole) { | ||||||
|             return |             return | ||||||
|  | @ -109,37 +105,56 @@ export default class Hotkeys { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     static generateDocumentation(): BaseUIElement { |     static generateDocumentation(): BaseUIElement { | ||||||
|         let byKey: [string, string | Translation][] = Hotkeys._docs.data |         return new VariableUiElement( | ||||||
|             .map(({ key, documentation }) => { |             Hotkeys._docs.mapD((docs) => { | ||||||
|                 const modifiers = Object.keys(key).filter((k) => k !== "nomod" && k !== "onUp") |                 let byKey: [string, string | Translation, Translation[] | undefined][] = docs | ||||||
|                 let keycode: string = key["ctrl"] ?? key["shift"] ?? key["alt"] ?? key["nomod"] |                     .map(({ key, documentation, alsoTriggeredBy }) => { | ||||||
|                 if (keycode.length == 1) { |                         const modifiers = Object.keys(key).filter( | ||||||
|                     keycode = keycode.toUpperCase() |                             (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 === " ") { |                 const t = Translations.t.hotkeyDocumentation | ||||||
|                     keycode = "Spacebar" |                 return new Combine([ | ||||||
|                 } |                     new Title(t.title, 1), | ||||||
|                 modifiers.push(keycode) |                     t.intro, | ||||||
|                 return <[string, string | Translation]>[modifiers.join("+"), documentation] |                     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 { |     static generateDocumentationDynamic(): BaseUIElement { | ||||||
|  |  | ||||||
|  | @ -44,7 +44,10 @@ | ||||||
|     Translations.t.hotkeyDocumentation.queryCurrentLocation, |     Translations.t.hotkeyDocumentation.queryCurrentLocation, | ||||||
|     () => { |     () => { | ||||||
|       displayLocation() |       displayLocation() | ||||||
|     } |     }, | ||||||
|  |     [ | ||||||
|  |     Translations.t.hotkeyDocumentation.shakePhone | ||||||
|  |     ] | ||||||
|   ) |   ) | ||||||
| 
 | 
 | ||||||
|   Motion.singleton.startListening() |   Motion.singleton.startListening() | ||||||
|  |  | ||||||
|  | @ -511,7 +511,19 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap { | ||||||
|             await Utils.waitFor(250) |             await Utils.waitFor(250) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 |     public installCustomKeyboardHandler(viewport: Store<HTMLDivElement>) { | ||||||
|  |         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 { |     private removeCurrentLayer(map: MLMap): void { | ||||||
|         if (this._currentRasterLayer) { |         if (this._currentRasterLayer) { | ||||||
|             // hide the previous layer
 |             // hide the previous layer
 | ||||||
|  |  | ||||||
|  | @ -66,6 +66,7 @@ | ||||||
|   import FilterPanel from "./BigComponents/FilterPanel.svelte" |   import FilterPanel from "./BigComponents/FilterPanel.svelte" | ||||||
|   import PrivacyPolicy from "./BigComponents/PrivacyPolicy.svelte" |   import PrivacyPolicy from "./BigComponents/PrivacyPolicy.svelte" | ||||||
|   import { BBox } from "../Logic/BBox" |   import { BBox } from "../Logic/BBox" | ||||||
|  |   import { MapLibreAdaptor } from "./Map/MapLibreAdaptor.js" | ||||||
| 
 | 
 | ||||||
|   export let state: ThemeViewState |   export let state: ThemeViewState | ||||||
|   let layout = state.layout |   let layout = state.layout | ||||||
|  | @ -100,7 +101,7 @@ | ||||||
|   let visualFeedback = state.visualFeedback |   let visualFeedback = state.visualFeedback | ||||||
|   let viewport: UIEventSource<HTMLDivElement> = new UIEventSource<HTMLDivElement>(undefined) |   let viewport: UIEventSource<HTMLDivElement> = new UIEventSource<HTMLDivElement>(undefined) | ||||||
|   let mapproperties: MapProperties = state.mapProperties |   let mapproperties: MapProperties = state.mapProperties | ||||||
| 
 |   state.mapProperties.installCustomKeyboardHandler(viewport) | ||||||
|   function updateViewport() { |   function updateViewport() { | ||||||
|     const rect = viewport.data?.getBoundingClientRect() |     const rect = viewport.data?.getBoundingClientRect() | ||||||
|     if (!rect) { |     if (!rect) { | ||||||
|  | @ -159,7 +160,7 @@ | ||||||
|   <div |   <div | ||||||
|     class="absolute top-0 left-0 flex h-screen w-screen items-center justify-center overflow-hidden pointer-events-none" |     class="absolute top-0 left-0 flex h-screen w-screen items-center justify-center overflow-hidden pointer-events-none" | ||||||
|   > |   > | ||||||
|     <div bind:this={$viewport} style="border: 2px solid #ff000044; width: 300px; height: 300px" /> |     <div bind:this={$viewport} class:border={$visualFeedback} style="border: 2px solid #ff000044; width: 300px; height: 300px" /> | ||||||
|   </div> |   </div> | ||||||
| {/if} | {/if} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue