diff options
| author | Roger Rehnelt <noreply@maennchen1.de> | 2026-10-01 20:53:24 +0200 |
|---|---|---|
| committer | Roger Rehnelt <noreply@maennchen1.de> | 2026-10-01 20:53:24 +0200 |
| commit | 85565a5938d61d00395272c4982022543025e239 (patch) | |
| tree | 8ad66e82afd5a69a5bf6cf664e06d21e1aa65b4d | |
| parent | 520d8ea12b8f97bc847131e0abd752e4bf94891a (diff) | |
Widerruf: Knöpfe und Dialog per Tastatur bedienbar
„Vertrag widerrufen“ ist überall ein echter Knopf; der Dialog ist modal, hält den Fokus, schließt mit Escape und gibt den Fokus zurück (K50).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4 files changed, 62 insertions, 24 deletions
diff --git a/views/mods/mod_revocation/wp_footer.phtml b/views/mods/mod_revocation/wp_footer.phtml index 767a264..4ca299a 100644 --- a/views/mods/mod_revocation/wp_footer.phtml +++ b/views/mods/mod_revocation/wp_footer.phtml @@ -15,10 +15,10 @@ <?php if (\wpsg_ShopController::getShop()->get_option('wpsg_mod_revocation_view_flyout') === '1') { ?> - <div id="wpsg_revocation_button"> - <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>Vertrag widerrufen</title><path d="M13.5,8H12V13L16.28,15.54L17,14.33L13.5,12.25V8M13,3A9,9 0 0,0 4,12H1L4.96,16.03L9,12H6A7,7 0 0,1 13,5A7,7 0 0,1 20,12A7,7 0 0,1 13,19C11.07,19 9.32,18.21 8.06,16.94L6.64,18.36C8.27,20 10.5,21 13,21A9,9 0 0,0 22,12A9,9 0 0,0 13,3" /></svg> + <button type="button" id="wpsg_revocation_button" aria-haspopup="dialog" aria-controls="wpsg_revocation_dialog"> + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M13.5,8H12V13L16.28,15.54L17,14.33L13.5,12.25V8M13,3A9,9 0 0,0 4,12H1L4.96,16.03L9,12H6A7,7 0 0,1 13,5A7,7 0 0,1 20,12A7,7 0 0,1 13,19C11.07,19 9.32,18.21 8.06,16.94L6.64,18.36C8.27,20 10.5,21 13,21A9,9 0 0,0 22,12A9,9 0 0,0 13,3" /></svg> <span id="wpsg_revocation_button_text"><?php echo esc_html__('Vertrag widerrufen', 'wpsg'); ?></span> - </div> + </button> <?php } else { ?> @@ -27,17 +27,17 @@ <?php } ?> <!-- Dialog --> -<div id="wpsg_revocation_dialog" class=""> - <div class="inner"> +<div id="wpsg_revocation_dialog" class="" aria-hidden="true" inert> + <div class="inner" role="dialog" aria-modal="true" aria-labelledby="wpsg_revocation_dialog_title" tabindex="-1"> <div class="header"> - <span><?php echo esc_html__('Widerrufsformular', 'wpsg'); ?></span> - <a href="#" class="close"> - <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z" /></svg> - </a> + <span id="wpsg_revocation_dialog_title"><?php echo esc_html__('Widerrufsformular', 'wpsg'); ?></span> + <button type="button" class="close" aria-label="<?php echo esc_attr__('Schließen', 'wpsg'); ?>"> + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z" /></svg> + </button> </div> <div class="content"></div> <div class="footer"> - <button class="button button-primary wpsg_revocation_button_submit"><?php echo esc_html__('Widerruf bestätigen', 'wpsg'); ?></button> + <button type="button" class="button button-primary wpsg_revocation_button_submit"><?php echo esc_html__('Widerruf bestätigen', 'wpsg'); ?></button> </div> <div class="loading"> <img src="<?php echo esc_url(WPSG_URL.'views/gfx/ajax-loader.gif'); ?>" alt="<?php echo esc_attr__('Bitte warten ...', 'wpsg'); ?>" /> @@ -179,18 +179,37 @@ }).catch(show_error); } + // Auslöser, auf den der Fokus nach dem Schließen zurückgeht (K50) + let trigger = null; + const close_dialog = event => { - event.preventDefault(); - event.stopPropagation(); + if (event) { + + event.preventDefault(); + event.stopPropagation(); + + } + + if (!el_dialog.classList.contains('visible')) return; el_dialog.classList.remove('visible'); + el_dialog.setAttribute('aria-hidden', 'true'); + el_dialog.inert = true; + + if (trigger && document.contains(trigger)) trigger.focus(); + trigger = null; }; const open_dialog = el => { - el_dialog.classList.toggle('visible'); + trigger = document.activeElement; + + el_dialog.inert = false; + el_dialog.removeAttribute('aria-hidden'); + el_dialog.classList.add('visible'); el_dialog.classList.add('loading'); + el_dialog.querySelector('.inner').focus(); const dialog_config = { arProductID: [], @@ -245,9 +264,11 @@ el_dialog_content.innerHTML = html; el_dialog.classList.remove('loading'); - }).catch(show_error); + // Erstes leeres Feld, sonst das erste Feld des Formulars + const fields = [ ...el_dialog_content.querySelectorAll('input:not([type=hidden]), select, textarea') ]; + ( fields.find(f => f.value === '') || fields[0] )?.focus(); - setTimeout(() => {document.querySelector('[name="wpsg_revocation_reason[row][0][product_nr]"]')?.focus();}, 1000) + }).catch(show_error); }; @@ -379,15 +400,28 @@ }) - // ESC Taste lauschen + // Escape schließt, Tab bleibt im Dialog (aria-modal) document.addEventListener('keydown', function (e) { + if (!el_dialog.classList.contains('visible')) return; + if (e.key === 'Escape') { - el_dialog.classList.remove('visible'); + close_dialog(e); + return; } + if (e.key !== 'Tab') return; + + const focusable = [ ...el_dialog.querySelectorAll('button, [href], input:not([type=hidden]), select, textarea, [tabindex]:not([tabindex="-1"])') ].filter(f => !f.disabled && f.offsetParent !== null); + if (focusable.length === 0) return; + + const first = focusable[0], last = focusable[focusable.length - 1]; + + if (e.shiftKey && (document.activeElement === first || document.activeElement === el_dialog.querySelector('.inner'))) { e.preventDefault(); last.focus(); } + else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); } + }); } )(); diff --git a/views/mods/mod_revocation/wpsg_mod_kundenverwaltung_order_after_products.phtml b/views/mods/mod_revocation/wpsg_mod_kundenverwaltung_order_after_products.phtml index 95dfa7f..5833e78 100644 --- a/views/mods/mod_revocation/wpsg_mod_kundenverwaltung_order_after_products.phtml +++ b/views/mods/mod_revocation/wpsg_mod_kundenverwaltung_order_after_products.phtml @@ -37,9 +37,10 @@ data-order_product_id="<?php echo $oOrderProduct->getId(); ?>" data-product_id="<?php echo $oOrderProduct->getProduct()->getId(); ?>" data-amount="<?php echo $amount; ?>" + aria-label="<?php echo \esc_attr(\wpsg_translate(\__('#1# für einen gemeinsamen Widerruf auswählen', 'wpsg'), $oOrderProduct->getProduct()->getProductName(true))); ?>" /> - <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>Vertrag widerrufen</title><path d="M13.5,8H12V13L16.28,15.54L17,14.33L13.5,12.25V8M13,3A9,9 0 0,0 4,12H1L4.96,16.03L9,12H6A7,7 0 0,1 13,5A7,7 0 0,1 20,12A7,7 0 0,1 13,19C11.07,19 9.32,18.21 8.06,16.94L6.64,18.36C8.27,20 10.5,21 13,21A9,9 0 0,0 22,12A9,9 0 0,0 13,3" /></svg> - <span><?php echo __('Vertrag widerrufen', 'wpsg'); ?></span> + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M13.5,8H12V13L16.28,15.54L17,14.33L13.5,12.25V8M13,3A9,9 0 0,0 4,12H1L4.96,16.03L9,12H6A7,7 0 0,1 13,5A7,7 0 0,1 20,12A7,7 0 0,1 13,19C11.07,19 9.32,18.21 8.06,16.94L6.64,18.36C8.27,20 10.5,21 13,21A9,9 0 0,0 22,12A9,9 0 0,0 13,3" /></svg> + <button type="button" class="wpsg_revocation_order_product_open" aria-haspopup="dialog" aria-controls="wpsg_revocation_dialog"><?php echo \esc_html__('Vertrag widerrufen', 'wpsg'); ?></button> </div> <?php } else { ?> diff --git a/views/mods/mod_revocation/wpsg_producttemplate_afterButton.phtml b/views/mods/mod_revocation/wpsg_producttemplate_afterButton.phtml index f6f3334..70a23d8 100644 --- a/views/mods/mod_revocation/wpsg_producttemplate_afterButton.phtml +++ b/views/mods/mod_revocation/wpsg_producttemplate_afterButton.phtml @@ -19,9 +19,9 @@ <?php if (\wpsg_ShopController::getShop()->get_option('wpsg_mod_revocation_view_product') === '1') { ?> - <div class="wpsg_revocation_product_button" data-product_anr="<?php echo \esc_attr((string)(\wpsg_ShopController::getShop()->view['data']['anr']??'')); ?>"> - <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true"><title>Vertrag widerrufen</title><path d="M13.5,8H12V13L16.28,15.54L17,14.33L13.5,12.25V8M13,3A9,9 0 0,0 4,12H1L4.96,16.03L9,12H6A7,7 0 0,1 13,5A7,7 0 0,1 20,12A7,7 0 0,1 13,19C11.07,19 9.32,18.21 8.06,16.94L6.64,18.36C8.27,20 10.5,21 13,21A9,9 0 0,0 22,12A9,9 0 0,0 13,3" /></svg> - <p class="wpsg_revocation_product_button_text"><?php echo \esc_html__('Vertrag widerrufen', 'wpsg'); ?></p> - </div> + <button type="button" class="wpsg_revocation_product_button" aria-haspopup="dialog" aria-controls="wpsg_revocation_dialog" data-product_anr="<?php echo \esc_attr((string)(\wpsg_ShopController::getShop()->view['data']['anr']??'')); ?>"> + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M13.5,8H12V13L16.28,15.54L17,14.33L13.5,12.25V8M13,3A9,9 0 0,0 4,12H1L4.96,16.03L9,12H6A7,7 0 0,1 13,5A7,7 0 0,1 20,12A7,7 0 0,1 13,19C11.07,19 9.32,18.21 8.06,16.94L6.64,18.36C8.27,20 10.5,21 13,21A9,9 0 0,0 22,12A9,9 0 0,0 13,3" /></svg> + <span class="wpsg_revocation_product_button_text"><?php echo \esc_html__('Vertrag widerrufen', 'wpsg'); ?></span> + </button> <?php } ?> diff --git a/views/mods/mod_revocation/wpsg_revocation.css b/views/mods/mod_revocation/wpsg_revocation.css index b74f0bb..dd16281 100644 --- a/views/mods/mod_revocation/wpsg_revocation.css +++ b/views/mods/mod_revocation/wpsg_revocation.css @@ -4,6 +4,9 @@ /* Button am Produkt (Produkttemplate, SmartCheckout) */ .wpsg_revocation_product_button { display:inline-flex; align-items:center; gap:0.4rem; width:auto; padding:0.35rem 0.7rem; border:1px solid var(--rv-line); border-radius:var(--rv-radius); background:none; color:inherit; font-size:0.85rem; line-height:1.2; cursor:pointer; user-select:none; transition:background-color 0.15s ease-out, border-color 0.15s ease-out; } +.wpsg_revocation_product_button, #wpsg_revocation_button, #wpsg_revocation_dialog .inner > .header .close, .wpsg_revocation_order_product_open { margin:0; font-family:inherit; text-align:left; -webkit-appearance:none; appearance:none; } +#wpsg_revocation_dialog .inner > .header .close, .wpsg_revocation_order_product_open { padding:0; border:0; background:none; } +.wpsg_revocation_order_product_open { color:inherit; font-size:inherit; line-height:inherit; cursor:pointer; text-decoration:underline; } .wpsg_revocation_product_button:hover { background:var(--rv-flat); border-color:currentColor; } .wpsg_revocation_product_button > svg { flex:0 0 16px; width:16px; height:16px; fill:currentColor; opacity:0.7; } /* Doppelte Klasse, damit Theme-Regeln wie ".entry-content p { margin-bottom:… }" nicht in den Button durchschlagen */ @@ -11,7 +14,7 @@ /* Flyout am rechten Bildschirmrand, im Ruhezustand ist nur das Symbol sichtbar */ #wpsg_revocation_button { position:fixed; right:0; top:25%; z-index:10; display:flex; align-items:center; gap:0.6rem; flex-wrap:nowrap; padding:0.6rem 0.9rem 0.6rem 0.6rem; border:1px solid var(--rv-line); border-right:0; border-radius:var(--rv-radius) 0 0 var(--rv-radius); background-color:#FFFFFF; color:#1d1d1d; font-size:0.9rem; box-shadow:0 2px 10px rgba(0, 0, 0, 0.08); cursor:pointer; transform:translateX(calc(100% - (26px + 1.2rem))); transition:transform 0.3s ease-out; } -#wpsg_revocation_button:hover, #wpsg_revocation_button:focus-within { transform:translateX(0); } +#wpsg_revocation_button:hover, #wpsg_revocation_button:focus-within, #wpsg_revocation_button:focus-visible { transform:translateX(0); } #wpsg_revocation_button svg { flex:0 0 26px; width:26px; height:26px; fill:currentColor; opacity:0.8; } #wpsg_revocation_button span { white-space:nowrap; } |
