forked from MapComplete/MapComplete
A11y: improve flow with screenreader, some more refactoring to svelte, see #1181
This commit is contained in:
parent
40067e35d4
commit
48ac539272
15 changed files with 188 additions and 226 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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 } }
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue