diff --git a/scripts/workers-harness/shot.cjs b/scripts/workers-harness/shot.cjs
index 42f9e8b..516c377 100644
--- a/scripts/workers-harness/shot.cjs
+++ b/scripts/workers-harness/shot.cjs
@@ -1606,7 +1606,7 @@ async function handyP4(browser, base) {
const reach2 = await reachOf(page, '.wk-pult-reply textarea, .wk-pult-reply .wk-pult-panel-foot .btn')
const area = panel.locator('textarea')
const areaBox = await area.evaluate((el) => { const r = el.getBoundingClientRect(); return { left: Math.round(r.left), right: Math.round(r.right), height: Math.round(r.height) } })
- check('handy-p4: Antwortfeld: Textfeld und „Jetzt senden“ im Fenster, nicht verdeckt, Knopf mindestens 40 px hoch, Feld mindestens 100 px hoch', reach2.length === 2 && badReach(reach2).length === 0 && areaBox.left >= 0 && areaBox.right <= 390 && areaBox.height >= 100, { reach2, areaBox })
+ check('handy-p4: Antwortfeld: Textfeld, „Abbrechen“ und „Jetzt senden“ im Fenster, nicht verdeckt, Knöpfe mindestens 40 px hoch, Feld mindestens 100 px hoch', reach2.length === 3 && badReach(reach2).length === 0 && areaBox.left >= 0 && areaBox.right <= 390 && areaBox.height >= 100, { reach2, areaBox })
await panel.scrollIntoViewIfNeeded()
await shoot(page, 'handy-p4-antwort-offen.png')
await area.fill('x'.repeat(400) + ' https://www.sv-namborn.example/' + 'lang/'.repeat(30))
@@ -1676,7 +1676,7 @@ async function handyP4(browser, base) {
await setPultFault(page, 'paste', null)
await noScroll('beide Einfüge-Felder offen, 413 sichtbar')
const reach6 = await reachOf(page, '.wk-pult-paste textarea, .wk-pult-paste .wk-pult-panel-foot .btn, li.wk-pult-row .wk-pult-actions > button')
- check('handy-p4: Einfüge-Felder, „Einfügen“ und „Antwort des Kunden einfügen“ sind erreichbar (im Fenster, nicht verdeckt, Knöpfe mindestens 40 px hoch)', reach6.length === 6 && badReach(reach6).length === 0, { count: reach6.length, bad: badReach(reach6) })
+ check('handy-p4: Einfüge-Felder, „Abbrechen“, „Einfügen“ und „Antwort des Kunden einfügen“ sind erreichbar (im Fenster, nicht verdeckt, Knöpfe mindestens 40 px hoch)', reach6.length === 8 && badReach(reach6).length === 0, { count: reach6.length, bad: badReach(reach6) })
await plat.scrollIntoViewIfNeeded()
await shoot(page, 'handy-p4-einfuegen-413.png')
await shoot(page, 'handy-p4-einfuegen-ganz.png', { fullPage: true })
diff --git a/src/components/workers/PultList.jsx b/src/components/workers/PultList.jsx
index 3d0ce6b..cd191fc 100644
--- a/src/components/workers/PultList.jsx
+++ b/src/components/workers/PultList.jsx
@@ -47,16 +47,28 @@ function rowName(label, title, joiner = ': ') {
}
// Aufgeklapptes Feld unter den Aktionen: „Als Mensch antworten“ (.wk-pult-reply) und „Antwort des Kunden einfügen“ (.wk-pult-paste).
-// Textfeld, Zähler (Zeichen wie der Dienst sie zählt, höchstens REPLY_TEXT_MAX) und ein Knopf; die Prüfung macht die Zeile beim Absenden.
-function TextPanel({ className, label, hint, value, onChange, submitLabel, onSubmit, busy }) {
+// Textfeld, Zähler (Zeichen wie der Dienst sie zählt, höchstens REPLY_TEXT_MAX) und zwei Knöpfe; die Prüfung macht die Zeile beim Absenden.
+// Esc (aus dem ganzen Feld heraus, nicht nur aus dem Textfeld) und „Abbrechen“ schließen das Feld, der eingegebene Text bleibt in der Liste erhalten.
+// Während einer Anfrage ist beides gesperrt: das Feld soll nicht mitten im Senden verschwinden.
+function TextPanel({ className, label, hint, value, onChange, submitLabel, onSubmit, onClose, busy }) {
const count = charCount(value)
+ // preventDefault: die Handy-Leiste (App.jsx) schließt bei Esc nur, wenn das Ereignis nicht behandelt wurde
+ const onKeyDown = (e) => {
+ if (e.key !== 'Escape' || busy) return
+ e.preventDefault()
+ e.stopPropagation()
+ onClose()
+ }
return (
-
+
)
@@ -72,6 +84,7 @@ function PultRow({ api, item, row, text, fields, onText, onField, refresh, onFin
const [note, setNote] = useState(null) // { kind: 'ok' | 'err', text }
const busyRef = useRef(false) // sperrt schon im selben Takt, in dem busy noch nicht greift (Doppelklick)
const aliveRef = useRef(true)
+ const actionsRef = useRef(null) // die Aktionsgruppe: darin sucht close() den Knopf, der ein Feld geöffnet hat
useEffect(() => {
aliveRef.current = true
@@ -173,6 +186,12 @@ function PultRow({ api, item, row, text, fields, onText, onField, refresh, onFin
const replyValue = fields.reply ?? text
const pasteValue = fields.paste ?? ''
const toggle = (which) => setOpen((current) => (current === which ? null : which))
+ // Feld schließen (Esc, „Abbrechen“) und den Fokus an den Knopf zurückgeben, der es geöffnet hat: der bleibt eingehängt, der Fokus kann sofort dorthin.
+ // Der Knopf trägt seine Kennung in data-opens (Button reicht sie weiter; ein ref ginge an Button vorbei).
+ const close = (which) => {
+ setOpen(null)
+ actionsRef.current?.querySelector(`[data-opens="${which}"]`)?.focus()
+ }
const sendReply = () => {
const check = validateReplyText(replyValue)
@@ -254,7 +273,7 @@ function PultRow({ api, item, row, text, fields, onText, onField, refresh, onFin
)}
{hasActions && (
-
+
{row.canSend && row.kind === 'mail_antwort' && (
mail
?
E-Mail schreiben
@@ -276,7 +295,7 @@ function PultRow({ api, item, row, text, fields, onText, onField, refresh, onFin
)}
{row.canReply && (
-