Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82a74277cb | ||
|
|
5d55a65c22 |
No files matched your search
@@ -1,5 +1,15 @@
|
||||
# Projektstatus
|
||||
|
||||
## Fassung 1.22.4: Ziehknopf im Menü neben der Zusammenfassung (22.09.2026)
|
||||
|
||||
**Anlass:** Umut: „ja räum das auf und Release 1.22.4“ – die bekannte axe-Meldung `nested-interactive` im Menü-Editor.
|
||||
|
||||
- Der Ziehknopf eines Menüeintrags stand in der `<summary>`, also ein Knopf in einem Knopf. Screenreader lesen so etwas unzuverlässig vor, und die Tastatur erreicht es nicht sauber. Jetzt steht er als Geschwister vor `<details>`, per CSS über die linke Ecke der Zusammenfassung gelegt (`menu-entry.html.twig`, `createRow` in `menu-editor.js`). Aussehen und Bedienung bleiben gleich: Ziehen mit Maus, Stift und Finger, Pfeiltasten auf dem Knopf.
|
||||
- Der Hinweis zu fehlenden Sprungzielen im Menü und die Hilfe zum Feld „Sprungziel“ im Editor nennen jetzt die richtige Stelle (Sektion, Zeile oder Spalte) und den Weg über die Auswahl im Menü.
|
||||
- `test:admin`: Die Fehlermeldung nach einem abgewiesenen Speichern wurde direkt nach der Serverantwort abgefragt, sie erscheint aber erst, nachdem die Antwort gelesen ist. Jetzt wartet der Test auf sie. Der Check scheitert weiterhin an der älteren Stelle beim Klick in den Strukturbaum des Editors.
|
||||
|
||||
**Geprüft:** Die Editor-Prüfung prüft die ganze Menüseite mit axe, ohne Ausnahme. Sie stellt sicher, dass in keiner Zusammenfassung ein Knopf steht, und verschiebt einen Eintrag per Pfeiltaste (Fokus bleibt auf dem Knopf). Bildschirmfoto `reports/editor/menu-editor.png` angesehen.
|
||||
|
||||
## Fassung 1.22.3: die übrigen abgetippten Adressen (22.09.2026)
|
||||
|
||||
**Anlass:** Umut: „die offenen Punkte auch umsetzen und Release 1.22.3“ – die drei Reste aus 1.22.2.
|
||||
|
||||
@@ -998,3 +998,9 @@ details.card > summary:hover { background: #fafbf8 }
|
||||
.term-chip { border: 1px solid var(--line); background: #fff; color: var(--ink); border-radius: 999px; padding: 4px 11px; font-size: 12px; cursor: pointer }
|
||||
.term-chip[aria-pressed="true"] { background: #edf4dc; border-color: var(--accent); font-weight: 650 }
|
||||
.term-chip[aria-pressed="true"]::before { content: '✓ '; }
|
||||
/* Der Ziehknopf eines Menüeintrags liegt über der linken Ecke der Zusammenfassung, gehört aber
|
||||
nicht zu ihr - sonst stünde ein Knopf in einem Knopf. */
|
||||
.menu-entry { position: relative }
|
||||
.menu-entry > .menu-drag-handle { position: absolute; left: 14px; top: 10px; z-index: 1 }
|
||||
.menu-entry summary { padding-left: 58px; min-height: 60px }
|
||||
@media (max-width: 560px) { .menu-entry > .menu-drag-handle { left: 8px; top: 8px } .menu-entry summary { padding-left: 50px; min-height: 56px } }
|
||||
@@ -530,7 +530,7 @@ function renderContent(block, panel) {
|
||||
add.textContent = '+ Eintrag hinzufügen'; add.addEventListener('click', () => addAccordionEntry(block.id));
|
||||
panel.append(hint, entries, add);
|
||||
}
|
||||
if (['section', 'row', 'column'].includes(block.type)) field('Sprungziel', 'anchor', block.anchor, { anchor: true, maxLength: 64, help: 'Zum Beispiel kontakt eingeben. Buttons und Menülinks mit #kontakt springen dann hierher; von anderen Seiten mit /seitenadresse#kontakt.' });
|
||||
if (['section', 'row', 'column'].includes(block.type)) field('Sprungziel', 'anchor', block.anchor, { anchor: true, maxLength: 64, help: 'Zum Beispiel kontakt. Im Menü lässt es sich dann als Sprungziel dieser Seite auswählen, ein Button springt mit #kontakt hierher.' });
|
||||
if (block.type === 'accordionitem') {
|
||||
field('Titel', 'title', block.props.title, { maxLength: 160 });
|
||||
field('Beim Laden geöffnet', 'open', block.props.open, { checkbox: true, help: 'Gilt für die veröffentlichte Seite. Im Editor bleiben alle Einträge zum Bearbeiten geöffnet.' });
|
||||
|
||||
@@ -75,8 +75,9 @@ function createRow(data) {
|
||||
for (const key of ['position', 'level', 'type']) field(key, data[key] ?? 0, '', 'hidden');
|
||||
const details = el('details'); const summary = el('summary');
|
||||
const { handle, chevron } = chrome();
|
||||
summary.append(handle, el('strong', 'menu-entry-title', data.label), el('span', 'menu-entry-kind', data.type === 'page' ? 'Seitenlink' : 'Eigener Link'), chevron);
|
||||
const body = el('div', 'menu-entry-fields'); details.append(summary, body); row.append(details);
|
||||
// Der Ziehknopf steht neben der Zusammenfassung, nicht in ihr - kein Knopf in einem Knopf.
|
||||
summary.append(el('strong', 'menu-entry-title', data.label), el('span', 'menu-entry-kind', data.type === 'page' ? 'Seitenlink' : 'Eigener Link'), chevron);
|
||||
const body = el('div', 'menu-entry-fields'); details.append(summary, body); row.append(handle, details);
|
||||
const label = field('label', data.label, 'Beschriftung'); label.required = true; label.maxLength = 80;
|
||||
if (data.type === 'page') {
|
||||
const picker = form.querySelector('.menu-add .content-picker').cloneNode(true); delete picker.dataset.ready;
|
||||
@@ -136,7 +137,7 @@ function anchorChoice(row) {
|
||||
for (const item of items) select.append(new Option(item.label, item.anchor));
|
||||
if (current && !items.some(item => item.anchor === current)) select.append(new Option(`#${current} (auf der Seite nicht gefunden)`, current));
|
||||
select.value = current;
|
||||
help.textContent = items.length ? '' : 'Diese Seite hat keine Sprungziele. Im Editor bekommt eine Sektion unter „Erweitert“ einen Anker.';
|
||||
help.textContent = items.length ? '' : 'Diese Seite hat keine Sprungziele. Im Editor lässt sich einer Sektion, Zeile oder Spalte ein Sprungziel geben.';
|
||||
} catch {
|
||||
if (mine === request) help.textContent = 'Sprungziele konnten nicht geladen werden.';
|
||||
}
|
||||
|
||||
@@ -143,7 +143,7 @@ try {
|
||||
await page.getByRole('button', { name: 'Link hinzufügen', exact: true }).click();
|
||||
assert.equal((await menuSave()).status(), 422);
|
||||
assert.equal(await entries.last().locator('[data-key="url"]').inputValue(), 'javascript:alert(1)');
|
||||
assert.equal(await page.locator('#menu-error').isVisible(), true);
|
||||
await page.locator('#menu-error').waitFor({ state: 'visible' });
|
||||
await entries.last().locator('summary').click();
|
||||
await entries.last().getByLabel('Adresse', { exact: true }).fill('/#kontakt');
|
||||
assert.equal((await menuSave()).status(), 200);
|
||||
@@ -155,7 +155,7 @@ try {
|
||||
await page.getByLabel('Name des Menüs').fill('QA lokaler Stand');
|
||||
assert.equal((await menuSave()).status(), 409);
|
||||
assert.equal(await page.getByLabel('Name des Menüs').inputValue(), 'QA lokaler Stand');
|
||||
assert.equal(await page.locator('#menu-error').isVisible(), true);
|
||||
await page.locator('#menu-error').waitFor({ state: 'visible' });
|
||||
checks.push({ name: 'menu search by keyboard, add/nest/remove/undo/save, invalid URL and conflict retain input', passed: true });
|
||||
|
||||
await page.goto(origin + '/admin/forms');
|
||||
|
||||
@@ -1189,6 +1189,8 @@ try {
|
||||
await page.locator('dialog.page-dialog [role="option"]').first().click();
|
||||
await page.locator('dialog.page-dialog').waitFor({ state: 'detached' });
|
||||
assert.match(await page.locator('#link-url').inputValue(), /^\/[a-z0-9\-/]*$/);
|
||||
await page.locator('#link-label').fill('Eigener Prüflink');
|
||||
await page.locator('#menu-add-url').click();
|
||||
await page.locator('#menu-search').focus();
|
||||
await page.locator('#menu-search-results [role="option"]').first().click();
|
||||
await page.locator('#menu-add-page').click();
|
||||
@@ -1196,11 +1198,21 @@ try {
|
||||
const sprungziel = neuerEintrag.getByLabel('Sprungziel', { exact: true });
|
||||
assert.equal(await sprungziel.evaluate(node => node.tagName), 'SELECT', 'the anchor is a choice');
|
||||
assert.equal(await sprungziel.locator('option').first().textContent(), 'Seitenanfang');
|
||||
// Nur die neuen Felder: die Zusammenfassung der Menüeinträge trägt seit jeher einen Ziehknopf
|
||||
// (nested-interactive), das ist eine eigene, bekannte Baustelle.
|
||||
// Die ganze Seite: seit der Ziehknopf neben der Zusammenfassung steht, gibt es keinen Knopf
|
||||
// mehr in einem Knopf.
|
||||
await neuerEintrag.locator('summary').click();
|
||||
const menuAxe = await new AxeBuilder({ page }).include('#menu-items .menu-entry:last-child .menu-entry-fields').include('.menu-add').withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa']).analyze();
|
||||
assert.deepEqual(menuAxe.violations.map(v => v.id), [], 'anchor choice and link field');
|
||||
await axe('menu editor');
|
||||
await page.screenshot({ path: 'reports/editor/menu-editor.png' });
|
||||
assert.equal(await neuerEintrag.locator('summary [data-menu-drag]').count(), 0, 'no button inside the summary');
|
||||
// Ziehknopf per Tastatur: Pfeil nach oben verschiebt den Eintrag.
|
||||
const eintraege = page.locator('#menu-items .menu-entry');
|
||||
const anzahl = await eintraege.count();
|
||||
assert.ok(anzahl >= 2);
|
||||
const titel = await neuerEintrag.locator('.menu-entry-title').textContent();
|
||||
await eintraege.nth(anzahl - 1).locator('[data-menu-drag]').focus();
|
||||
await page.keyboard.press('ArrowUp');
|
||||
assert.equal(await eintraege.nth(anzahl - 2).locator('.menu-entry-title').textContent(), titel, 'the entry moved up');
|
||||
assert.equal(await eintraege.nth(anzahl - 2).locator('[data-menu-drag]').evaluate(node => node === document.activeElement), true, 'the handle keeps focus');
|
||||
checks.push({ name: 'menu: page search for own links, anchors as a choice', passed: true });
|
||||
// Beitrag: vorhandene Kategorien stehen zum Anklicken am Feld.
|
||||
await page.goto(origin + '/admin/posts');
|
||||
|
||||
@@ -2,8 +2,11 @@
|
||||
<input type="hidden" data-key="position" name="items[{{ n }}][position]" value="{{ n }}">
|
||||
<input type="hidden" data-key="level" name="items[{{ n }}][level]" value="{{ row.level }}">
|
||||
<input type="hidden" data-key="type" name="items[{{ n }}][type]" value="{{ row.type }}">
|
||||
{# Der Ziehknopf steht neben der Zusammenfassung, nicht in ihr: ein Knopf in einem Knopf ist
|
||||
für Screenreader und Tastatur nicht bedienbar (axe: nested-interactive). #}
|
||||
<button class="menu-drag-handle" type="button" data-menu-drag aria-label="{{ row.label }} verschieben" title="Ziehen · Pfeiltasten ändern Reihenfolge und Ebene"><svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true" fill="currentColor"><circle cx="8" cy="5" r="1.6"/><circle cx="16" cy="5" r="1.6"/><circle cx="8" cy="12" r="1.6"/><circle cx="16" cy="12" r="1.6"/><circle cx="8" cy="19" r="1.6"/><circle cx="16" cy="19" r="1.6"/></svg></button>
|
||||
<details>
|
||||
<summary><button class="menu-drag-handle" type="button" data-menu-drag aria-label="{{ row.label }} verschieben" title="Ziehen · Pfeiltasten ändern Reihenfolge und Ebene"><svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true" fill="currentColor"><circle cx="8" cy="5" r="1.6"/><circle cx="16" cy="5" r="1.6"/><circle cx="8" cy="12" r="1.6"/><circle cx="16" cy="12" r="1.6"/><circle cx="8" cy="19" r="1.6"/><circle cx="16" cy="19" r="1.6"/></svg></button><strong class="menu-entry-title">{{ row.label }}</strong><span class="menu-entry-kind">{{ row.type == 'page' ? 'Seitenlink' : 'Eigener Link' }}</span><svg class="menu-chevron" viewBox="0 0 24 24" width="20" height="20" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2"><path d="m6 9 6 6 6-6"/></svg></summary>
|
||||
<summary><strong class="menu-entry-title">{{ row.label }}</strong><span class="menu-entry-kind">{{ row.type == 'page' ? 'Seitenlink' : 'Eigener Link' }}</span><svg class="menu-chevron" viewBox="0 0 24 24" width="20" height="20" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2"><path d="m6 9 6 6 6-6"/></svg></summary>
|
||||
<div class="menu-entry-fields">
|
||||
<label for="menu-label-{{ n }}">Beschriftung</label><input id="menu-label-{{ n }}" data-key="label" name="items[{{ n }}][label]" maxlength="80" required value="{{ row.label }}">
|
||||
{% if row.type == 'page' %}
|
||||
|
||||
Reference in new issue
Block a user