A11y: improve flow with screenreader, some more refactoring to svelte, see #1181

This commit is contained in:
Pieter Vander Vennet 2023-12-13 02:16:53 +01:00
parent 40067e35d4
commit 48ac539272
15 changed files with 188 additions and 226 deletions

View file

@ -1,7 +1,7 @@
{
"id": "unit",
"description": {
"en": "Library layer with all common units"
"en": "Library layer with all common units. Units can _only_ be imported from this file."
},
"source": "special:library",
"units": [

View file

@ -4,7 +4,7 @@
"nl": "Breng jouw buurtnatuur in kaart"
},
"description": {
"nl": "<img style='float:right;margin: 1em;width: 10em;height: auto;' src='./assets/themes/buurtnatuur/groen_logo.svg' alt='logo-groen' class='logo/> <br /><b>Natuur maakt gelukkig.</b> Aan de hand van deze website willen we de natuur dicht bij ons beter inventariseren. Met als doel meer mensen te laten genieten van toegankelijke natuur én te strijden voor meer natuur in onze buurten.<ul><li>In welke natuurgebieden kan jij terecht? Hoe toegankelijk zijn ze?</li><li>In welke bossen kan een gezin in jouw gemeente opnieuw op adem komen?</li><li>Op welke onbekende plekjes is het zalig spelen?</li></ul><p>Samen kleuren we heel Vlaanderen en Brussel groen.Blijf op de hoogte van de resultaten van buurtnatuur.be: <a href='https://www.groen.be/buurtnatuur' target='_blank'>meld je aan voor e-mailupdates</a>."
"nl": "<img style='float:right;margin: 1em;width: 10em;height: auto;' src='./assets/themes/buurtnatuur/groen_logo.svg' alt='logo-groen' class='logo'/> <br /><b>Natuur maakt gelukkig.</b> Aan de hand van deze website willen we de natuur dicht bij ons beter inventariseren. Met als doel meer mensen te laten genieten van toegankelijke natuur én te strijden voor meer natuur in onze buurten.<ul><li>In welke natuurgebieden kan jij terecht? Hoe toegankelijk zijn ze?</li><li>In welke bossen kan een gezin in jouw gemeente opnieuw op adem komen?</li><li>Op welke onbekende plekjes is het zalig spelen?</li></ul><p>Samen kleuren we heel Vlaanderen en Brussel groen.Blijf op de hoogte van de resultaten van buurtnatuur.be: <a href='https://www.groen.be/buurtnatuur' target='_blank'>meld je aan voor e-mailupdates</a>."
},
"shortDescription": {
"nl": "Met deze tool kan je natuur in je buurt in kaart brengen en meer informatie geven over je favoriete plekje"

View file

@ -182,7 +182,8 @@
"backgroundSwitch": "Switch background",
"cancel": "Cancel",
"confirm": "Confirm",
"customThemeIntro": "<h3>Custom themes</h3>These are previously visited user-generated themes.",
"customThemeIntro": "These are previously visited user-generated themes.",
"customThemeTitle": "Custom themes",
"download": {
"downloadAsPdf": "Download a PDF of the current map",
"downloadAsPdfHelper": "Ideal to print the current map",

View file

@ -10346,4 +10346,4 @@
"render": "wind turbine"
}
}
}
}

View file

