mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-09 23:12:06 -03:30
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:
parent
93791f67fb
commit
d05c3b5a9e
@ -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
|
||||
|
||||
@ -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>
|
||||
@ -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}">
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user