Accessibility: add focus trapping, debug tab cycling, UI tweaks for mobile browser

This commit is contained in:
Pieter Vander Vennet 2023-12-07 21:57:20 +01:00
parent 307549b593
commit 8ae4d810d6
19 changed files with 123 additions and 77 deletions

13
package-lock.json generated
View file

@ -6,7 +6,7 @@
"packages": {
"": {
"name": "mapcomplete",
"version": "0.36.1",
"version": "0.36.2",
"license": "GPL-3.0-or-later",
"dependencies": {
"@rgossiaux/svelte-headlessui": "^1.0.2",
@ -56,6 +56,7 @@
"svg-path-parser": "^1.1.0",
"tailwind-merge": "^1.13.1",
"tailwindcss": "^3.1.8",
"trap-focus-svelte": "^1.0.1",
"vite-node": "^0.28.3",
"vitest": "^0.28.3",
"wikibase-sdk": "^7.14.0",
@ -12140,6 +12141,11 @@
"node": ">=14"
}
},
"node_modules/trap-focus-svelte": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/trap-focus-svelte/-/trap-focus-svelte-1.0.1.tgz",
"integrity": "sha512-qacSd68+c12mudUu9Mo70Ea16263ich2APFh1d0K7k9rLtwNcxlxNqA6l7Wv7xdzhJbC9TASxroiDSkiN2349w=="
},
"node_modules/ts-api-utils": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.1.tgz",
@ -22659,6 +22665,11 @@
"punycode": "^2.3.0"
}
},
"trap-focus-svelte": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/trap-focus-svelte/-/trap-focus-svelte-1.0.1.tgz",
"integrity": "sha512-qacSd68+c12mudUu9Mo70Ea16263ich2APFh1d0K7k9rLtwNcxlxNqA6l7Wv7xdzhJbC9TASxroiDSkiN2349w=="
},
"ts-api-utils": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.1.tgz",

View file

@ -143,6 +143,7 @@
"svg-path-parser": "^1.1.0",
"tailwind-merge": "^1.13.1",
"tailwindcss": "^3.1.8",
"trap-focus-svelte": "^1.0.1",
"vite-node": "^0.28.3",
"vitest": "^0.28.3",
"wikibase-sdk": "^7.14.0",

View file

