UX: usertest + fix some surfaced issues, see #2108

This commit is contained in:
Pieter Vander Vennet 2025-09-14 01:50:59 +02:00
parent 45481a879d
commit f5e71f2f1c
9 changed files with 128 additions and 5 deletions

View file

@ -0,0 +1,49 @@
User test Paul
Tech Skills: High, low OSM knowledge
Demography: M, ~22
Language: NL
MapComplete App (Google-Play versie)
No predetermined goal to achieve
- Wilt taalfout fixen: ommetoer via 3 login systemen....
Out of scope voor beginners
- Loginscherm is wit:
Chrome openen ernaast
Technische bug, maar hoe te fixen?
- Toiletten: vragen over databron
- Hackerspace kaart: ik heb twee symbolen, wat is het verschil? Ik heb geen legende?
-> Achtergrondkaart selectie? Daar legende?
Aanklikken symbooltjes: softwaregericht / makerspace... Ooohhhh
- OK, is duidelijk
- "Geen idee wat panoramax is" > Open Panoramax/Mapillary hier
Geen idee wat dit is
Heeft wat meer info nodig (!)
"Allemaal links naar andere maps..."
- Zoekfunctie: de dingen staan in de weg
-> Ik kan op terug klikken. Ja nee, dat dacht ik al
-> Terug terug gaat niet terug
-> "Gouda". Ja nee, dat is niet handig...
Ik wil een ander thema... Ah ja
-> 'Active filters' staat in de weg - max height: 1/2 height
-> "Clear text"-knop staat permanent aan, opvolgtest: niet gevonden
- Kompas: "ooh, het wijzertje is mijn kompas!" -> Popup tonen met "wijst nu naar je kompas"
Fixed by adding popovers
- Trees: icoontje ontbreekt in app
Niemand heeft onze boom toegevoegd! Toevoegen
"Ja, het is een loofboom"
Locatie toevoegen gaat vlot
Voeg toe! Gaat vlot
Foto toevoegen: gaat ook vlot
Plantnet-detectie: gaat vlot, maar niet de juiste boomsoort
"Oh, ik kon er gewoon op klikken" -> duidelijk maken voor preview
- Waarom staan restaurants los van cafés?
OSM-keuze

View file

