From 6572cbe8d7618047e6c1cd0e8dda8a2a9668a696 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 7e5ac83daad4dbbc3efa29a82730b7291e7baabb 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 c2bd27a1652941b80c12d4407dab815845a33d2a 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 b3519e4eb14c72ea5a47186ff655193a38fa201f 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 b78d15f1986a58205d45ba6d984bf39a2af0a034 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 18a59e8b990c5030fc99475cafe99276b3ec16f2 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 f3bacd76f78163551ee0ab0bb3f5308467f84cae 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 2ac4e5634285ce48eeed017cdf541977b5ecc47d 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 286ef4ef5faf54653a8facf4733de727747871aa 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, },