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,10 +105,15 @@ 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 }) => {
|
||||||
|
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) {
|
if (keycode.length == 1) {
|
||||||
keycode = keycode.toUpperCase()
|
keycode = keycode.toUpperCase()
|
||||||
}
|
}
|
||||||
|
@ -120,7 +121,11 @@ export default class Hotkeys {
|
||||||
keycode = "Spacebar"
|
keycode = "Spacebar"
|
||||||
}
|
}
|
||||||
modifiers.push(keycode)
|
modifiers.push(keycode)
|
||||||
return <[string, string | Translation]>[modifiers.join("+"), documentation]
|
return <[string, string | Translation, Translation[] | undefined]>[
|
||||||
|
modifiers.join("+"),
|
||||||
|
documentation,
|
||||||
|
alsoTriggeredBy,
|
||||||
|
]
|
||||||
})
|
})
|
||||||
.sort()
|
.sort()
|
||||||
byKey = Utils.NoNull(byKey)
|
byKey = Utils.NoNull(byKey)
|
||||||
|
@ -135,11 +140,21 @@ export default class Hotkeys {
|
||||||
t.intro,
|
t.intro,
|
||||||
new Table(
|
new Table(
|
||||||
[t.key, t.action],
|
[t.key, t.action],
|
||||||
byKey.map(([key, doc]) => {
|
byKey.map(([key, doc, alsoTriggeredBy]) => {
|
||||||
return [new FixedUiElement(key).SetClass("literal-code"), doc]
|
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]
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
])
|
])
|
||||||
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
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…
Reference in a new issue