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