Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,21 @@ Changelog ist die Upgrade-Anleitung für die Tools.

## [Unreleased]

### @basicbar/ui (→ wird `ui/v0.7.1`)

**A11y-Fix `TranslatableField`** (#11): Der `hint` hatte mit
`text-slate-400` nur ~2,6:1 Kontrast und war nicht mit dem Feld verknüpft.
Jetzt `text-slate-600 dark:text-slate-300` (AA), und Hinweis,
Veraltet-Hinweis und Pflichtfeld-Fehler hängen per `aria-describedby` an der
Eingabe (Screenreader lesen sie mit dem Feld vor); Veraltet- und
Fehlertext ebenfalls auf AA-Kontrast (`amber-700`, `rose-600`).
`renderInput` bekommt dafür `describedBy`, `RichTextEditor` eine gleichnamige
Prop (auch zur Laufzeit aktualisiert).

Migration: Tarball auf `ui/v0.7.1`. Wer `renderInput` mit `RichTextEditor`
nutzt, reicht zusätzlich `describedBy={describedBy}` durch (sonst bleibt der
Editor ohne Beschreibung — nichts bricht).

### basicbar-integrations (→ wird `integrations/v0.3.0`)

**`CapabilitiesView` entfernt** (Framework-Review: kein Tool hat
Expand Down
8 changes: 6 additions & 2 deletions packages/ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,10 @@ die Maschinenübersetzung erhält die Tags). `renderInput` bekommt neben `id`
auch `labelId` — die Id von `TranslatableField`s eigenem sichtbaren `<label>`
(nur gesetzt, wenn die `label`-Prop übergeben wurde); durchgereicht als
`labelledBy` bindet der Editor sich per `aria-labelledby` an dieses Label,
statt ein zweites, redundantes `ariaLabel` zu brauchen. Auch `onBlur` wird
statt ein zweites, redundantes `ariaLabel` zu brauchen. Ebenso `describedBy`
(die Ids der gerade sichtbaren Hilfe-/Statustexte: `hint`, Veraltet-Hinweis,
Pflichtfeld-Fehler) — als `describedBy` durchgereicht setzt der Editor
`aria-describedby`, Screenreader lesen den Hinweis mit dem Feld vor. Auch `onBlur` wird
durchgereicht (die `onBlur`-Prop des Feldes), falls der eigene Editor ein
Blur-Speichern verdrahten will:

Expand All @@ -199,13 +202,14 @@ Blur-Speichern verdrahten will:
values={{ de: form.description_de, en: form.description_en }}
onChange={(lang, html) => setField(`description_${lang}`, html)}
format="html"
renderInput={({ value, onChange, id, labelId }) => (
renderInput={({ value, onChange, id, labelId, describedBy }) => (
<RichTextEditor
value={value}
onChange={onChange}
onUploadImage={uploadRichImage}
id={id}
labelledBy={labelId}
describedBy={describedBy}
/>
)}
/>
Expand Down
4 changes: 2 additions & 2 deletions packages/ui/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/ui/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@basicbar/ui",
"version": "0.7.0",
"version": "0.7.1",
"description": "Design-System-Basis der virtUOS -bar-Tools: Tailwind-Preset, Basis-Styles, Theme (Dark Mode), i18n-Bootstrap, contentLang, TranslatableField, Preferences, RichText; RichTextEditor (TipTap) als eigener Entry",
"license": "Apache-2.0",
"author": "Universität Osnabrück (virtUOS)",
Expand Down
23 changes: 23 additions & 0 deletions packages/ui/src/RichTextEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ export interface RichTextEditorProps {
* on the editable element); an alternative to `ariaLabel` when a visible
* label already exists (e.g. `TranslatableField`'s `labelId`). */
labelledBy?: string;
/** Ids of helper texts describing this editor (sets `aria-describedby`),
* e.g. `TranslatableField`'s `describedBy`. */
describedBy?: string;
id?: string;
/** `false` renders the content read-only in the editor's own frame — no
* toolbar, no caret, pasted/dropped files ignored — e.g. while a form is
Expand Down Expand Up @@ -109,6 +112,7 @@ export function RichTextEditor({
onUploadImage,
ariaLabel,
labelledBy,
describedBy,
id,
editable = true,
}: RichTextEditorProps) {
Expand Down Expand Up @@ -266,6 +270,25 @@ export function RichTextEditor({
if (editor && editor.isEditable !== editable) editor.setEditable(editable);
}, [editor, editable]);

// Not in `editorProps.attributes`: those are read once and re-applied by
// ProseMirror on every update, but the described-by set changes at runtime
// (e.g. a required-field error appearing), so it is synced directly.
useEffect(() => {
if (!editor) return;
const apply = () => {
// Before mount `editor.view` is a stub that throws on `dom`.
if (!editor.isInitialized || editor.isDestroyed) return;
const dom = editor.view.dom;
if (describedBy) dom.setAttribute("aria-describedby", describedBy);
else dom.removeAttribute("aria-describedby");
};
apply();
editor.on("create", apply);
return () => {
editor.off("create", apply);
};
}, [editor, describedBy]);

if (!editor) return null;

function setLink() {
Expand Down
26 changes: 22 additions & 4 deletions packages/ui/src/TranslatableField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ export interface RenderInputArgs {
* custom editor that has no native `<label htmlFor>` association of its
* own (`RichTextEditor`'s `labelledBy`). */
labelId?: string;
/** Space-separated ids of the field's helper/status texts (hint, outdated
* notice, required error), when any is shown — pass through as
* `aria-describedby` (`RichTextEditor`'s `describedBy`). */
describedBy?: string;
}

const DEFAULT_INPUT_CLASS =
Expand Down Expand Up @@ -239,6 +243,14 @@ export function TranslatableField({
const showTabs = !singleLanguage;
const tabsLabel = ariaLabel ?? label;
const labelId = label ? `${inputId}-label` : undefined;
// Helper/status texts are announced with the input (aria-describedby).
const hintId = `${baseId}-hint`;
const staleId = `${baseId}-stale`;
const requiredId = `${baseId}-required`;
const describedBy =
[hint && hintId, activeStale && staleId, canonicalEmpty && requiredId]
.filter(Boolean)
.join(" ") || undefined;

return (
<div className={`block text-xs text-slate-500 dark:text-slate-400 ${className}`}>
Expand Down Expand Up @@ -315,7 +327,7 @@ export function TranslatableField({
)}
<div className="mt-1">
{renderInput ? (
renderInput({ lang: active, value, onChange: set, onBlur, id: inputId, labelId })
renderInput({ lang: active, value, onChange: set, onBlur, id: inputId, labelId, describedBy })
) : multiline ? (
<textarea
id={inputId}
Expand All @@ -324,6 +336,7 @@ export function TranslatableField({
placeholder={placeholder}
onChange={(e) => set(e.target.value)}
onBlur={onBlur}
aria-describedby={describedBy}
className={inputClass}
/>
) : (
Expand All @@ -333,12 +346,13 @@ export function TranslatableField({
placeholder={placeholder}
onChange={(e) => set(e.target.value)}
onBlur={onBlur}
aria-describedby={describedBy}
className={inputClass}
/>
)}
</div>
{activeStale && (
<p className="mt-1 text-amber-600 dark:text-amber-400">
<p id={staleId} className="mt-1 text-amber-700 dark:text-amber-400">
{t("The other language was changed since this translation.")}
</p>
)}
Expand Down Expand Up @@ -369,9 +383,13 @@ export function TranslatableField({
</div>
)}
{translateError && <p className="mt-1 text-rose-500">{translateError}</p>}
{hint && <p className="mt-1 text-slate-400 dark:text-slate-500">{hint}</p>}
{hint && (
<p id={hintId} className="mt-1 text-slate-600 dark:text-slate-300">
{hint}
</p>
)}
{canonicalEmpty && (
<p className="mt-1 text-rose-500">
<p id={requiredId} className="mt-1 text-rose-600 dark:text-rose-400">
{t("A value in {{language}} is required.", {
language: defaultContentLangLabel(),
})}
Expand Down
Loading