From cd5ff195ea9ef2f609d726f71d503e4bed8150ce Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Thu, 3 Sep 2026 11:38:21 +0200 Subject: [PATCH 1/9] Make copy button style configurable Allow callers to override the button class and pass through inline styles, while keeping the existing primary-button default. The button content wrapper is also centred so the default and copied states stay aligned. --- .../Frontend/js/src/components/CopyToClipboardComponent.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Framework/Frontend/js/src/components/CopyToClipboardComponent.js b/Framework/Frontend/js/src/components/CopyToClipboardComponent.js index ccc4c898a..d4d03d65c 100644 --- a/Framework/Frontend/js/src/components/CopyToClipboardComponent.js +++ b/Framework/Frontend/js/src/components/CopyToClipboardComponent.js @@ -89,7 +89,7 @@ export class CopyToClipboardComponent extends StatefulComponent { */ view(vnode) { const { attrs, children } = vnode; - const { value: clipboardTargetValue = '', id } = attrs; + const { value: clipboardTargetValue = '', id, className = 'button.btn.btn-primary' } = attrs; let available = true; let message = ''; @@ -104,14 +104,15 @@ export class CopyToClipboardComponent extends StatefulComponent { const successContent = [iconCheck(), h('', 'Copied!')]; return h( - 'button.btn.btn-primary', + className, { id: `copy-${id}`, onclick: () => this.copyToClipboard(clipboardTargetValue), disabled: !available, title: message || null, + style: attrs.style, }, - h('div.flex-row.g1', this._successStateTimeout ? successContent : defaultContent), + h('div.flex-row.g1.justify-center', this._successStateTimeout ? successContent : defaultContent), ); } } From 6ca60ab0289230675b65b915917bc72377812abb Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Tue, 8 Sep 2026 09:40:04 +0200 Subject: [PATCH 2/9] Stealth fix to restart the success timer if clicked before completed Reset the success timer if clicked again otherwise the button text resets at unpredictable moments and user is left confused if the copy worked or not. --- .../Frontend/js/src/components/CopyToClipboardComponent.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Framework/Frontend/js/src/components/CopyToClipboardComponent.js b/Framework/Frontend/js/src/components/CopyToClipboardComponent.js index d4d03d65c..04e857bc5 100644 --- a/Framework/Frontend/js/src/components/CopyToClipboardComponent.js +++ b/Framework/Frontend/js/src/components/CopyToClipboardComponent.js @@ -36,6 +36,10 @@ export class CopyToClipboardComponent extends StatefulComponent { */ copyToClipboard(clipboardTargetValue) { navigator.clipboard.writeText(clipboardTargetValue); + if (this._successStateTimeout) { + clearTimeout(this._successStateTimeout); + } + this._successStateTimeout = setTimeout(() => { this._successStateTimeout = null; this.notify(); From 9dde5e5d27c8521fc932caa3c4589bfe5d3da853 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Tue, 8 Sep 2026 12:07:49 +0200 Subject: [PATCH 3/9] Simplify clipboard button styling Update `CopyToClipboardComponent` to accept a `classes` suffix instead of a full `className` selector, and build the button selector from a consistent `button.btn` base. --- .../Frontend/js/src/components/CopyToClipboardComponent.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Framework/Frontend/js/src/components/CopyToClipboardComponent.js b/Framework/Frontend/js/src/components/CopyToClipboardComponent.js index 04e857bc5..ef432b21c 100644 --- a/Framework/Frontend/js/src/components/CopyToClipboardComponent.js +++ b/Framework/Frontend/js/src/components/CopyToClipboardComponent.js @@ -93,7 +93,7 @@ export class CopyToClipboardComponent extends StatefulComponent { */ view(vnode) { const { attrs, children } = vnode; - const { value: clipboardTargetValue = '', id, className = 'button.btn.btn-primary' } = attrs; + const { value: clipboardTargetValue = '', id, classes = '.btn-primary' } = attrs; let available = true; let message = ''; @@ -108,7 +108,7 @@ export class CopyToClipboardComponent extends StatefulComponent { const successContent = [iconCheck(), h('', 'Copied!')]; return h( - className, + `button.btn${classes}`, { id: `copy-${id}`, onclick: () => this.copyToClipboard(clipboardTargetValue), From d5f81d6abde8d9a154aa7569a9e121e262fd0d16 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Tue, 8 Sep 2026 14:18:21 +0200 Subject: [PATCH 4/9] Improve clipboard button docs and accessibility Documented expected `vnode.attrs` fields in `CopyToClipboardComponent.view()` and added `ariaLive: 'polite'` to the status content container so copy success feedback is announced to assistive technologies. --- .../Frontend/js/src/components/CopyToClipboardComponent.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Framework/Frontend/js/src/components/CopyToClipboardComponent.js b/Framework/Frontend/js/src/components/CopyToClipboardComponent.js index ef432b21c..b3acdd83e 100644 --- a/Framework/Frontend/js/src/components/CopyToClipboardComponent.js +++ b/Framework/Frontend/js/src/components/CopyToClipboardComponent.js @@ -89,6 +89,11 @@ export class CopyToClipboardComponent extends StatefulComponent { * Renders the button that allows copying text to the clipboard. * * @param {vnode} vnode The virtual DOM node containing the attrs and children. + * @param {object} vnode.attrs The attributes passed to the component. + * @param {string} vnode.attrs.value The text to be copied to the clipboard. + * @param {string} vnode.attrs.id The unique identifier for the copy button will become 'copy-{id}'. + * @param {string} vnode.attrs.classes The CSS classes to be applied to the copy button. + * @param {string} vnode.attrs.style The inline styles to be applied to the copy button. * @returns {Component} The copyToClipboard button component */ view(vnode) { @@ -116,7 +121,7 @@ export class CopyToClipboardComponent extends StatefulComponent { title: message || null, style: attrs.style, }, - h('div.flex-row.g1.justify-center', this._successStateTimeout ? successContent : defaultContent), + h('div.flex-row.g1.justify-center', { ariaLive: 'polite' }, this._successStateTimeout ? successContent : defaultContent), ); } } From 47d02d9521344fed67f23b6fd2df6e6b8ed2a08d Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Tue, 8 Sep 2026 15:47:40 +0200 Subject: [PATCH 5/9] Fix CopyToClipboard button class handling --- .../Frontend/js/src/components/CopyToClipboardComponent.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Framework/Frontend/js/src/components/CopyToClipboardComponent.js b/Framework/Frontend/js/src/components/CopyToClipboardComponent.js index b3acdd83e..b340f01dd 100644 --- a/Framework/Frontend/js/src/components/CopyToClipboardComponent.js +++ b/Framework/Frontend/js/src/components/CopyToClipboardComponent.js @@ -98,7 +98,7 @@ export class CopyToClipboardComponent extends StatefulComponent { */ view(vnode) { const { attrs, children } = vnode; - const { value: clipboardTargetValue = '', id, classes = '.btn-primary' } = attrs; + const { value: clipboardTargetValue = '', id, className = 'btn-primary' } = attrs; let available = true; let message = ''; @@ -113,13 +113,14 @@ export class CopyToClipboardComponent extends StatefulComponent { const successContent = [iconCheck(), h('', 'Copied!')]; return h( - `button.btn${classes}`, + `button.btn`, { id: `copy-${id}`, onclick: () => this.copyToClipboard(clipboardTargetValue), disabled: !available, title: message || null, style: attrs.style, + className, }, h('div.flex-row.g1.justify-center', { ariaLive: 'polite' }, this._successStateTimeout ? successContent : defaultContent), ); From d909ce2fde7b4b933d825b1d93b33ba966baeaf4 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Tue, 8 Sep 2026 16:26:51 +0200 Subject: [PATCH 6/9] Handle clipboard copy failures Wrap clipboard writes in error handling and add an optional `onFailure` callback so callers can react when copying is unavailable or fails. The component also now destructures `style` explicitly. --- .../components/CopyToClipboardComponent.js | 31 ++++++++++++++----- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/Framework/Frontend/js/src/components/CopyToClipboardComponent.js b/Framework/Frontend/js/src/components/CopyToClipboardComponent.js index b340f01dd..ac2d9486c 100644 --- a/Framework/Frontend/js/src/components/CopyToClipboardComponent.js +++ b/Framework/Frontend/js/src/components/CopyToClipboardComponent.js @@ -32,12 +32,25 @@ export class CopyToClipboardComponent extends StatefulComponent { * Copies the specified text to the clipboard. * * @param {string} clipboardTargetValue The text to be copied to the clipboard. + * @param {function} onFailure The callback function to be invoked if copying to the clipboard fails. * @returns {void} */ - copyToClipboard(clipboardTargetValue) { - navigator.clipboard.writeText(clipboardTargetValue); - if (this._successStateTimeout) { - clearTimeout(this._successStateTimeout); + copyToClipboard(clipboardTargetValue, onFailure) { + try { + navigator.clipboard.writeText(clipboardTargetValue); + if (this._successStateTimeout) { + clearTimeout(this._successStateTimeout); + } + + this._successStateTimeout = setTimeout(() => { + this._successStateTimeout = null; + this.notify(); + }, 2000); + this.notify(); + } catch (error) { + if (onFailure) { + onFailure(error); + } } this._successStateTimeout = setTimeout(() => { @@ -94,11 +107,13 @@ export class CopyToClipboardComponent extends StatefulComponent { * @param {string} vnode.attrs.id The unique identifier for the copy button will become 'copy-{id}'. * @param {string} vnode.attrs.classes The CSS classes to be applied to the copy button. * @param {string} vnode.attrs.style The inline styles to be applied to the copy button. + * @param {function} vnode.attrs.onFailure The callback function to be invoked if copying to the clipboard fails. * @returns {Component} The copyToClipboard button component */ view(vnode) { const { attrs, children } = vnode; - const { value: clipboardTargetValue = '', id, className = 'btn-primary' } = attrs; + // Attributes other than those listed are not forwarded to the button element + const { value: clipboardTargetValue = '', id, className = 'btn-primary', style, onFailure } = attrs; let available = true; let message = ''; @@ -113,13 +128,13 @@ export class CopyToClipboardComponent extends StatefulComponent { const successContent = [iconCheck(), h('', 'Copied!')]; return h( - `button.btn`, + 'button.btn', { id: `copy-${id}`, - onclick: () => this.copyToClipboard(clipboardTargetValue), + onclick: () => this.copyToClipboard(clipboardTargetValue, onFailure), disabled: !available, title: message || null, - style: attrs.style, + style, className, }, h('div.flex-row.g1.justify-center', { ariaLive: 'polite' }, this._successStateTimeout ? successContent : defaultContent), From d80c80c774b4a62391df86614bf6cb339548b16b Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Tue, 8 Sep 2026 16:41:22 +0200 Subject: [PATCH 7/9] Fix copying/pasting error --- .../Frontend/js/src/components/CopyToClipboardComponent.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Framework/Frontend/js/src/components/CopyToClipboardComponent.js b/Framework/Frontend/js/src/components/CopyToClipboardComponent.js index ac2d9486c..27f6740ca 100644 --- a/Framework/Frontend/js/src/components/CopyToClipboardComponent.js +++ b/Framework/Frontend/js/src/components/CopyToClipboardComponent.js @@ -52,12 +52,6 @@ export class CopyToClipboardComponent extends StatefulComponent { onFailure(error); } } - - this._successStateTimeout = setTimeout(() => { - this._successStateTimeout = null; - this.notify(); - }, 2000); - this.notify(); } /** From f88d1ac99499ce1604ba94173e612a37011076f0 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Tue, 8 Sep 2026 16:41:49 +0200 Subject: [PATCH 8/9] Await clipboard writes in copy component Done so clipboard write failures are properly caught by the existing error handling path, instead of proceeding as if copy succeeded. --- .../Frontend/js/src/components/CopyToClipboardComponent.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Framework/Frontend/js/src/components/CopyToClipboardComponent.js b/Framework/Frontend/js/src/components/CopyToClipboardComponent.js index 27f6740ca..3966cc425 100644 --- a/Framework/Frontend/js/src/components/CopyToClipboardComponent.js +++ b/Framework/Frontend/js/src/components/CopyToClipboardComponent.js @@ -35,9 +35,9 @@ export class CopyToClipboardComponent extends StatefulComponent { * @param {function} onFailure The callback function to be invoked if copying to the clipboard fails. * @returns {void} */ - copyToClipboard(clipboardTargetValue, onFailure) { + async copyToClipboard(clipboardTargetValue, onFailure) { try { - navigator.clipboard.writeText(clipboardTargetValue); + await navigator.clipboard.writeText(clipboardTargetValue); if (this._successStateTimeout) { clearTimeout(this._successStateTimeout); } From fc6a43cffd6c74585bf120385755ad2f134fabc6 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Tue, 8 Sep 2026 17:00:40 +0200 Subject: [PATCH 9/9] Fix tooltip text when no disabled message --- .../Frontend/js/src/components/CopyToClipboardComponent.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Framework/Frontend/js/src/components/CopyToClipboardComponent.js b/Framework/Frontend/js/src/components/CopyToClipboardComponent.js index 3966cc425..ccc5d0ded 100644 --- a/Framework/Frontend/js/src/components/CopyToClipboardComponent.js +++ b/Framework/Frontend/js/src/components/CopyToClipboardComponent.js @@ -127,7 +127,7 @@ export class CopyToClipboardComponent extends StatefulComponent { id: `copy-${id}`, onclick: () => this.copyToClipboard(clipboardTargetValue, onFailure), disabled: !available, - title: message || null, + title: message || '', style, className, },