From 8a7d8a43ce8c63672dddeebb9fbc1aec4226fda5 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Tue, 12 Dec 2023 19:18:50 +0100 Subject: [PATCH] Accessibility: add (translatable) aria labels, update to translation system, see #1181 --- langs/en.json | 2 +- public/css/index-tailwind-output.css | 12 ++-- src/Logic/UIEventSource.ts | 24 ++++++-- src/UI/Base/Dropdown.svelte | 1 - src/UI/Base/Tr.svelte | 24 ++------ src/UI/BigComponents/Geosearch.svelte | 18 +++++- src/UI/Image/NearbyImagesCollapsed.svelte | 1 + src/UI/InputElement/LanguagePicker.svelte | 67 +++++++++++++---------- src/UI/i18n/Locale.ts | 4 ++ src/UI/i18n/Translation.ts | 17 ++++++ src/Utils/ariaLabel.ts | 20 +++++++ src/index.css | 12 ++-- 12 files changed, 130 insertions(+), 72 deletions(-) create mode 100644 src/Utils/ariaLabel.ts diff --git a/langs/en.json b/langs/en.json index b722dd1d4..88316ed2b 100644 --- a/langs/en.json +++ b/langs/en.json @@ -289,7 +289,7 @@ "generatedWith": "Generated with mapcomplete.org/{layoutid}", "versionInfo": "v{version} - generated on {date}" }, - "pickLanguage": "Choose a language: ", + "pickLanguage": "Select language", "poweredByOsm": "Powered by OpenStreetMap", "questionBox": { "answeredMultiple": "You answered {answered} questions", diff --git a/public/css/index-tailwind-output.css b/public/css/index-tailwind-output.css index 6db9c8207..0da20a516 100644 --- a/public/css/index-tailwind-output.css +++ b/public/css/index-tailwind-output.css @@ -2469,7 +2469,7 @@ button.link:hover { fill: var(--foreground-color) !important; } -label { +label:not(.neutral-label) { /** * Label should _contain_ the input element */ @@ -2485,27 +2485,27 @@ label { transition: all 250ms; } -label:hover { +label:hover:not(.neutral-label) { background-color: var(--catch-detail-color); color: var(--catch-detail-foregroundcolor); border: 2px solid var(--interactive-contrast) } -label:not(.no-image-background) img { +label:not(.no-image-background):not(.neutral-label) img { padding: 0.25rem; border-radius: 0.25rem; background: var(--low-interaction-background); } -label svg path { +label:not(.neutral-label) svg path { transition: all 250ms; } -label:hover:not(.no-image-background) svg path { +label:hover:not(.no-image-background):not(.neutral-label) svg path { fill: var(--catch-detail-foregroundcolor) !important; } -label.checked { +label.checked:not(.neutral-label) { border: 2px solid var(--foreground-color); } diff --git a/src/Logic/UIEventSource.ts b/src/Logic/UIEventSource.ts index 658d21aff..f1321170e 100644 --- a/src/Logic/UIEventSource.ts +++ b/src/Logic/UIEventSource.ts @@ -96,7 +96,12 @@ export abstract class Store implements Readable { abstract map(f: (t: T) => J): Store abstract map(f: (t: T) => J, extraStoresToWatch: Store[]): Store - + abstract map( + f: (t: T) => J, + extraStoresToWatch: Store[], + callbackDestroyFunction: (f: () => void) => void + ): Store + M public mapD( f: (t: Exclude) => J, extraStoresToWatch?: Store[] @@ -329,9 +334,13 @@ export class ImmutableStore extends Store { return ImmutableStore.pass } - map(f: (t: T) => J, extraStores: Store[] = undefined): ImmutableStore { + map( + f: (t: T) => J, + extraStores: Store[] = undefined, + ondestroyCallback?: (f: () => void) => void + ): ImmutableStore { if (extraStores?.length > 0) { - return new MappedStore(this, f, extraStores, undefined, f(this.data)) + return new MappedStore(this, f, extraStores, undefined, f(this.data), ondestroyCallback) } return new ImmutableStore(f(this.data)) } @@ -463,7 +472,11 @@ class MappedStore extends Store { return this._data } - map(f: (t: T) => J, extraStores: Store[] = undefined): Store { + map( + f: (t: T) => J, + extraStores: Store[] = undefined, + ondestroyCallback?: (f: () => void) => void + ): Store { let stores: Store[] = undefined if (extraStores?.length > 0 || this._extraStores?.length > 0) { stores = [] @@ -483,7 +496,8 @@ class MappedStore extends Store { f, // we could fuse the functions here (e.g. data => f(this._f(data), but this might result in _f being calculated multiple times, breaking things stores, this._callbacks, - f(this.data) + f(this.data), + ondestroyCallback ) } diff --git a/src/UI/Base/Dropdown.svelte b/src/UI/Base/Dropdown.svelte index c5f30f8ce..347eaf61a 100644 --- a/src/UI/Base/Dropdown.svelte +++ b/src/UI/Base/Dropdown.svelte @@ -2,7 +2,6 @@ import { UIEventSource } from "../../Logic/UIEventSource.js" export let value: UIEventSource - let i: any = value.data let htmlElement: HTMLSelectElement function selectAppropriateValue() { if (!htmlElement) { diff --git a/src/UI/Base/Tr.svelte b/src/UI/Base/Tr.svelte index 2f1a80a7f..10ff04c65 100644 --- a/src/UI/Base/Tr.svelte +++ b/src/UI/Base/Tr.svelte @@ -3,36 +3,20 @@ * Properly renders a translation */ import { Translation } from "../i18n/Translation" - import { onDestroy } from "svelte" - import Locale from "../i18n/Locale" - import { Utils } from "../../Utils" - import FromHtml from "./FromHtml.svelte" import WeblateLink from "./WeblateLink.svelte" + import { Store } from "../../Logic/UIEventSource" export let t: Translation export let cls: string = "" - export let tags: Record | undefined = undefined // Text for the current language - let txt: string | undefined + let txt: Store = t.current + - $: onDestroy( - Locale.language.addCallbackAndRunD((l) => { - const translation = t?.textFor(l) - if (translation === undefined) { - return - } - if (tags) { - txt = Utils.SubstituteKeys(txt, tags) - } else { - txt = translation - } - }) - ) {#if t} - + {$txt} {/if} diff --git a/src/UI/BigComponents/Geosearch.svelte b/src/UI/BigComponents/Geosearch.svelte index 893d4ac68..452cb5dfc 100644 --- a/src/UI/BigComponents/Geosearch.svelte +++ b/src/UI/BigComponents/Geosearch.svelte @@ -1,5 +1,5 @@ {#if availableLanguages?.length > 1}
- - - {#if preferredFiltered} - {#each preferredFiltered as language} + + {/if} diff --git a/src/UI/i18n/Locale.ts b/src/UI/i18n/Locale.ts index 9c88ceb8c..b97044683 100644 --- a/src/UI/i18n/Locale.ts +++ b/src/UI/i18n/Locale.ts @@ -63,6 +63,10 @@ export default class Locale { source = LocalStorageSource.Get("language", browserLanguage) } + source.addCallbackAndRun((l) => { + document.documentElement.setAttribute("lang", l) + }) + if (!Utils.runningFromConsole) { // @ts-ignore window.setLanguage = function (language: string) { diff --git a/src/UI/i18n/Translation.ts b/src/UI/i18n/Translation.ts index e7511abb9..806ce0cee 100644 --- a/src/UI/i18n/Translation.ts +++ b/src/UI/i18n/Translation.ts @@ -2,6 +2,7 @@ import Locale from "./Locale" import { Utils } from "../../Utils" import BaseUIElement from "../BaseUIElement" import LinkToWeblate from "../Base/LinkToWeblate" +import { Store } from "../../Logic/UIEventSource" export class Translation extends BaseUIElement { public static forcedLanguage = undefined @@ -9,6 +10,9 @@ export class Translation extends BaseUIElement { public readonly translations: Record public readonly context?: string + private _current: Store + private onDestroy: () => void + constructor(translations: string | Record, context?: string) { super() if (translations === undefined) { @@ -66,6 +70,18 @@ export class Translation extends BaseUIElement { return this.textFor(Translation.forcedLanguage ?? Locale.language.data) } + get current(): Store { + if (!this._current) { + this._current = Locale.language.map( + (l) => this.textFor(l), + [], + (f) => { + this.onDestroy = f + } + ) + } + return this._current + } static ExtractAllTranslationsFrom( object: any, context = "" @@ -108,6 +124,7 @@ export class Translation extends BaseUIElement { Destroy() { super.Destroy() + this.onDestroy() this.isDestroyed = true } diff --git a/src/Utils/ariaLabel.ts b/src/Utils/ariaLabel.ts new file mode 100644 index 000000000..eacc0d4e6 --- /dev/null +++ b/src/Utils/ariaLabel.ts @@ -0,0 +1,20 @@ +import { Translation } from "../UI/i18n/Translation" + +export function ariaLabel(htmlElement: Element, t: Translation) { + let onDestroy: () => void = undefined + + t.current.map( + (label) => { + console.log("Setting arialabel", label, "to", htmlElement) + htmlElement.setAttribute("aria-label", label) + }, + [], + (f) => { + onDestroy = f + } + ) + + return { + destroy() {}, + } +} diff --git a/src/index.css b/src/index.css index 1815e8f2c..efd7af1ee 100644 --- a/src/index.css +++ b/src/index.css @@ -307,7 +307,7 @@ button.link:hover { } -label { +label:not(.neutral-label) { /** * Label should _contain_ the input element */ @@ -323,27 +323,27 @@ label { transition: all 250ms; } -label:hover { +label:hover:not(.neutral-label) { background-color: var(--catch-detail-color); color: var(--catch-detail-foregroundcolor); border: 2px solid var(--interactive-contrast) } -label:not(.no-image-background) img { +label:not(.no-image-background):not(.neutral-label) img { padding: 0.25rem; border-radius: 0.25rem; background: var(--low-interaction-background); } -label svg path { +label:not(.neutral-label) svg path { transition: all 250ms; } -label:hover:not(.no-image-background) svg path { +label:hover:not(.no-image-background):not(.neutral-label) svg path { fill: var(--catch-detail-foregroundcolor) !important; } -label.checked { +label.checked:not(.neutral-label) { border: 2px solid var(--foreground-color); }