@ -1796,14 +1796,14 @@ video {
padding: 0.25rem;
}
.p-0\.5 {
padding: 0.125rem;
}
.p-0 {
padding: 0px;
}
.p-0\.5 {
padding: 0.125rem;
}
.p-12 {
padding: 3rem;
}
@ -2244,7 +2244,6 @@ body {
.focusable {
/* Not a 'real' class, but rather an indication to FloatOver and ModalRight to, when they open, grab the focus */
border: 1px solid red
}
svg,

View file

@ -190,19 +190,45 @@ export default class GenerateImageAnalysis extends Script {
if (!existsSync(viewDir)) {
mkdirSync(viewDir)
}
const targetpath = datapath + "/views.csv"
const total = allImages.size
let dloaded = 0
let skipped = 0
let err = 0
for (const image of Array.from(allImages)) {
const cachedView = viewDir + "/" + image.replace(/\\/g, "_")
const cachedView = viewDir + "/" + image.replace(/\//g, "_")
let attribution: LicenseInfo
if (existsSync(cachedView)) {
attribution = JSON.parse(readFileSync(cachedView, "utf8"))
skipped++
} else {
attribution = await Imgur.singleton.DownloadAttribution(image)
writeFileSync(cachedView, JSON.stringify(attribution))
try {
attribution = await Imgur.singleton.DownloadAttribution(image)
await ScriptUtils.sleep(500)
writeFileSync(cachedView, JSON.stringify(attribution))
dloaded++
} catch (e) {
err++
continue
}
}
results.push([image, attribution.views])
if (dloaded % 50 === 0) {
console.log({
dloaded,
skipped,
total,
err,
progress: Math.round(dloaded + skipped + err),
})
}
if ((dloaded + skipped + err) % 100 === 0) {
console.log("Writing views to", targetpath)
fs.writeFileSync(targetpath, results.map((r) => r.join(",")).join("\n"))
}
}
const targetpath = datapath + "/views.csv"
console.log("Writing views to", targetpath)
fs.writeFileSync(targetpath, results.map((r) => r.join(",")).join("\n"))
}
@ -416,8 +442,8 @@ export default class GenerateImageAnalysis extends Script {
const imageBackupPath = args[0]
await this.downloadData(datapath, cached)
await this.downloadMetadata(datapath)
await this.downloadViews(datapath)
await this.downloadMetadata(datapath)
await this.downloadAllImages(datapath, imageBackupPath)
this.analyze(datapath)
}

View file

@ -39,7 +39,7 @@ export default class UserRelatedState {
public readonly installedUserThemes: Store<string[]>
public readonly showAllQuestionsAtOnce: UIEventSource<boolean>
public readonly showTags: UIEventSource<"no" | undefined | "always" | "yes" | "full">
public readonly showCrosshair: UIEventSource<"yes" | undefined>
public readonly showCrosshair: UIEventSource<"yes" | "always" | "no" | undefined>
public readonly fixateNorth: UIEventSource<undefined | "yes">
public readonly homeLocation: FeatureSource
/**

View file

@ -1,10 +1,11 @@
import { Translation } from "../UI/i18n/Translation"
import { DenominationConfigJson } from "./ThemeConfig/Json/UnitConfigJson"
import Translations from "../UI/i18n/Translations"
import { Store } from "../Logic/UIEventSource"
import BaseUIElement from "../UI/BaseUIElement"
import Toggle from "../UI/Input/Toggle"
/**
* A 'denomination' is one way to write a certain quantity.
* For example, 'meter', 'kilometer', 'mile' and 'foot' are all possible ways to quantify 'length'
*/
export class Denomination {
public readonly canonical: string
public readonly _canonicalSingular: string
@ -53,8 +54,8 @@ export class Denomination {
/**
* Create a representation of the given value
* @param value: the value from OSM
* @param actAsDefault: if set and the value can be parsed as number, will be parsed and trimmed
* @param value the value from OSM
* @param actAsDefault if set and the value can be parsed as number, will be parsed and trimmed
*
* const unit = new Denomination({
* canonicalDenomination: "m",
@ -82,6 +83,8 @@ export class Denomination {
* unit.canonicalValue("42", true) // =>"42"
* unit.canonicalValue("42 m", true) // =>"42"
* unit.canonicalValue("42 meter", true) // =>"42"
*
*
*/
public canonicalValue(value: string, actAsDefault: boolean): string {
if (value === undefined) {

View file

@ -1,28 +1,35 @@
<script lang="ts">
import { createEventDispatcher, onMount } from "svelte";
import { XCircleIcon } from "@rgossiaux/svelte-heroicons/solid"
import { twMerge } from "tailwind-merge"
import { XCircleIcon } from "@rgossiaux/svelte-heroicons/solid";
import { twMerge } from "tailwind-merge";
import { Utils } from "../../Utils";
import { trapFocus } from 'trap-focus-svelte'
/**
* The slotted element will be shown on top, with a lower-opacity border
*/
const dispatch = createEventDispatcher<{ close }>()
export let extraClasses = "p-4 md:p-6"
let mainContent: HTMLElement
const dispatch = createEventDispatcher<{ close }>();
export let extraClasses = "p-4 md:p-6";
let mainContent: HTMLElement;
onMount(() => {
console.log("Mounting floatover")
mainContent?.focus()
})
requestAnimationFrame(() => {
Utils.focusOnFocusableChild(mainContent);
});
});
</script>
<div
class={twMerge("absolute top-0 right-0 h-screen w-screen", extraClasses)}
style="background-color: #00000088; z-index: 20"
on:click={() => {
<!-- Draw the background over the total screen -->
<div class="w-screen h-screen absolute top-0 left-0" style="background-color: #00000088; z-index: 20" on:click={() => {
dispatch("close")
}}
}}>
</div>
<!-- draw a _second_ absolute div, placed using 'bottom' which will be above the navigation bar on mobile browsers -->
<div
class={twMerge("absolute bottom-0 right-0 h-full w-screen", extraClasses)}
use:trapFocus
style="z-index: 21"
>
<div bind:this={mainContent} class="content normal-background" on:click|stopPropagation={() => {}}>
<div class="h-full rounded-xl">
@ -30,21 +37,23 @@
</div>
<slot name="close-button">
<!-- The close button is placed _after_ the default slot in order to always paint it on top -->
<div
class="absolute right-10 top-10 h-8 w-8 cursor-pointer"
<button
class="absolute right-10 top-10 h-8 w-8 cursor-pointer p-0 border-none bg-white"
on:click={() => dispatch("close")}
>
<XCircleIcon />
</div>
</button>
</slot>
</div>
</div>
<style>
.content {
height: 100%;
border-radius: 0.5rem;
overflow-x: hidden;
box-shadow: 0 0 1rem #00000088;
}
.content {
height: 100%;
border-radius: 0.5rem;
overflow-x: hidden;
box-shadow: 0 0 1rem #00000088;
}
</style>

View file

@ -10,14 +10,14 @@
export let state: {
osmConnection: OsmConnection
featureSwitches?: { featureSwitchUserbadge?: UIEventSource<boolean> }
featureSwitches?: { featureSwitchEnableLogin?: UIEventSource<boolean> }
}
/**
* If set, 'loading' will act as if we are already logged in.
*/
export let ignoreLoading: boolean = false
let loadingStatus = state?.osmConnection?.loadingStatus ?? new ImmutableStore("logged-in")
let badge = state?.featureSwitches?.featureSwitchUserbadge ?? new ImmutableStore(true)
let badge = state?.featureSwitches?.featureSwitchEnableLogin ?? new ImmutableStore(true)
const t = Translations.t.general
const offlineModes: Partial<Record<OsmServiceState, Translation>> = {
offline: t.loginFailedOfflineMode,

View file

@ -2,6 +2,7 @@
import { createEventDispatcher, onMount } from "svelte";
import { XCircleIcon } from "@rgossiaux/svelte-heroicons/solid";
import { Utils } from "../../Utils";
import { trapFocus } from 'trap-focus-svelte'
/**
* The slotted element will be shown on the right side
@ -13,13 +14,13 @@
onMount(() => {
window.setTimeout(
() => Utils.focusOnFocusableChild(mainContent), 250
)
})
);
});
</script>
<div
bind:this={mainContent}
use:trapFocus
class="absolute top-0 right-0 h-screen w-full overflow-y-auto drop-shadow-2xl md:w-6/12 lg:w-5/12 xl:w-4/12"
style="max-width: 100vw; max-height: 100vh"
>

View file

@ -14,10 +14,8 @@
export let selectedElement: Feature
export let highlightedRendering: UIEventSource<string> = undefined
let tags: UIEventSource<Record<string, string>> = state.featureProperties.getStore(selectedElement.properties.id)
$: {
tags = state.featureProperties.getStore(selectedElement.properties.id)
}
export let tags: UIEventSource<Record<string, string>> = state.featureProperties.getStore(selectedElement.properties.id)
let _metatags: Record<string, string>
onDestroy(
state.userRelatedState.preferencesAsTags.addCallbackAndRun((tags) => {
@ -28,7 +26,7 @@
let knownTagRenderings: Store<TagRenderingConfig[]> = tags.mapD(tgs => layer.tagRenderings.filter(
(config) =>
(config.condition?.matchesProperties(tgs) ?? true) &&
config.metacondition?.matchesProperties({ ...tgs, ..._metatags } ?? true) &&
(config.metacondition?.matchesProperties({ ...tgs, ..._metatags }) ?? true) &&
config.IsKnown(tgs)
))
</script>
@ -39,7 +37,7 @@
<Tr t={Translations.t.general.returnToTheMap} />
</button>
{:else}
<div class="flex h-full flex-col gap-y-2 overflow-y-auto p-1 px-2 focusable" tabindex="-1">
<div class="flex h-full w-full flex-col gap-y-2 overflow-y-auto p-1 px-2 focusable" tabindex="-1">
{#each $knownTagRenderings as config (config.id)}
<TagRenderingEditable
{tags}

View file

@ -12,7 +12,7 @@ import { twMerge } from "tailwind-merge";
export let image: ProvidedImage
export let clss: string = undefined
async function download() {
const response = await fetch(image.url)
const response = await fetch(image.url_hd ?? image.url )
const blob = await response.blob()
Utils.offerContentsAsDownloadableFile(blob, new URL(image.url).pathname.split("/").at(-1), {
mimetype: "image/jpg",
@ -22,11 +22,11 @@ async function download() {
</script>
<div class={twMerge("w-full h-full relative", clss)}>
<div class="absolute top-0 left-0 w-full h-full overflow-hidden">
<div class="absolute top-0 left-0 w-full h-full overflow-hidden panzoom-container focusable">
<ImagePreview image={image} />
</div>
<div class="absolute bottom-0 left-0 w-full pointer-events-none flex flex-wrap justify-between items-end">
<div class="pointer-events-auto w-fit opacity-50 hover:opacity-100 transition-colors duration-200">
<div class="pointer-events-auto w-fit opacity-50 hover:opacity-100 transition-colors duration-200 m-1">
<ImageAttribution image={image} />
</div>

View file

@ -25,4 +25,4 @@
</script>
<img bind:this={panzoomEl} src={image.url_hd ?? image.url} class="w-full h-auto"/>
<img bind:this={panzoomEl} src={image.url_hd ?? image.url} class="w-fit h-fit panzoom-image"/>

View file

@ -13,8 +13,11 @@
import Loading from "../Base/Loading.svelte"
export let state: SpecialVisualizationState
export let tags: Store<OsmTags>
export let featureId = tags.data.id
export let tags: Store<OsmTags> = undefined
export let featureId = tags?.data?.id
if(featureId === undefined){
throw "No tags or featureID given"
}
export let showThankYou: boolean = true
const { uploadStarted, uploadFinished, retried, failed } =
state.imageUploadManager.getCountsFor(featureId)

View file

@ -77,7 +77,7 @@ export interface SpecialVisualizationState {
readonly showTags: UIEventSource<"no" | undefined | "always" | "yes" | "full">
readonly mangroveIdentity: MangroveIdentity
readonly showAllQuestionsAtOnce: UIEventSource<boolean>
readonly preferencesAsTags: Store<Record<string, string>>
readonly preferencesAsTags: UIEventSource<Record<string, string>>
readonly language: UIEventSource<string>
}
readonly lastClickObject: WritableFeatureSource

View file

@ -40,7 +40,7 @@ import FeatureReviews from "../Logic/Web/MangroveReviews"
import Maproulette from "../Logic/Maproulette"
import SvelteUIElement from "./Base/SvelteUIElement"
import { BBoxFeatureSourceForLayer } from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource"
import { Feature, Point } from "geojson"
import { Feature } from "geojson"
import { GeoOperations } from "../Logic/GeoOperations"
import CreateNewNote from "./Popup/CreateNewNote.svelte"
import AddNewPoint from "./Popup/AddNewPoint/AddNewPoint.svelte"
@ -48,8 +48,7 @@ import UserProfile from "./BigComponents/UserProfile.svelte"
import Link from "./Base/Link"
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig"
import { OsmTags, WayId } from "../Models/OsmFeature"
import MoveWizard from "./Popup/MoveWizard"
import { WayId } from "../Models/OsmFeature"
import SplitRoadWizard from "./Popup/SplitRoadWizard"
import { ExportAsGpxViz } from "./Popup/ExportAsGpxViz"
import WikipediaPanel from "./Wikipedia/WikipediaPanel.svelte"
@ -82,6 +81,8 @@ import MarkAsFavouriteMini from "./Popup/MarkAsFavouriteMini.svelte"
import NextChangeViz from "./OpeningHours/NextChangeViz.svelte"
import NearbyImages from "./Image/NearbyImages.svelte"
import NearbyImagesCollapsed from "./Image/NearbyImagesCollapsed.svelte"
import { svelte } from "@sveltejs/vite-plugin-svelte"
import MoveWizard from "./Popup/MoveWizard.svelte"
class NearbyImageVis implements SpecialVisualization {
// Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests
@ -515,12 +516,11 @@ export default class SpecialVisualizations {
return undefined
}
return new MoveWizard(
<Feature<Point>>feature,
<UIEventSource<OsmTags>>tagSource,
return new SvelteUIElement(MoveWizard, {
state,
layer.allowMove
)
featureToMove: feature,
layer,
})
},
},
{

View file

@ -89,7 +89,7 @@
})
let selectedLayer: UIEventSource<LayerConfig> = state.selectedElement.mapD(element => state.layout.getMatchingLayer(element.properties));
let selectedLayer: Store<LayerConfig> = state.selectedElement.mapD(element => state.layout.getMatchingLayer(element.properties));
let currentZoom = state.mapProperties.zoom;
let showCrosshair = state.userRelatedState.showCrosshair;
@ -125,7 +125,6 @@
bounds={state.mapProperties.bounds}
perLayer={state.perLayer}
selectedElement={state.selectedElement}
{selectedLayer}
/>
</div>
</If>
@ -144,7 +143,6 @@
{#if currentViewLayer?.tagRenderings && currentViewLayer.defaultIcon()}
<MapControlButton
on:click={() => {
selectedLayer.setData(currentViewLayer)
selectedElement.setData(state.currentView.features?.data?.[0])
}}
>
@ -269,7 +267,7 @@
>
<XCircleIcon />
</div>
<ImageOperations clss="focusable" image={$previewedImage} />
<ImageOperations image={$previewedImage} />
</FloatOver>
</If>

View file

@ -1658,7 +1658,6 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
}
const child = <HTMLElement>childs.item(0)
if (child === null) {
console.log("Focussing on child element: no child element found for", el)
return undefined
}
if (
@ -1668,7 +1667,6 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
) {
child.setAttribute("tabindex", "-1")
}
console.log("Focussing on", child)
child?.focus()
})
}

View file

@ -69,12 +69,11 @@ body {
color: var(--foreground-color);
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 */
border: 1px solid red
}
svg,
img {
box-sizing: content-box;

View file

@ -40,7 +40,7 @@
<body>
<div class="h-full" id="maindiv">
<div class="h-screen" id="maindiv">
<div id="default-main h-full">
<div class="w-full h-screen flex flex-col items-center justify-between p-8">
<div class="w-full h-full flex flex-col items-center">