Add copy functionality to out-of-band code page

- Add clipboard copy functionality for oob code
- Use styling from official patternfly component
- Improved user experience with visual feedback on copy action

Closes #42094

Signed-off-by: Tim Menze <58325404+TimMnz09@users.noreply.github.com>
This commit is contained in:
Tim M 2025-09-10 00:04:23 +02:00 committed by GitHub
parent 93791f67fb
commit d05c3b5a9e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 154 additions and 2 deletions

View File

@ -53,6 +53,7 @@ emailUpdatedTitle=Email updated
emailUpdated=The account email has been successfully updated to {0}.
updatePasswordTitle=Update password
codeSuccessTitle=Success code
codeSuccess=Success code
codeErrorTitle=Error code\: {0}
displayUnsupported=Requested display type unsupported
browserRequired=Browser required to login
@ -522,6 +523,12 @@ idp-email-verification-help-text=Link your account by validating your email.
idp-username-password-form-display-name=Username and password
idp-username-password-form-help-text=Link your account by logging in.
# Code
code-clipboard-label=Show content
code-copy-label=Copy to clipboard
code-copy-success=Code copied to clipboard
code-copy-failure=Failed to copy code to clipboard
finalDeletionConfirmation=If you delete your account, it cannot be restored. To keep your account, click Cancel.
irreversibleAction=This action is irreversible
deleteAccountConfirm=Delete account confirmation

View File

@ -11,10 +11,86 @@
<div id="kc-code">
<#if code.success>
<p>${msg("copyCodeInstruction")}</p>
<@field.input name="code" label="" value=code.code />
<@field.clipboard name="code" label="" ariaLabel=msg("codeSuccess") value=code.code />
<script type="module">
(() => {
const copyButton = document.getElementById('kc-code-copy-button');
const toggleButton = document.getElementById('kc-code-toggle');
const input = document.getElementById('kc-code');
const expandableContent = document.getElementById('kc-code-content');
const clipboardContainer = document.getElementById('kc-code-clipboard');
// Validate elements
if (!copyButton || !toggleButton || !input || !expandableContent || !clipboardContainer) {
console.error("Missing required DOM elements for code interactions.");
return;
}
// Validate Clipboard API
if (!navigator.clipboard) {
console.error("Clipboard API not supported in this browser.");
copyButton.style.display = 'none';
return;
}
// Handle copy functionality
copyButton.addEventListener('click', async() => {
const originalIcon = copyButton.innerHTML;
// Get value from expandable content
let value = expandableContent.querySelector('code')?.textContent;
try {
await navigator.clipboard.writeText(value);
updateCopyButton(true, copyButton, originalIcon);
} catch (err) {
console.error('Copy failed: ', err);
updateCopyButton(false, copyButton, originalIcon);
}
});
// Handle toggle functionality
toggleButton.addEventListener('click', () => {
const newExpanded = !(toggleButton.getAttribute('aria-expanded') === 'true');
toggleButton.setAttribute('aria-expanded', newExpanded.toString());
expandableContent.hidden = !newExpanded;
// Update icon
const icon = toggleButton.querySelector('#kc-code-toggle-icon');
if (icon) {
icon.className = newExpanded ? toggleButton.dataset.iconExpandedClass : toggleButton.dataset.iconCollapsedClass;
}
// Update clipboard container class
if (newExpanded) {
clipboardContainer.classList.add(toggleButton.dataset.classExpanded);
} else {
clipboardContainer.classList.remove(toggleButton.dataset.classExpanded);
}
});
})();
// Update copy button to show success or failure state
function updateCopyButton(success, button, originalIcon) {
button.setAttribute('aria-label', success ? button.dataset.successLabel : button.dataset.failureLabel);
const icon = button.querySelector('#kc-code-copy-icon');
if (icon) {
icon.className = success ? button.dataset.iconSuccess : button.dataset.iconFailure;
}
button.disabled = true;
// Reset button after 2 seconds
setTimeout(() => {
button.innerHTML = originalIcon;
button.setAttribute('aria-label', '${msg("code-copy-label")}');
button.disabled = false;
}, 2000);
}
</script>
<#else>
<p id="error">${kcSanitize(code.error)}</p>
</#if>
</div>
</#if>
</@layout.registrationLayout>
</@layout.registrationLayout>