@ -876,16 +876,16 @@ video {
margin-bottom: 1rem;
}
.mx-1 {
margin-left: 0.25rem;
margin-right: 0.25rem;
}
.my-2 {
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}
.mx-1 {
margin-left: 0.25rem;
margin-right: 0.25rem;
}
.mx-10 {
margin-left: 2.5rem;
margin-right: 2.5rem;
@ -1262,10 +1262,6 @@ video {
width: 16rem;
}
.w-1\/2 {
width: 50%;
}
.w-14 {
width: 3.5rem;
}
@ -1533,6 +1529,10 @@ video {
justify-self: end;
}
.justify-self-center {
justify-self: center;
}
.overflow-auto {
overflow: auto;
}
@ -2892,6 +2892,10 @@ a.link-underline {
width: 6rem;
}
.sm\:w-1\/2 {
width: 50%;
}
.sm\:flex-nowrap {
flex-wrap: nowrap;
}

View file

@ -7,12 +7,18 @@
import Translations from "./i18n/Translations"
import Logo from "../assets/svg/Logo.svelte"
import Tr from "./Base/Tr.svelte"
import ToSvelte from "./Base/ToSvelte.svelte"
import MoreScreen from "./BigComponents/MoreScreen"
import LoginToggle from "./Base/LoginToggle.svelte"
import Pencil from "../assets/svg/Pencil.svelte"
import Login from "../assets/svg/Login.svelte"
import Constants from "../Models/Constants"
import { Store, UIEventSource } from "../Logic/UIEventSource"
import { placeholder } from "../Utils/placeholder"
import { SearchIcon } from "@rgossiaux/svelte-heroicons/solid"
import ThemesList from "./BigComponents/ThemesList.svelte"
import { LayoutInformation } from "../Models/ThemeConfig/LayoutConfig"
import * as themeOverview from "../assets/generated/theme_overview.json"
import UnofficialThemeList from "./BigComponents/UnofficialThemeList.svelte"
const featureSwitches = new OsmConnectionFeatureSwitches()
const osmConnection = new OsmConnection({
@ -20,17 +26,45 @@
oauth_token: QueryParameters.GetQueryParameter(
"oauth_token",
undefined,
"Used to complete the login"
"Used to complete the login",
),
})
const state = new UserRelatedState(osmConnection)
const t = Translations.t.index
const tr = Translations.t.general.morescreen
let userLanguages = osmConnection.userDetails.map((ud) => ud.languages)
let themeSearchText: UIEventSource<string | undefined> = new UIEventSource<string>(undefined)
document.addEventListener("keydown", function(event) {
if (event.ctrlKey && event.code === "KeyF") {
document.getElementById("theme-search")?.focus()
event.preventDefault()
}
})
let visitedHiddenThemes: Store<LayoutInformation[]>
const hiddenThemes: LayoutInformation[] =
(themeOverview["default"] ?? themeOverview)?.filter((layout) => layout.hideFromOverview) ?? []
{
const prefix = "mapcomplete-hidden-theme-"
const userPreferences = state.osmConnection.preferencesHandler.preferences
visitedHiddenThemes = userPreferences.map(preferences => {
const knownIds = new Set<string>(
Object.keys(preferences)
.filter((key) => key.startsWith(prefix))
.map((key) => key.substring(prefix.length, key.length - "-enabled".length)),
)
return hiddenThemes.filter((theme) => knownIds.has(theme.id))
})
}
</script>
<div class="m-4 flex flex-col">
<LanguagePicker clss="self-end" assignTo={state.language} availableLanguages={t.title.SupportedLanguages()}
preferredLanguages={userLanguages} />
<LanguagePicker assignTo={state.language} availableLanguages={t.title.SupportedLanguages()} clss="self-end"
preferredLanguages={userLanguages} />
<div class="mt-4 flex">
<div class="m-3 flex-none">
@ -48,9 +82,42 @@
</div>
</div>
<ToSvelte construct={new MoreScreen(state, true)} />
<form class="flex justify-center" on:submit|preventDefault={_ => MoreScreen.applySearch(themeSearchText.data)}>
<label
class="flex rounded-full border-2 border-black items-center my-2 w-full sm:w-1/2 neutral-label">
<SearchIcon aria-hidden="true" class="w-8 h-8" />
<input autofocus bind:value={$themeSearchText} class="mr-4 w-full" id="theme-search"
type="search"
use:placeholder={tr.searchForATheme}>
</label>
</form>
<ThemesList search={themeSearchText} {state} themes={MoreScreen.officialThemes} />
<LoginToggle {state}>
<ThemesList
hideThemes={false}
isCustom={false}
search={themeSearchText}
{state}
themes={$visitedHiddenThemes}
>
<svelte:fragment slot="title">
<h3>
<Tr t={tr.previouslyHiddenTitle} />
</h3>
<p>
<Tr t={tr.hiddenExplanation.Subs({
hidden_discovered: $visitedHiddenThemes.length.toString(),
total_hidden: hiddenThemes.length.toString(),
})} />
</p>
</svelte:fragment>
</ThemesList>
<UnofficialThemeList search={themeSearchText} {state} />
<div slot="not-logged-in">
<button class="w-full" on:click={() => osmConnection.AttemptLogin()}>
<Login class="mr-2 h-6 w-6 " />
@ -64,9 +131,13 @@
<Pencil class="mr-2 h-6 w-6" />
<Tr t={Translations.t.general.morescreen.createYourOwnTheme} />
</a>
</LoginToggle>
<Tr cls="link-underline" t={Translations.t.general.aboutMapComplete.intro} />
<Tr t={tr.streetcomplete} />
<div class="subtle mb-16 self-end">
v{Constants.vNumber}
</div>

View file

@ -5,6 +5,7 @@
import { Translation } from "../i18n/Translation"
import WeblateLink from "./WeblateLink.svelte"
import { Store } from "../../Logic/UIEventSource"
import FromHtml from "./FromHtml.svelte"
export let t: Translation
export let cls: string = ""
@ -14,9 +15,9 @@
</script>
{#if txt}
{#if $txt}
<span class={cls}>
{$txt}
<FromHtml src={$txt}/>
<WeblateLink context={t.context} />
</span>
{/if}

View file

@ -10,13 +10,13 @@
import { BBox } from "../../Logic/BBox"
import { GeoIndexedStoreForLayer } from "../../Logic/FeatureSource/Actors/GeoIndexedStore"
import { createEventDispatcher, onDestroy } from "svelte"
import { placeholder } from "../../Utils/placeholder"
export let perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer> | undefined = undefined
export let bounds: UIEventSource<BBox>
export let selectedElement: UIEventSource<Feature> | undefined = undefined
export let clearAfterView: boolean = true
let searchContents: string = ""
export let triggerSearch: UIEventSource<any> = new UIEventSource<any>(undefined)
onDestroy(
@ -31,17 +31,6 @@
let feedback: string = undefined
let placeholder = Translations.t.general.search.search.current
$:{
if(inputElement){
inputElement.placeholder = placeholder.data
}
}
onDestroy(placeholder.addCallbackAndRunD(placeholder => {
if(inputElement){
inputElement.placeholder = placeholder
}
}))
Hotkeys.RegisterHotkey({ ctrl: "F" }, Translations.t.hotkeyDocumentation.selectSearch, () => {
@ -124,6 +113,7 @@
bind:this={inputElement}
on:keypress={(keypr) => (keypr.key === "Enter" ? performSearch() : undefined)}
bind:value={searchContents}
use:placeholder={Translations.t.general.search.search}
/>
{/if}
</form>

View file

@ -1,50 +0,0 @@
<script lang="ts">
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import { UIEventSource } from "../../Logic/UIEventSource"
import * as themeOverview from "../../assets/generated/theme_overview.json"
import { Utils } from "../../Utils"
import ThemesList from "./ThemesList.svelte"
import Translations from "../i18n/Translations"
import { LayoutInformation } from "../../Models/ThemeConfig/LayoutConfig"
import LoginToggle from "../Base/LoginToggle.svelte"
export let search: UIEventSource<string>
export let state: { osmConnection: OsmConnection }
export let onMainScreen: boolean = true
const prefix = "mapcomplete-hidden-theme-"
const hiddenThemes: LayoutInformation[] =
(themeOverview["default"] ?? themeOverview)?.filter((layout) => layout.hideFromOverview) ?? []
const userPreferences = state.osmConnection.preferencesHandler.preferences
const t = Translations.t.general.morescreen
let knownThemesId: string[]
$: knownThemesId = Utils.NoNull(
Object.keys($userPreferences)
.filter((key) => key.startsWith(prefix))
.map((key) => key.substring(prefix.length, key.length - "-enabled".length))
)
$: console.log("Known theme ids:", knownThemesId)
$: knownThemes = hiddenThemes.filter((theme) => knownThemesId.includes(theme.id))
</script>
<LoginToggle {state}>
<ThemesList
hideThemes={false}
isCustom={false}
{onMainScreen}
{search}
{state}
themes={knownThemes}
>
<svelte:fragment slot="title">
<h3>{t.previouslyHiddenTitle.toString()}</h3>
<p>
{t.hiddenExplanation.Subs({
hidden_discovered: knownThemes.length.toString(),
total_hidden: hiddenThemes.length.toString(),
})}
</p>
</svelte:fragment>
</ThemesList>
</LoginToggle>

View file

@ -1,109 +1,48 @@
import Svg from "../../Svg"
import Combine from "../Base/Combine"
import Translations from "../i18n/Translations"
import LayoutConfig, { LayoutInformation } from "../../Models/ThemeConfig/LayoutConfig"
import { LayoutInformation } from "../../Models/ThemeConfig/LayoutConfig"
import { ImmutableStore, Store } from "../../Logic/UIEventSource"
import UserRelatedState from "../../Logic/State/UserRelatedState"
import { Utils } from "../../Utils"
import themeOverview from "../../assets/generated/theme_overview.json"
import { TextField } from "../Input/TextField"
import Locale from "../i18n/Locale"
import SvelteUIElement from "../Base/SvelteUIElement"
import ThemesList from "./ThemesList.svelte"
import HiddenThemeList from "./HiddenThemeList.svelte"
import UnofficialThemeList from "./UnofficialThemeList.svelte"
export default class MoreScreen extends Combine {
private static readonly officialThemes: LayoutInformation[] = themeOverview
export default class MoreScreen {
public static readonly officialThemes: LayoutInformation[] = themeOverview
constructor(
state: UserRelatedState & {
layoutToUse?: LayoutConfig
},
onMainScreen: boolean = false
) {
const tr = Translations.t.general.morescreen
const search = new TextField({
placeholder: tr.searchForATheme,
})
search.enterPressed.addCallbackD((searchTerm) => {
searchTerm = searchTerm.toLowerCase()
if (!searchTerm) {
return
}
if (searchTerm === "personal") {
window.location.href = MoreScreen.createUrlFor(
{ id: "personal" },
false,
state
).data
}
if (searchTerm === "bugs" || searchTerm === "issues") {
window.location.href = "https://github.com/pietervdvn/MapComplete/issues"
}
if (searchTerm === "source") {
window.location.href = "https://github.com/pietervdvn/MapComplete"
}
if (searchTerm === "docs") {
window.location.href = "https://github.com/pietervdvn/MapComplete/tree/develop/Docs"
}
if (searchTerm === "osmcha" || searchTerm === "stats") {
window.location.href = Utils.OsmChaLinkFor(7)
}
// Enter pressed -> search the first _official_ matchin theme and open it
const publicTheme = MoreScreen.officialThemes.find(
(th) =>
th.hideFromOverview == false &&
th.id !== "personal" &&
MoreScreen.MatchesLayout(th, searchTerm)
)
if (publicTheme !== undefined) {
window.location.href = MoreScreen.createUrlFor(publicTheme, false, state).data
}
const hiddenTheme = MoreScreen.officialThemes.find(
(th) => th.id !== "personal" && MoreScreen.MatchesLayout(th, searchTerm)
)
if (hiddenTheme !== undefined) {
window.location.href = MoreScreen.createUrlFor(hiddenTheme, false, state).data
}
})
if (onMainScreen) {
search.focus()
document.addEventListener("keydown", function (event) {
if (event.ctrlKey && event.code === "KeyF") {
search.focus()
event.preventDefault()
}
})
public static applySearch(searchTerm: string) {
searchTerm = searchTerm.toLowerCase()
if (!searchTerm) {
return
}
if (searchTerm === "personal") {
window.location.href = MoreScreen.createUrlFor({ id: "personal" }, false).data
}
if (searchTerm === "bugs" || searchTerm === "issues") {
window.location.href = "https://github.com/pietervdvn/MapComplete/issues"
}
if (searchTerm === "source") {
window.location.href = "https://github.com/pietervdvn/MapComplete"
}
if (searchTerm === "docs") {
window.location.href = "https://github.com/pietervdvn/MapComplete/tree/develop/Docs"
}
if (searchTerm === "osmcha" || searchTerm === "stats") {
window.location.href = Utils.OsmChaLinkFor(7)
}
// Enter pressed -> search the first _official_ matchin theme and open it
const publicTheme = MoreScreen.officialThemes.find(
(th) =>
th.hideFromOverview == false &&
th.id !== "personal" &&
MoreScreen.MatchesLayout(th, searchTerm)
)
if (publicTheme !== undefined) {
window.location.href = MoreScreen.createUrlFor(publicTheme, false).data
}
const hiddenTheme = MoreScreen.officialThemes.find(
(th) => th.id !== "personal" && MoreScreen.MatchesLayout(th, searchTerm)
)
if (hiddenTheme !== undefined) {
window.location.href = MoreScreen.createUrlFor(hiddenTheme, false).data
}
const searchBar = new Combine([
Svg.search_svg().SetClass("w-8"),
search.SetClass("mr-4 w-full"),
]).SetClass("flex rounded-full border-2 border-black items-center my-2 w-1/2")
super([
new Combine([searchBar]).SetClass("flex justify-center"),
new SvelteUIElement(ThemesList, {
state,
onMainScreen,
search: search.GetValue(),
themes: MoreScreen.officialThemes,
}),
new SvelteUIElement(HiddenThemeList, {
state,
onMainScreen,
search: search.GetValue(),
}),
new SvelteUIElement(UnofficialThemeList, {
state,
onMainScreen,
search: search.GetValue(),
}),
tr.streetcomplete.Clone().SetClass("block text-base mx-10 my-3 mb-10"),
])
}
public static MatchesLayout(
@ -139,7 +78,7 @@ export default class MoreScreen extends Combine {
return false
}
private static createUrlFor(
public static createUrlFor(
layout: { id: string; definition?: string },
isCustom: boolean,
state?: { layoutToUse?: { id } }

View file

@ -4,7 +4,7 @@
import NextButton from "../Base/NextButton.svelte"
import Geosearch from "./Geosearch.svelte"
import ThemeViewState from "../../Models/ThemeViewState"
import { UIEventSource } from "../../Logic/UIEventSource"
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import { SearchIcon } from "@rgossiaux/svelte-heroicons/solid"
import { twJoin } from "tailwind-merge"
import { Utils } from "../../Utils"
@ -16,6 +16,7 @@
import Add from "../../assets/svg/Add.svelte"
import Location_refused from "../../assets/svg/Location_refused.svelte"
import Crosshair from "../../assets/svg/Crosshair.svelte"
import FromHtml from "../Base/FromHtml.svelte"
/**
* The theme introduction panel
@ -27,12 +28,12 @@
let triggerSearch: UIEventSource<any> = new UIEventSource<any>(undefined)
let searchEnabled = false
let geopermission: Readable<GeolocationPermissionState> =
let geopermission: Store<GeolocationPermissionState> =
state.geolocation.geolocationState.permission
let currentGPSLocation = state.geolocation.geolocationState.currentGPSLocation
geopermission.addCallback((perm) => console.log(">>>> Permission", perm))
function jumpToCurrentLocation() {
const glstate = state.geolocation.geolocationState
if (glstate.currentGPSLocation.data !== undefined) {
@ -57,8 +58,8 @@
<Tr t={Translations.t.general.welcomeExplanation.addNew} />
{/if}
<Tr t={layout.descriptionTail} />
<Tr t={layout.descriptionTail}/>
<!-- Buttons: open map, go to location, search -->
<NextButton clss="primary w-full" on:click={() => state.guistate.themeIsOpened.setData(false)}>
<div class="flex w-full justify-center text-2xl">
@ -110,8 +111,8 @@
<Geosearch
bounds={state.mapProperties.bounds}
on:searchCompleted={() => state.guistate.themeIsOpened.setData(false)}
on:searchIsValid={(isValid) => {
searchEnabled = isValid
on:searchIsValid={(event) => {
searchEnabled = event.detail
}}
perLayer={state.perLayer}
{selectedElement}

View file

@ -12,7 +12,6 @@
export let themes: LayoutInformation[]
export let state: { osmConnection: OsmConnection }
export let isCustom: boolean = false
export let onMainScreen: boolean = true
export let hideThemes: boolean = true
// Filter theme based on search value
@ -25,34 +24,25 @@
<section class="w-full">
<slot name="title" />
{#if onMainScreen}
<div class="gap-4 md:grid md:grid-flow-row md:grid-cols-2 lg:grid-cols-3">
{#each filteredThemes as theme (theme.id)}
{#if theme !== undefined && !(hideThemes && theme?.hideFromOverview)}
<!-- TODO: doesn't work if first theme is hidden -->
{#if theme === firstTheme && !isCustom && $search !== "" && $search !== undefined}
<ThemeButton
{theme}
{isCustom}
userDetails={state.osmConnection.userDetails}
{state}
selected={true}
/>
{:else}
<ThemeButton {theme} {isCustom} userDetails={state.osmConnection.userDetails} {state} />
{/if}
{/if}
{/each}
</div>
{:else}
<div>
{#each filteredThemes as theme (theme.id)}
{#if theme !== undefined && !(hideThemes && theme?.hideFromOverview)}
<div class="gap-4 md:grid md:grid-flow-row md:grid-cols-2 lg:grid-cols-3">
{#each filteredThemes as theme (theme.id)}
{#if theme !== undefined && !(hideThemes && theme?.hideFromOverview)}
<!-- TODO: doesn't work if first theme is hidden -->
{#if theme === firstTheme && !isCustom && $search !== "" && $search !== undefined}
<ThemeButton
{theme}
{isCustom}
userDetails={state.osmConnection.userDetails}
{state}
selected={true}
/>
{:else}
<ThemeButton {theme} {isCustom} userDetails={state.osmConnection.userDetails} {state} />
{/if}
{/each}
</div>
{/if}
{/if}
{/each}
</div>
{#if filteredThemes.length === 0}
<NoThemeResultButton {search} />

View file

@ -5,12 +5,12 @@
import ThemesList from "./ThemesList.svelte"
import Translations from "../i18n/Translations"
import UserRelatedState from "../../Logic/State/UserRelatedState"
import Tr from "../Base/Tr.svelte"
export let search: UIEventSource<string>
export let state: UserRelatedState & {
osmConnection: OsmConnection
}
export let onMainScreen: boolean = true
const t = Translations.t.general
const currentIds: Store<string[]> = state.installedUserThemes
@ -24,14 +24,15 @@
<ThemesList
{search}
{state}
{onMainScreen}
themes={customThemes}
isCustom={true}
hideThemes={false}
>
<svelte:fragment slot="title">
<!-- TODO: Change string to exclude html -->
{@html t.customThemeIntro.toString()}
<h3>
<Tr t={t.customThemeTitle} />
</h3>
<Tr t={t.customThemeIntro} />
</svelte:fragment>
</ThemesList>
{/if}

View file

@ -1,20 +1,17 @@
import { Translation } from "../UI/i18n/Translation"
export function ariaLabel(htmlElement: Element, t: Translation) {
let onDestroy: () => void = undefined
let destroy: () => void = undefined
t.current.map(
(label) => {
console.log("Setting arialabel", label, "to", htmlElement)
htmlElement.setAttribute("aria-label", label)
},
[],
(f) => {
onDestroy = f
destroy = f
}
)
return {
destroy() {},
}
return { destroy }
}

17
src/Utils/placeholder.ts Normal file
View file

@ -0,0 +1,17 @@
import { Translation } from "../UI/i18n/Translation"
export function placeholder(htmlElement: HTMLInputElement, t: Translation) {
let destroy: () => void = undefined
t.current.map(
(label) => {
htmlElement.setAttribute("placeholder", label)
},
[],
(f) => {
destroy = f
}
)
return { destroy }
}