diff --git a/CHANGELOG.md b/CHANGELOG.md index 424b0df..9dd3933 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- [Added Trim and CopyButton components](https://github.com/multiversx/mx-sdk-dapp-core-ui/pull/28) - [Added transaction account](https://github.com/multiversx/mx-sdk-dapp-core-ui/pull/27) - [Clean package.json export](https://github.com/multiversx/mx-sdk-dapp-core-ui/pull/26) - [Updated transactions table props](https://github.com/multiversx/mx-sdk-dapp-core-ui/pull/25) diff --git a/src/components.d.ts b/src/components.d.ts index 52e9757..67ccefd 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -5,8 +5,8 @@ * It contains typing information for all components that exist in this project. */ import { HTMLStencilElement, JSXBase } from "@stencil/core/internal"; -import { CustomToastType, IComponentToast, ISimpleToast } from "./components/toasts-list/components/transaction-toast/transaction-toast.type"; import { IconDefinition } from "@fortawesome/free-solid-svg-icons"; +import { CustomToastType, IComponentToast, ISimpleToast } from "./components/toasts-list/components/transaction-toast/transaction-toast.type"; import { LocalJSX as JSX, VNode } from "@stencil/core"; import { ILedgerConnectModalData } from "./components/ledger-connect-modal/ledger-connect-modal.types"; import { IEventBus } from "./utils/EventBus"; @@ -15,8 +15,8 @@ import { ISignTransactionsModalData } from "./components/sign-transactions-modal import { CustomToastType as CustomToastType1, IToastDataState, ITransaction, ITransactionProgressState, ITransactionToast } from "./components/toasts-list/components/transaction-toast/transaction-toast.type"; import { ITransactionAccount, ITransactionIconInfo, ITransactionsTableRow } from "./components/transactions-table/transactions-table.type"; import { IWalletConnectModalData } from "./components/wallet-connect-modal/wallet-connect-modal.types"; -export { CustomToastType, IComponentToast, ISimpleToast } from "./components/toasts-list/components/transaction-toast/transaction-toast.type"; export { IconDefinition } from "@fortawesome/free-solid-svg-icons"; +export { CustomToastType, IComponentToast, ISimpleToast } from "./components/toasts-list/components/transaction-toast/transaction-toast.type"; export { LocalJSX as JSX, VNode } from "@stencil/core"; export { ILedgerConnectModalData } from "./components/ledger-connect-modal/ledger-connect-modal.types"; export { IEventBus } from "./utils/EventBus"; @@ -32,6 +32,12 @@ export namespace Components { "ticker": string; "usdValue"?: string; } + interface CopyButton { + "class"?: string; + "copyIcon": IconDefinition; + "successIcon": IconDefinition; + "text": string; + } interface CustomToast { "toast": IComponentToast; } @@ -42,7 +48,7 @@ export namespace Components { "link": string; "text"?: string; } - interface FontawesomeIcon { + interface FaIcon { "class"?: string; "description"?: string; "icon": IconDefinition; @@ -177,6 +183,11 @@ export namespace Components { "class"?: string; "data": string; } + interface TrimText { + "class"?: string; + "dataTestId": string; + "text": string; + } interface WalletConnectModal { "data": IWalletConnectModalData; "getEventBus": () => Promise; @@ -213,6 +224,12 @@ declare global { prototype: HTMLBalanceComponentElement; new (): HTMLBalanceComponentElement; }; + interface HTMLCopyButtonElement extends Components.CopyButton, HTMLStencilElement { + } + var HTMLCopyButtonElement: { + prototype: HTMLCopyButtonElement; + new (): HTMLCopyButtonElement; + }; interface HTMLCustomToastElementEventMap { "handleDeleteToast": string; } @@ -236,11 +253,11 @@ declare global { prototype: HTMLExplorerLinkElement; new (): HTMLExplorerLinkElement; }; - interface HTMLFontawesomeIconElement extends Components.FontawesomeIcon, HTMLStencilElement { + interface HTMLFaIconElement extends Components.FaIcon, HTMLStencilElement { } - var HTMLFontawesomeIconElement: { - prototype: HTMLFontawesomeIconElement; - new (): HTMLFontawesomeIconElement; + var HTMLFaIconElement: { + prototype: HTMLFaIconElement; + new (): HTMLFaIconElement; }; interface HTMLFormatAmountElement extends Components.FormatAmount, HTMLStencilElement { } @@ -459,6 +476,12 @@ declare global { prototype: HTMLTransactionsTableElement; new (): HTMLTransactionsTableElement; }; + interface HTMLTrimTextElement extends Components.TrimText, HTMLStencilElement { + } + var HTMLTrimTextElement: { + prototype: HTMLTrimTextElement; + new (): HTMLTrimTextElement; + }; interface HTMLWalletConnectModalElement extends Components.WalletConnectModal, HTMLStencilElement { } var HTMLWalletConnectModalElement: { @@ -467,9 +490,10 @@ declare global { }; interface HTMLElementTagNameMap { "balance-component": HTMLBalanceComponentElement; + "copy-button": HTMLCopyButtonElement; "custom-toast": HTMLCustomToastElement; "explorer-link": HTMLExplorerLinkElement; - "fontawesome-icon": HTMLFontawesomeIconElement; + "fa-icon": HTMLFaIconElement; "format-amount": HTMLFormatAmountElement; "fungible-component": HTMLFungibleComponentElement; "generic-modal": HTMLGenericModalElement; @@ -497,6 +521,7 @@ declare global { "transaction-toast-progress": HTMLTransactionToastProgressElement; "transaction-toast-wrapper": HTMLTransactionToastWrapperElement; "transactions-table": HTMLTransactionsTableElement; + "trim-text": HTMLTrimTextElement; "wallet-connect-modal": HTMLWalletConnectModalElement; } } @@ -507,6 +532,12 @@ declare namespace LocalJSX { "ticker"?: string; "usdValue"?: string; } + interface CopyButton { + "class"?: string; + "copyIcon"?: IconDefinition; + "successIcon"?: IconDefinition; + "text"?: string; + } interface CustomToast { "onHandleDeleteToast"?: (event: CustomToastCustomEvent) => void; "toast"?: IComponentToast; @@ -518,7 +549,7 @@ declare namespace LocalJSX { "link"?: string; "text"?: string; } - interface FontawesomeIcon { + interface FaIcon { "class"?: string; "description"?: string; "icon"?: IconDefinition; @@ -654,14 +685,20 @@ declare namespace LocalJSX { "class"?: string; "data"?: string; } + interface TrimText { + "class"?: string; + "dataTestId"?: string; + "text"?: string; + } interface WalletConnectModal { "data"?: IWalletConnectModalData; } interface IntrinsicElements { "balance-component": BalanceComponent; + "copy-button": CopyButton; "custom-toast": CustomToast; "explorer-link": ExplorerLink; - "fontawesome-icon": FontawesomeIcon; + "fa-icon": FaIcon; "format-amount": FormatAmount; "fungible-component": FungibleComponent; "generic-modal": GenericModal; @@ -689,6 +726,7 @@ declare namespace LocalJSX { "transaction-toast-progress": TransactionToastProgress; "transaction-toast-wrapper": TransactionToastWrapper; "transactions-table": TransactionsTable; + "trim-text": TrimText; "wallet-connect-modal": WalletConnectModal; } } @@ -697,9 +735,10 @@ declare module "@stencil/core" { export namespace JSX { interface IntrinsicElements { "balance-component": LocalJSX.BalanceComponent & JSXBase.HTMLAttributes; + "copy-button": LocalJSX.CopyButton & JSXBase.HTMLAttributes; "custom-toast": LocalJSX.CustomToast & JSXBase.HTMLAttributes; "explorer-link": LocalJSX.ExplorerLink & JSXBase.HTMLAttributes; - "fontawesome-icon": LocalJSX.FontawesomeIcon & JSXBase.HTMLAttributes; + "fa-icon": LocalJSX.FaIcon & JSXBase.HTMLAttributes; "format-amount": LocalJSX.FormatAmount & JSXBase.HTMLAttributes; "fungible-component": LocalJSX.FungibleComponent & JSXBase.HTMLAttributes; "generic-modal": LocalJSX.GenericModal & JSXBase.HTMLAttributes; @@ -727,6 +766,7 @@ declare module "@stencil/core" { "transaction-toast-progress": LocalJSX.TransactionToastProgress & JSXBase.HTMLAttributes; "transaction-toast-wrapper": LocalJSX.TransactionToastWrapper & JSXBase.HTMLAttributes; "transactions-table": LocalJSX.TransactionsTable & JSXBase.HTMLAttributes; + "trim-text": LocalJSX.TrimText & JSXBase.HTMLAttributes; "wallet-connect-modal": LocalJSX.WalletConnectModal & JSXBase.HTMLAttributes; } } diff --git a/src/components/copy-button/copy-button.css b/src/components/copy-button/copy-button.css new file mode 100644 index 0000000..2934cbe --- /dev/null +++ b/src/components/copy-button/copy-button.css @@ -0,0 +1,3 @@ +.copy-button { + color: #6c757d; +} diff --git a/src/components/copy-button/copy-button.tsx b/src/components/copy-button/copy-button.tsx new file mode 100644 index 0000000..978e15c --- /dev/null +++ b/src/components/copy-button/copy-button.tsx @@ -0,0 +1,51 @@ +import { Component, Prop, h, State } from '@stencil/core'; +import { faCheck, faCopy, IconDefinition } from '@fortawesome/free-solid-svg-icons'; +import { copyToClipboard } from 'utils/copyToClipboard'; + +@Component({ + tag: 'copy-button', + styleUrl: 'copy-button.css', + shadow: true, +}) +export class CopyButton { + @Prop() class?: string = 'copy-button'; + @Prop() copyIcon: IconDefinition = faCopy; + @Prop() successIcon: IconDefinition = faCheck; + @Prop() text: string; + + @State() isSuccess: boolean = false; + + private timeoutId: number | undefined; + + private handleClick = async (event: MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + + const trimmedText = this.text ? this.text.trim() : this.text; + const success = await copyToClipboard(trimmedText); + + this.isSuccess = success; + + if (success) { + this.timeoutId = window.setTimeout(() => { + this.isSuccess = false; + }, 1000); + } + }; + + disconnectedCallback() { + // Clear the timeout if the component is unmounted + if (this.timeoutId) { + clearTimeout(this.timeoutId); + this.timeoutId = undefined; // Reset the timeout ID + } + } + + render() { + return ( + + + + ); + } +} diff --git a/src/components/copy-button/tests/copy-button.spec.ts b/src/components/copy-button/tests/copy-button.spec.ts new file mode 100644 index 0000000..3d10850 --- /dev/null +++ b/src/components/copy-button/tests/copy-button.spec.ts @@ -0,0 +1,109 @@ +import { newSpecPage } from '@stencil/core/testing'; +import { CopyButton } from '../copy-button'; +import * as copyUtils from 'utils/copyToClipboard'; + +describe('CopyButton', () => { + it('renders with default props', async () => { + const page = await newSpecPage({ + components: [CopyButton], + html: '', + }); + + expect(page.root).toEqualHtml(` + + + + + + + + `); + }); + + it('renders with custom class', async () => { + const page = await newSpecPage({ + components: [CopyButton], + html: '', + }); + + expect(page.root).toEqualHtml(` + + + + + + + + `); + }); + + it('changes to success icon when clicked and copy succeeds', async () => { + jest.spyOn(copyUtils, 'copyToClipboard').mockResolvedValue(true); + + const page = await newSpecPage({ + components: [CopyButton], + html: '', + }); + + const copyButton = page.root; + const anchor = copyButton.shadowRoot.querySelector('a'); + + await anchor.click(); + await page.waitForChanges(); + + expect(copyButton).toEqualHtml(` + + + + + + + + `); + }); + + it('remains with copy icon when clicked and copy fails', async () => { + jest.spyOn(copyUtils, 'copyToClipboard').mockResolvedValue(false); + + const page = await newSpecPage({ + components: [CopyButton], + html: '', + }); + + const copyButton = page.root; + const anchor = copyButton.shadowRoot.querySelector('a'); + + await anchor.click(); + await page.waitForChanges(); + + expect(copyButton).toEqualHtml(` + + + + + + + + `); + }); + + it('prevents default behavior and stops propagation on click', async () => { + const page = await newSpecPage({ + components: [CopyButton], + html: '', + }); + + const copyButton = page.root; + const anchor = copyButton.shadowRoot.querySelector('a'); + + const mockEvent = { + preventDefault: jest.fn(), + stopPropagation: jest.fn(), + }; + + anchor.dispatchEvent(new MouseEvent('click', mockEvent as any)); + + expect(mockEvent.preventDefault).toHaveBeenCalledTimes(1); + expect(mockEvent.stopPropagation).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/components/fontawesome-icon/fontawesome-icon.tsx b/src/components/fa-icon/fa-icon.tsx similarity index 78% rename from src/components/fontawesome-icon/fontawesome-icon.tsx rename to src/components/fa-icon/fa-icon.tsx index 5d1190a..2fc1b70 100644 --- a/src/components/fontawesome-icon/fontawesome-icon.tsx +++ b/src/components/fa-icon/fa-icon.tsx @@ -3,12 +3,11 @@ import { getIconHtmlFromIconDefinition } from 'utils/icons/getIconHtmlFromIconDe import { IconDefinition } from '@fortawesome/free-solid-svg-icons'; @Component({ - tag: 'fontawesome-icon', - styleUrl: 'fontawesome-icon.css', + tag: 'fa-icon', shadow: true, }) -export class FontawesomeIcon { - @Prop() class?: string = 'fontawesome-icon'; +export class FaIcon { + @Prop() class?: string = 'fa-icon'; @Prop() icon: IconDefinition; @Prop() description?: string; diff --git a/src/components/fontawesome-icon/readme.md b/src/components/fa-icon/readme.md similarity index 80% rename from src/components/fontawesome-icon/readme.md rename to src/components/fa-icon/readme.md index 8c80358..e158df1 100644 --- a/src/components/fontawesome-icon/readme.md +++ b/src/components/fa-icon/readme.md @@ -1,4 +1,4 @@ -# fontawesome-icon +# fa-icon @@ -9,7 +9,7 @@ | Property | Attribute | Description | Type | Default | | ------------- | ------------- | ----------- | ---------------- | -------------------- | -| `class` | `class` | | `string` | `'fontawesome-icon'` | +| `class` | `class` | | `string` | `'fa-icon'` | | `description` | `description` | | `string` | `undefined` | | `icon` | -- | | `IconDefinition` | `undefined` | @@ -24,9 +24,9 @@ ### Graph ```mermaid graph TD; - transaction-account --> fontawesome-icon - transaction-icon --> fontawesome-icon - style fontawesome-icon fill:#f9f,stroke:#333,stroke-width:4px + transaction-account --> fa-icon + transaction-icon --> fa-icon + style fa-icon fill:#f9f,stroke:#333,stroke-width:4px ``` ---------------------------------------------- diff --git a/src/components/fontawesome-icon/tests/fontawesome-icon.spec.tsx b/src/components/fa-icon/tests/fa-icon.spec.tsx similarity index 68% rename from src/components/fontawesome-icon/tests/fontawesome-icon.spec.tsx rename to src/components/fa-icon/tests/fa-icon.spec.tsx index 8b42abf..15d3e4c 100644 --- a/src/components/fontawesome-icon/tests/fontawesome-icon.spec.tsx +++ b/src/components/fa-icon/tests/fa-icon.spec.tsx @@ -1,13 +1,13 @@ import { h } from '@stencil/core'; import { newSpecPage } from '@stencil/core/testing'; -import { FontawesomeIcon } from '../fontawesome-icon'; +import { FaIcon } from '../fa-icon'; import { faUser } from '@fortawesome/free-solid-svg-icons/faUser'; -describe('FontawesomeIcon Component', () => { +describe('FaIcon Component', () => { it('should render with icon prop', async () => { const page = await newSpecPage({ - components: [FontawesomeIcon], - template: () => , + components: [FaIcon], + template: () => , }); const i = page.root.shadowRoot.querySelector('i'); @@ -19,8 +19,8 @@ describe('FontawesomeIcon Component', () => { it('should not render when icon prop is not provided', async () => { const page = await newSpecPage({ - components: [FontawesomeIcon], - template: () => , + components: [FaIcon], + template: () => , }); expect(page.root.shadowRoot.childNodes.length).toBe(0); @@ -28,8 +28,8 @@ describe('FontawesomeIcon Component', () => { it('should apply custom class when provided', async () => { const page = await newSpecPage({ - components: [FontawesomeIcon], - template: () => , + components: [FaIcon], + template: () => , }); const i = page.root.shadowRoot.querySelector('i'); @@ -38,8 +38,8 @@ describe('FontawesomeIcon Component', () => { it('should set description as title when provided', async () => { const page = await newSpecPage({ - components: [FontawesomeIcon], - template: () => , + components: [FaIcon], + template: () => , }); const i = page.root.shadowRoot.querySelector('i'); @@ -48,11 +48,11 @@ describe('FontawesomeIcon Component', () => { it('should use default class when no class prop is provided', async () => { const page = await newSpecPage({ - components: [FontawesomeIcon], - template: () => , + components: [FaIcon], + template: () => , }); const i = page.root.shadowRoot.querySelector('i'); - expect(i.classList.contains('fontawesome-icon')).toBe(true); + expect(i.classList.contains('fa-icon')).toBe(true); }); }); diff --git a/src/components/fontawesome-icon/fontawesome-icon.css b/src/components/fontawesome-icon/fontawesome-icon.css deleted file mode 100644 index e69de29..0000000 diff --git a/src/components/transactions-table/components/transaction-account/readme.md b/src/components/transactions-table/components/transaction-account/readme.md index 0e19580..e2ac434 100644 --- a/src/components/transactions-table/components/transaction-account/readme.md +++ b/src/components/transactions-table/components/transaction-account/readme.md @@ -24,14 +24,14 @@ ### Depends on -- [fontawesome-icon](../../../fontawesome-icon) +- [fa-icon](../../../fa-icon) - [explorer-link](../../../explorer-link) - [transaction-account-name](./components/transaction-account-name) ### Graph ```mermaid graph TD; - transaction-account --> fontawesome-icon + transaction-account --> fa-icon transaction-account --> explorer-link transaction-account --> transaction-account-name transaction-row --> transaction-account diff --git a/src/components/transactions-table/components/transaction-account/tests/transaction-account.spec.tsx b/src/components/transactions-table/components/transaction-account/tests/transaction-account.spec.tsx index c76deb5..07d9c97 100644 --- a/src/components/transactions-table/components/transaction-account/tests/transaction-account.spec.tsx +++ b/src/components/transactions-table/components/transaction-account/tests/transaction-account.spec.tsx @@ -34,7 +34,7 @@ describe('TransactionAccount Component', () => { template: () => , }); - const lockedIcon = page.root.shadowRoot.querySelector('fontawesome-icon'); + const lockedIcon = page.root.shadowRoot.querySelector('fa-icon'); expect(lockedIcon).not.toBeNull(); }); @@ -46,7 +46,7 @@ describe('TransactionAccount Component', () => { template: () => , }); - const contractIcon = page.root.shadowRoot.querySelector('fontawesome-icon'); + const contractIcon = page.root.shadowRoot.querySelector('fa-icon'); expect(contractIcon).not.toBeNull(); }); diff --git a/src/components/transactions-table/components/transaction-account/transaction-account.css b/src/components/transactions-table/components/transaction-account/transaction-account.css deleted file mode 100644 index e69de29..0000000 diff --git a/src/components/transactions-table/components/transaction-account/transaction-account.tsx b/src/components/transactions-table/components/transaction-account/transaction-account.tsx index 464976d..9afcc61 100644 --- a/src/components/transactions-table/components/transaction-account/transaction-account.tsx +++ b/src/components/transactions-table/components/transaction-account/transaction-account.tsx @@ -5,7 +5,6 @@ import { faFileAlt } from '@fortawesome/free-solid-svg-icons/faFileAlt'; @Component({ tag: 'transaction-account', - styleUrl: 'transaction-account.css', shadow: true, }) export class TransactionAccount { @@ -18,9 +17,9 @@ export class TransactionAccount { render() { return (
- {this.showLockedAccounts && this.account.isTokenLocked && } + {this.showLockedAccounts && this.account.isTokenLocked && } - {this.account.isContract && } + {this.account.isContract && } {this.account.showLink ? ( diff --git a/src/components/transactions-table/components/transaction-age/transaction-age.css b/src/components/transactions-table/components/transaction-age/transaction-age.css deleted file mode 100644 index e69de29..0000000 diff --git a/src/components/transactions-table/components/transaction-age/transaction-age.tsx b/src/components/transactions-table/components/transaction-age/transaction-age.tsx index 30c122f..c3d50f3 100644 --- a/src/components/transactions-table/components/transaction-age/transaction-age.tsx +++ b/src/components/transactions-table/components/transaction-age/transaction-age.tsx @@ -3,7 +3,6 @@ import { DataTestIdsEnum } from 'constants/dataTestIds.enum'; @Component({ tag: 'transaction-age', - styleUrl: 'transaction-age.css', shadow: true, }) export class TransactionAge { diff --git a/src/components/transactions-table/components/transaction-hash/readme.md b/src/components/transactions-table/components/transaction-hash/readme.md index 178da9c..340f78b 100644 --- a/src/components/transactions-table/components/transaction-hash/readme.md +++ b/src/components/transactions-table/components/transaction-hash/readme.md @@ -29,7 +29,7 @@ graph TD; transaction-hash --> transaction-icon transaction-hash --> explorer-link - transaction-icon --> fontawesome-icon + transaction-icon --> fa-icon transaction-row --> transaction-hash style transaction-hash fill:#f9f,stroke:#333,stroke-width:4px ``` diff --git a/src/components/transactions-table/components/transaction-hash/transaction-hash.css b/src/components/transactions-table/components/transaction-hash/transaction-hash.css deleted file mode 100644 index 5d4e87f..0000000 --- a/src/components/transactions-table/components/transaction-hash/transaction-hash.css +++ /dev/null @@ -1,3 +0,0 @@ -:host { - display: block; -} diff --git a/src/components/transactions-table/components/transaction-hash/transaction-hash.tsx b/src/components/transactions-table/components/transaction-hash/transaction-hash.tsx index 1aced23..8599225 100644 --- a/src/components/transactions-table/components/transaction-hash/transaction-hash.tsx +++ b/src/components/transactions-table/components/transaction-hash/transaction-hash.tsx @@ -4,7 +4,6 @@ import { DataTestIdsEnum } from 'constants/dataTestIds.enum'; @Component({ tag: 'transaction-hash', - styleUrl: 'transaction-hash.css', shadow: true, }) export class TransactionHash { diff --git a/src/components/transactions-table/components/transaction-icon/readme.md b/src/components/transactions-table/components/transaction-icon/readme.md index 958e700..d53ce6c 100644 --- a/src/components/transactions-table/components/transaction-icon/readme.md +++ b/src/components/transactions-table/components/transaction-icon/readme.md @@ -21,12 +21,12 @@ ### Depends on -- [fontawesome-icon](../../../fontawesome-icon) +- [fa-icon](../../../fa-icon) ### Graph ```mermaid graph TD; - transaction-icon --> fontawesome-icon + transaction-icon --> fa-icon transaction-hash --> transaction-icon style transaction-icon fill:#f9f,stroke:#333,stroke-width:4px ``` diff --git a/src/components/transactions-table/components/transaction-icon/transaction-icon.css b/src/components/transactions-table/components/transaction-icon/transaction-icon.css deleted file mode 100644 index e69de29..0000000 diff --git a/src/components/transactions-table/components/transaction-icon/transaction-icon.tsx b/src/components/transactions-table/components/transaction-icon/transaction-icon.tsx index da0e83b..304d3d7 100644 --- a/src/components/transactions-table/components/transaction-icon/transaction-icon.tsx +++ b/src/components/transactions-table/components/transaction-icon/transaction-icon.tsx @@ -5,7 +5,6 @@ import { ITransactionIconInfo } from '../../transactions-table.type'; @Component({ tag: 'transaction-icon', - styleUrl: 'transaction-icon.css', shadow: true, }) export class TransactionIcon { @@ -18,7 +17,7 @@ export class TransactionIcon { } return ( - transaction-method transaction-hash --> transaction-icon transaction-hash --> explorer-link - transaction-icon --> fontawesome-icon - transaction-account --> fontawesome-icon + transaction-icon --> fa-icon + transaction-account --> fa-icon transaction-account --> explorer-link transaction-account --> transaction-account-name transactions-table --> transaction-row diff --git a/src/components/transactions-table/readme.md b/src/components/transactions-table/readme.md index edb9c6b..7bedf7d 100644 --- a/src/components/transactions-table/readme.md +++ b/src/components/transactions-table/readme.md @@ -29,8 +29,8 @@ graph TD; transaction-row --> transaction-method transaction-hash --> transaction-icon transaction-hash --> explorer-link - transaction-icon --> fontawesome-icon - transaction-account --> fontawesome-icon + transaction-icon --> fa-icon + transaction-account --> fa-icon transaction-account --> explorer-link transaction-account --> transaction-account-name style transactions-table fill:#f9f,stroke:#333,stroke-width:4px diff --git a/src/components/trim/tests/trim-text.e2e.tsx b/src/components/trim/tests/trim-text.e2e.tsx new file mode 100644 index 0000000..20bad2d --- /dev/null +++ b/src/components/trim/tests/trim-text.e2e.tsx @@ -0,0 +1,26 @@ +import { newE2EPage } from '@stencil/core/testing'; + +describe('trim-text', () => { + it('should render the full text when not overflowing', async () => { + const page = await newE2EPage(); + await page.setContent(''); + + const element = await page.find('trim-text'); + const trimSpan = await element.find('trim-text >>> span'); + + expect(trimSpan).not.toBeNull(); + expect(trimSpan.innerText).toBe('Short textShort text'); + expect(await element.find('.overflow')).toBe(null); + }); + + it('should use custom class and data-testid', async () => { + const page = await newE2EPage(); + await page.setContent(''); + const element = await page.find('trim-text'); + const trimSpan = await element.find('trim-text >>> span'); + + expect(trimSpan).not.toBeNull(); + expect(trimSpan.getAttribute('class')).toContain('custom-class'); + expect(trimSpan.getAttribute('data-testid')).toBe('custom-id'); + }); +}); diff --git a/src/components/trim/trim-text.css b/src/components/trim/trim-text.css new file mode 100644 index 0000000..79ec31f --- /dev/null +++ b/src/components/trim/trim-text.css @@ -0,0 +1,132 @@ +.trim { + display: flex; + max-width: 100%; + overflow: hidden; + position: relative; + white-space: nowrap; + + &.overflow { + .ellipsis { + display: block; + } + } + + .left { + flex-shrink: 1; + font-size: 1px; + + overflow: hidden; + text-overflow: ellipsis; + text-align: left; + } + + .right { + flex-shrink: 1; + font-size: 1px; + + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + direction: rtl; + text-align: right; + } + + .left span, + .right span { + font-size: 0.875rem; + pointer-events: none; + user-select: none; + } + + .ellipsis { + flex-shrink: 0; + display: none; + pointer-events: none; + user-select: none; + } + + /* IE fix */ + @media all and (-ms-high-contrast: none), (-ms-high-contrast: active) { + .right { + text-overflow: clip; + } + } + + /* SAFARI fix */ + @supports (-webkit-hyphens: none) { + .right, + .left { + letter-spacing: -0.001em; + } + .right { + text-overflow: clip; + } + } + + .hidden-text-ref { + position: absolute; + display: block; + color: transparent; + } + + @media (max-width: 1199.98px) { + max-width: 26rem; + } + + @media (max-width: 991.98px) { + max-width: 12rem; + } + + @media (max-width: 768px) { + max-width: 8rem; + } +} + +.trim-wrapper { + display: flex; + max-width: 100%; + overflow: hidden; +} + +a:hover > .trim span { + color: #007bff; + + &.hidden-text-ref { + color: transparent; + } +} + +a > .trim span, +.text-primary > .trim span { + color: #1b46c2; + + &.hidden-text-ref { + color: transparent; + } +} + +.table .trim { + max-width: 10rem; +} + +.table .trim-only-sm .trim { + max-width: none; + + @media (max-width: 768px) { + max-width: 13rem; + } +} + +.trim-fs-sm .trim { + .left span, + .right span, + .ellipsis { + font-size: 0.875rem; + } +} + +.table .trim-size-xl .trim { + @media (max-width: 768px) { + max-width: 13rem; + } +} diff --git a/src/components/trim/trim-text.tsx b/src/components/trim/trim-text.tsx new file mode 100644 index 0000000..f3cdfd7 --- /dev/null +++ b/src/components/trim/trim-text.tsx @@ -0,0 +1,83 @@ +import { Component, Prop, h, State, Element } from '@stencil/core'; +import classNames from 'classnames'; +import { DataTestIdsEnum } from 'constants/dataTestIds.enum'; +import { ELLIPSIS } from 'constants/htmlStrings'; + +@Component({ + tag: 'trim-text', + styleUrl: 'trim-text.css', + shadow: true, +}) +export class TrimText { + @Element() el: HTMLElement; + + @Prop() text: string; + @Prop() class?: string = 'trim'; + @Prop() dataTestId: string = DataTestIdsEnum.trim; + + @State() overflow: boolean = false; + + private trimRef: HTMLSpanElement; + private hiddenTextRef: HTMLSpanElement; + + private resizeObserver: ResizeObserver; + + componentWillLoad() { + this.checkOverflow(); + this.setupResizeObserver(); + } + + disconnectedCallback() { + if (this.resizeObserver) { + this.resizeObserver.disconnect(); + } + + window.removeEventListener('resize', this.checkOverflow); + } + + private setupResizeObserver() { + this.resizeObserver = new ResizeObserver(() => { + this.checkOverflow(); + }); + + if (this.trimRef && this.hiddenTextRef) { + this.resizeObserver.observe(this.hiddenTextRef); + this.resizeObserver.observe(this.trimRef); + } + + window.addEventListener('resize', this.checkOverflow); + } + + private checkOverflow() { + if (this.hiddenTextRef && this.trimRef) { + const diff = this.hiddenTextRef.offsetWidth - this.trimRef.offsetWidth; + this.overflow = diff > 1; + } + } + + render() { + return ( + (this.trimRef = el as HTMLSpanElement)} class={classNames(this.class, { overflow: this.overflow })} data-testid={this.dataTestId}> + (this.hiddenTextRef = el as HTMLSpanElement)} class="hidden-text"> + {this.text} + + + {this.overflow ? ( +
+ + {String(this.text).substring(0, Math.floor(this.text.length / 2))} + + + {ELLIPSIS} + + + {String(this.text).substring(Math.ceil(this.text.length / 2))} + +
+ ) : ( + {this.text} + )} +
+ ); + } +} diff --git a/src/constants/dataTestIds.enum.ts b/src/constants/dataTestIds.enum.ts index 29c735d..16bdd01 100644 --- a/src/constants/dataTestIds.enum.ts +++ b/src/constants/dataTestIds.enum.ts @@ -29,4 +29,5 @@ export enum DataTestIdsEnum { transactionLink = 'transactionLink', transactionReceiver = 'transactionReceiver', transactionSender = 'transactionSender', + trim = 'trim', } diff --git a/src/constants/htmlStrings.ts b/src/constants/htmlStrings.ts new file mode 100644 index 0000000..55d50fa --- /dev/null +++ b/src/constants/htmlStrings.ts @@ -0,0 +1 @@ +export const ELLIPSIS = '...'; diff --git a/src/utils/copyToClipboard.ts b/src/utils/copyToClipboard.ts new file mode 100644 index 0000000..3173e96 --- /dev/null +++ b/src/utils/copyToClipboard.ts @@ -0,0 +1,52 @@ +import { isWindowAvailable } from './isWindowAvailable'; + +function fallbackCopyTextToClipboard(text: string) { + let success = false; + + if (!document || !document.body) { + console.error('Document or document.body is not available'); + return success; + } + + const textArea = document.createElement('textarea'); + textArea.value = text; + textArea.style.position = 'fixed'; + textArea.style.left = '-9999px'; + textArea.style.top = '0'; + document.body.appendChild(textArea); + + try { + textArea.focus(); + textArea.setSelectionRange(0, textArea.value.length); + success = document.execCommand('copy'); + } catch (err) { + console.error('Fallback: Oops, unable to copy', err); + } finally { + document.body.removeChild(textArea); + } + + return success; +} +export async function copyToClipboard(text: string) { + if (!isWindowAvailable()) { + return false; + } + + let success = false; + + if (!navigator.clipboard) { + success = fallbackCopyTextToClipboard(text); + } else { + success = await navigator.clipboard.writeText(text).then( + function done() { + return true; + }, + function error(err) { + console.error('Async: Could not copy text: ', err); + return false; + }, + ); + } + + return success; +} diff --git a/src/utils/isWindowAvailable.ts b/src/utils/isWindowAvailable.ts new file mode 100644 index 0000000..fba1824 --- /dev/null +++ b/src/utils/isWindowAvailable.ts @@ -0,0 +1 @@ +export const isWindowAvailable = () => typeof window != 'undefined' && typeof window?.location != 'undefined';