@ -32,6 +32,17 @@
"intro": "Get in touch with other people to get to know them, learn from them, …", "intro": "Get in touch with other people to get to know them, learn from them, …",
"title": "Get in touch with others" "title": "Get in touch with others"
}, },
"compass": {
"E": "Map has east pointing up",
"N": "Map has north pointing up",
"NE": "Map has northeast pointing up",
"NW": "Map has northwest pointing up",
"S": "Map has south pointing up",
"SE": "Map has southeast pointing up",
"SW": "Map has southwest pointing up",
"W": "Map has west pointing up",
"toCompass": "The map is now oriented to the device orientation"
},
"copy": { "copy": {
"button": "Create a copy", "button": "Create a copy",
"confirm": "Create copy at the specified location", "confirm": "Create copy at the specified location",

View file

@ -1970,6 +1970,10 @@ input[type="range"].range-lg::-moz-range-thumb {
max-height: 16rem; max-height: 16rem;
} }
.max-h-\[30vh\] {
max-height: 30vh;
}
.max-h-full { .max-h-full {
max-height: 100%; max-height: 100%;
} }
@ -5346,6 +5350,13 @@ input[type="text"] {
* This very important section defines what the various input elements look like within the 'low-interaction' and 'interactive'-blocks * This very important section defines what the various input elements look like within the 'low-interaction' and 'interactive'-blocks
*/ */
[type="search"]::-webkit-search-cancel-button,
[type="search"]::-webkit-search-decoration {
/* Disable the 'clear text field' button in chrome */
-webkit-appearance: none;
appearance: none;
}
/********* BUTTONS ***********/ /********* BUTTONS ***********/
button, .button { button, .button {

View file

@ -1,4 +1,8 @@
<div class="sidebar-unit"> <script lang="ts">
export let clss = ""
</script>
<div class={"sidebar-unit "+clss}>
<slot /> <slot />
</div> </div>

View file

@ -7,6 +7,12 @@
import Compass_back from "../../assets/svg/Compass_back.svelte" import Compass_back from "../../assets/svg/Compass_back.svelte"
import Compass_needle from "../../assets/svg/Compass_needle.svelte" import Compass_needle from "../../assets/svg/Compass_needle.svelte"
import North_arrow from "../../assets/svg/North_arrow.svelte" import North_arrow from "../../assets/svg/North_arrow.svelte"
import { Popover } from "flowbite-svelte"
import { Translation, TypedTranslation } from "../i18n/Translation"
import TrDyn from "../Base/TrDyn.svelte"
import Translations from "../i18n/Translations"
import Tr from "../Base/Tr.svelte"
import { GeoOperations } from "../../Logic/GeoOperations"
export let mapProperties: { rotation: UIEventSource<number>; allowRotating: Store<boolean> } export let mapProperties: { rotation: UIEventSource<number>; allowRotating: Store<boolean> }
let orientation = Orientation.singleton.alpha let orientation = Orientation.singleton.alpha
@ -17,24 +23,48 @@
} }
}) })
let mapRotation = mapProperties.rotation let mapRotation = mapProperties.rotation
let showHint = new UIEventSource(false)
showHint.stabilized(4000).addCallback(isShown => {
if (isShown) {
showHint.set(false)
}
})
let hovered = new UIEventSource(false)
let focused = new UIEventSource(false)
let hint: UIEventSource<Translation> = new UIEventSource(undefined)
export let size = "h-10 w-10" export let size = "h-10 w-10"
let wrapperClass = "absolute top-0 left-0 " + size let wrapperClass = "absolute top-0 left-0 " + size
let compassLoaded = Orientation.singleton.gotMeasurement let compassLoaded = Orientation.singleton.gotMeasurement
let allowRotation = mapProperties.allowRotating let allowRotation = mapProperties.allowRotating
function clicked() { function clicked() {
showHint.set(true)
if (mapProperties.rotation.data === 0) { if (mapProperties.rotation.data === 0) {
if (mapProperties.allowRotating.data && compassLoaded.data) { if (mapProperties.allowRotating.data && compassLoaded.data) {
mapProperties.rotation.set(orientation.data) mapProperties.rotation.set(orientation.data)
hint.set(Translations.t.compass.toCompass)
} }
} else { } else {
mapProperties.rotation.set(0) mapProperties.rotation.set(0)
hint.set(undefined)
} }
} }
let orientationText = mapProperties.rotation.mapD(r => {
const key = GeoOperations.bearingToHuman(r)
return Translations.t.compass[key]
})
let deviceOrientation = Orientation.singleton.alpha
Orientation.singleton.fakeMeasurements() // TODO Remove
</script> </script>
{#if $allowRotation || $gotNonZero} {#if $allowRotation || $gotNonZero}
<button class={"as-link pointer-events-auto relative " + size} on:click={() => clicked()}> <button class={"as-link pointer-events-auto relative " + size} on:click={() => clicked()}
on:mouseenter={() => hovered.set(true)} on:mouseleave={() => hovered.set(false)}
on:focus={() => focused.set(true)} on:blur={() => focused.set(false)}>
{#if $allowRotation && !$compassLoaded && !$gotNonZero} {#if $allowRotation && !$compassLoaded && !$gotNonZero}
<div <div
class={"rounded-full border-2 border-dotted border-gray-500 " + wrapperClass} class={"rounded-full border-2 border-dotted border-gray-500 " + wrapperClass}
@ -61,4 +91,14 @@
{/if} {/if}
{/if} {/if}
</button> </button>
<Popover placement="right" open={$showHint || $hovered || $focused} trigger="null">
<div class="flex flex-col">
{#if $hint !== undefined}
<Tr t={$hint} />
{:else if Math.abs($deviceOrientation - (($mapRotation + 360) % 360)) <= 45}
<Tr t={Translations.t.compass.toCompass} />
{/if}
<Tr t={$orientationText} />
</div>
</Popover>
{/if} {/if}

View file

@ -79,6 +79,7 @@
let featureSwitches = state.featureSwitches let featureSwitches = state.featureSwitches
let showHome = featureSwitches?.featureSwitchBackToThemeOverview ?? new ImmutableStore(true) let showHome = featureSwitches?.featureSwitchBackToThemeOverview ?? new ImmutableStore(true)
let allowLogin = featureSwitches?.featureSwitchEnableLogin ?? new ImmutableStore(true)
let pg = state.guistate.pageStates let pg = state.guistate.pageStates
let pendingChanges = state?.changes?.pendingChanges let pendingChanges = state?.changes?.pendingChanges
export let onlyLink: boolean export let onlyLink: boolean
@ -119,7 +120,7 @@
<div class="flex flex-col gap-y-2 overflow-y-auto px-2 sm:gap-y-3 sm:px-3"> <div class="flex flex-col gap-y-2 overflow-y-auto px-2 sm:gap-y-3 sm:px-3">
{#if $showHome || $isAndroid} {#if $showHome || $isAndroid}
<a class="button flex" class:primary={$loggedIn} href={Utils.HomepageLink()}> <a class="button flex" class:primary={$loggedIn || $allowLogin} href={Utils.HomepageLink()}>
<Squares2x2 class="h-10 w-10" /> <Squares2x2 class="h-10 w-10" />
{#if Utils.isIframe} {#if Utils.isIframe}
<Tr t={Translations.t.general.seeIndex} /> <Tr t={Translations.t.general.seeIndex} />

View file

@ -17,6 +17,7 @@
import { WithSearchState } from "../../Models/ThemeViewState/WithSearchState" import { WithSearchState } from "../../Models/ThemeViewState/WithSearchState"
export let activeFilters: (FilterSearchResult & ActiveFilter)[] export let activeFilters: (FilterSearchResult & ActiveFilter)[]
export let clss = ""
let language = Locale.language let language = Locale.language
let mergedActiveFilters = FilterSearch.mergeSemiIdenticalLayers(activeFilters, $language) let mergedActiveFilters = FilterSearch.mergeSemiIdenticalLayers(activeFilters, $language)
$: mergedActiveFilters = FilterSearch.mergeSemiIdenticalLayers(activeFilters, $language) $: mergedActiveFilters = FilterSearch.mergeSemiIdenticalLayers(activeFilters, $language)
@ -54,7 +55,7 @@
</script> </script>
{#if mergedActiveFilters.length > 0 || $nonactiveLayers.length > 0} {#if mergedActiveFilters.length > 0 || $nonactiveLayers.length > 0}
<SidebarUnit> <SidebarUnit {clss}>
<div class="flex justify-between"> <div class="flex justify-between">
<h3><Tr t={t.activeFilters} /></h3> <h3><Tr t={t.activeFilters} /></h3>

View file

@ -49,7 +49,7 @@
<div class="low-interaction flex flex-col gap-y-2 p-4"> <div class="low-interaction flex flex-col gap-y-2 p-4">
{#if $allowFilters} {#if $allowFilters}
<ActiveFilters {state} activeFilters={$activeFilters} /> <ActiveFilters clss={"flex-shrink "+ ($searchTerm.length > 0 ? "max-h-[30vh]" : "")} {state} activeFilters={$activeFilters} />
{/if} {/if}
{#if $searchTerm.length === 0 && $activeFilters.length === 0} {#if $searchTerm.length === 0 && $activeFilters.length === 0}
<div class="items-center p-8 text-center"> <div class="items-center p-8 text-center">

View file

@ -186,6 +186,12 @@ input[type="text"] {
* This very important section defines what the various input elements look like within the 'low-interaction' and 'interactive'-blocks * This very important section defines what the various input elements look like within the 'low-interaction' and 'interactive'-blocks
*/ */
[type="search"]::-webkit-search-cancel-button,
[type="search"]::-webkit-search-decoration {
/* Disable the 'clear text field' button in chrome */
-webkit-appearance: none;
appearance: none;
}
/********* BUTTONS ***********/ /********* BUTTONS ***********/
button, .button { button, .button {