View File

@ -88,6 +88,61 @@
</@group>
</#macro>
<#macro clipboard name label ariaLabel=label value="" readonly=true>
<@group name=name label=label>
<div class="${properties.kcCodeClipboardCopyClass}" id="kc-${name}-clipboard">
<div class="${properties.kcCodeClipboardCopyGroupClass}">
<div class="${properties.kcInputGroup}">
<div class="${properties.kcInputGroupItemClass}">
<button
class="${properties.kcFormPasswordVisibilityButtonClass}"
type="button"
aria-label="${msg("code-clipboard-label")}"
aria-expanded="false"
aria-controls="kc-${name}-content"
data-icon-expanded-class="${properties.kcAngleDownIconClass}"
data-icon-collapsed-class="${properties.kcAngleRightIconClass}"
data-expanded-class="${properties.kcExpandedClass}"
id="kc-${name}-toggle"
>
<i id="kc-${name}-toggle-icon" class="${properties.kcAngleRightIconClass}" aria-hidden="true"></i>
</button>
</div>
<div class="${properties.kcInputGroupItemClass} ${properties.kcFill}">
<span class="${properties.kcInputClass} <#if readonly>${properties.kcFormReadOnlyClass}</#if>">
<input
id="${name}"
name="${name}"
value="${value}"
type="text"
<#if readonly>readonly</#if>
aria-label="${ariaLabel}"
/>
</span>
</div>
<div class="${properties.kcInputGroupItemClass}">
<button
class="${properties.kcFormPasswordVisibilityButtonClass}"
type="button"
aria-label="${msg("code-copy-label")}"
data-icon-success="${properties.kcCheckIconClass}"
data-icon-failure="${properties.kcInputErrorIconClass}"
data-success-label="${msg("code-copy-success")}"
data-failure-label="${msg("code-copy-failure")}"
id="kc-${name}-copy-button"
>
<i id="kc-${name}-copy-icon" class="${properties.kcCopyIconClass}" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
<div class="${properties.kcCodeClipboardCopyContentClass}" id="kc-${name}-content" hidden>
<pre><code aria-label="${ariaLabel}">${value}</code></pre>
</div>
</div>
</@group>
</#macro>
<#macro checkbox name label value=false required=false>
<div class="${properties.kcCheckboxClass}">
<label for="${name}" class="${properties.kcCheckboxClass}">

View File

@ -109,6 +109,10 @@ div.kc-logo-text span {
text-align: center;
}
#kc-code pre code {
word-break: break-all;
}
hr {
margin-top: var(--pf-v5-global--spacer--sm);
margin-bottom: var(--pf-v5-global--spacer--md);

View File

@ -101,6 +101,16 @@ kcLoginOTPListItemIconClass=fa fa-mobile
kcLoginOTPListItemTitleClass=pf-v5-c-tile__title
kcLoginOTPListSelectedClass=pf-m-selected
kcCopyIconClass=fas fa-copy
kcCheckIconClass=fas fa-check
kcAngleRightIconClass=fas fa-angle-right
kcAngleDownIconClass=fas fa-angle-down
kcExpandedClass=pf-m-expanded
kcCodeClipboardCopyClass=pf-v5-c-clipboard-copy
kcCodeClipboardCopyGroupClass=pf-v5-c-clipboard-copy__group
kcCodeClipboardCopyContentClass=pf-v5-c-clipboard-copy__expandable-content
kcDarkModeClass=pf-v5-theme-dark
kcHtmlClass=login-pf