From 285b94779f25370a2ff02d9854aefbaaf1110029 Mon Sep 17 00:00:00 2001 From: Aleksey Solovyev Date: Tue, 29 Nov 2022 14:07:20 +0300 Subject: [PATCH] feat: add reusable copy button component --- src/backend/views/components/copy-button.twig | 25 +++++ src/frontend/js/modules/page.js | 39 ++++++- .../styles/components/copy-button.pcss | 103 ++++++++++++++++++ src/frontend/styles/main.pcss | 1 + 4 files changed, 166 insertions(+), 2 deletions(-) create mode 100644 src/backend/views/components/copy-button.twig create mode 100644 src/frontend/styles/components/copy-button.pcss diff --git a/src/backend/views/components/copy-button.twig b/src/backend/views/components/copy-button.twig new file mode 100644 index 0000000..2db7ee1 --- /dev/null +++ b/src/backend/views/components/copy-button.twig @@ -0,0 +1,25 @@ +{# + Reusable copy button component. + Available props: + - ariaLabel: label for better accessibility + - class: additional class for the button + - textToCopy: text to be copied to the clipboard (use '#' for anchor links) + + Usage examples: + {% include 'components/copy-button.twig' with { textToCopy: 'Lorem ipsum dolor' } %} + {% include 'components/copy-button.twig' with { textToCopy: '#anchor-link-dolor' } %} +#} + +{% set attrNameForTextToCopy = 'data-text-to-copy' %} + +{% set ariaLabel = ariaLabel ?? 'Copy to the Clipboard' %} + +{% set mainTag = 'button' %} +{% set mainClass = 'copy-button' %} + +<{{ mainTag }} class="{{ mainClass }} {{ class ?? '' }}" aria-label="{{ ariaLabel }}" {{ attrNameForTextToCopy }}="{{ textToCopy }}"> +
+
{{ svg('copy') }}
+
{{ svg('check') }}
+
+ diff --git a/src/frontend/js/modules/page.js b/src/frontend/js/modules/page.js index 148b3e6..126b5fd 100644 --- a/src/frontend/js/modules/page.js +++ b/src/frontend/js/modules/page.js @@ -20,7 +20,9 @@ export default class Page { */ static get CSS() { return { - page: 'page' + copyButton: 'copy-button', + copyButtonCopied: 'copy-button__copied', + page: 'page', }; } @@ -36,7 +38,11 @@ export default class Page { */ const page = document.querySelector(`.${Page.CSS.page}`); - page.addEventListener('click', (event) => { }); + page.addEventListener('click', (event) => { + if (event.target.classList.contains(Page.CSS.copyButton)) { + this.handleCopyButtonClickEvent(event); + } + }); } /** @@ -73,4 +79,33 @@ export default class Page { console.error(error); // @todo send to Hawk } } + + /** + * Handles copy button click events + * + * @param {Event} e - Event Object. + * @returns {Promise} + */ + async handleCopyButtonClickEvent({ target }) { + if (target.classList.contains(Page.CSS.copyButtonCopied)) return; + + let textToCopy = target.getAttribute('data-text-to-copy'); + if (!textToCopy) return; + + // Check if text to copy is an anchor link + if (/^#\S*$/.test(textToCopy)) + textToCopy = window.location.origin + window.location.pathname + textToCopy; + + try { + await copyToClipboard(textToCopy); + + target.classList.add(Page.CSS.copyButtonCopied); + target.addEventListener('mouseleave', () => { + setTimeout(() => target.classList.remove(Page.CSS.copyButtonCopied), 5e2); + }, { once: true }); + + } catch (error) { + console.error(error); // @todo send to Hawk + } + } } diff --git a/src/frontend/styles/components/copy-button.pcss b/src/frontend/styles/components/copy-button.pcss new file mode 100644 index 0000000..2ec2e42 --- /dev/null +++ b/src/frontend/styles/components/copy-button.pcss @@ -0,0 +1,103 @@ +.copy-button { + position: relative; + width: 28px; + height: 28px; + padding: 0; + border: none; + background: none; + cursor: pointer; + transition: opacity 200ms; + + @media (--can-hover) { + &:hover .copy-button__inner { + background: var(--color-link-hover); + } + } + + &::before { + content: ''; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + border-radius: 100%; + background-color: var(--color-success); + visibility: hidden; + pointer-events: none; + transform: scale(1); + transition: transform 400ms ease-out, opacity 400ms; + } + + &__inner { + @apply --squircle; + + display: flex; + justify-content: center; + align-items: center; + width: 100%; + height: 100%; + background: white; + pointer-events: none; + } + + &__icon--initial { + display: flex; + transform: translateZ(0); + } + + &__icon--success { + display: none; + width: 24px; + height: 24px; + color: white; + } + + &__copied { + + &::before { + opacity: 0; + visibility: visible; + transform: scale(3.5); + } + + .copy-button__inner, + .copy-button__inner:hover { + background: var(--color-success) !important; + animation: check-square-in 250ms ease-in; + } + + .copy-button__icon--initial { + display: none; + } + + .copy-button__icon--success { + display: flex; + animation: check-sign-in 350ms ease-in forwards; + } + } + + @keyframes check-sign-in { + from { + transform: scale(.7); + } + 80% { + transform: scale(1.1); + } + to { + transform: none; + } + } + + @keyframes check-square-in { + from { + transform: scale(1.05); + } + 80% { + transform: scale(.96); + } + to { + transform: none; + } + } +} diff --git a/src/frontend/styles/main.pcss b/src/frontend/styles/main.pcss index c436ade..d6edcbf 100644 --- a/src/frontend/styles/main.pcss +++ b/src/frontend/styles/main.pcss @@ -5,6 +5,7 @@ @import './carbon.pcss'; @import './components/header.pcss'; @import './components/writing.pcss'; +@import './components/copy-button.pcss'; @import './components/page.pcss'; @import './components/greeting.pcss'; @import './components/auth.pcss';