From 8a63cd25ad99c4800a98fe6be6e8b02befbd8c2a Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Tue, 8 Apr 2025 12:04:22 -0400 Subject: [PATCH 01/11] chore(clerk-js): Add truncation option to line items description text --- .../src/ui/components/Checkout/CheckoutComplete.tsx | 5 ++++- packages/clerk-js/src/ui/elements/LineItems.tsx | 12 +++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/clerk-js/src/ui/components/Checkout/CheckoutComplete.tsx b/packages/clerk-js/src/ui/components/Checkout/CheckoutComplete.tsx index 437cd1e21be..87f0894b40d 100644 --- a/packages/clerk-js/src/ui/components/Checkout/CheckoutComplete.tsx +++ b/packages/clerk-js/src/ui/components/Checkout/CheckoutComplete.tsx @@ -124,7 +124,10 @@ export const CheckoutComplete = ({ checkout }: { checkout: __experimental_Commer {/* TODO(@COMMERCE): needs localization */} - + + ); +} + export const LineItems = { Root, Group, From b0fc9fd2fc765e8e9d075840549d0b9fe1761ec1 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Tue, 8 Apr 2025 15:43:06 -0400 Subject: [PATCH 03/11] wip --- .../ui/customizables/elementDescriptors.ts | 1 + .../clerk-js/src/ui/elements/LineItems.tsx | 27 ++++++++++++++++--- packages/types/src/appearance.ts | 1 + 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/packages/clerk-js/src/ui/customizables/elementDescriptors.ts b/packages/clerk-js/src/ui/customizables/elementDescriptors.ts index 9658dc0ad4d..67e9d8c5aaa 100644 --- a/packages/clerk-js/src/ui/customizables/elementDescriptors.ts +++ b/packages/clerk-js/src/ui/customizables/elementDescriptors.ts @@ -42,6 +42,7 @@ export const APPEARANCE_KEYS = containsAllElementsConfigKeys([ 'lineItemsDescriptionSuffix', 'lineItemsDescriptionPrefix', 'lineItemsDescriptionText', + 'lineItemsCopyButton', 'actionCard', diff --git a/packages/clerk-js/src/ui/elements/LineItems.tsx b/packages/clerk-js/src/ui/elements/LineItems.tsx index f7a0eb56c33..41caa2109b4 100644 --- a/packages/clerk-js/src/ui/elements/LineItems.tsx +++ b/packages/clerk-js/src/ui/elements/LineItems.tsx @@ -97,7 +97,6 @@ function Title({ title, description }: TitleProps) { sx={t => ({ display: 'grid', color: variant === 'primary' ? t.colors.$colorText : t.colors.$colorTextSecondary, - marginTop: variant !== 'primary' ? t.space.$0x25 : undefined, ...common.textVariants(t)[textVariant], })} > @@ -122,13 +121,26 @@ function Title({ title, description }: TitleProps) { interface DescriptionProps { text: string | LocalizationKey; + /** + * When true, the text will be truncated to 15 characters. + * @default `false` + */ truncateText?: boolean; + /** + * When true, there will be a button to copy the text. + * @default `false` + */ copyText?: boolean; + /** + * The visually hidden label of the copy button. + * @default `Copy` + */ + copyLabel?: string; prefix?: string | LocalizationKey; suffix?: string | LocalizationKey; } -function Description({ text, prefix, suffix, truncateText = false, copyText = false }: DescriptionProps) { +function Description({ text, prefix, suffix, truncateText = false, copyText = false, copyLabel }: DescriptionProps) { const context = React.useContext(GroupContext); if (!context) { throw new Error('LineItems.Description must be used within LineItems.Group'); @@ -197,7 +209,12 @@ function Description({ text, prefix, suffix, truncateText = false, copyText = fa })} /> )} - {typeof text === 'string' && copyText ? : null} + {typeof text === 'string' && copyText ? ( + + ) : null} {suffix ? ( ({ color: t.colors.$colorTextSecondary, ...common.textVariants(t).caption, + justifySelf: 'flex-end', })} /> ) : null} @@ -213,7 +231,7 @@ function Description({ text, prefix, suffix, truncateText = false, copyText = fa ); } -function CopyButton({ text }: { text: string }) { +function CopyButton({ text, copyLabel = 'Copy' }: { text: string; copyLabel?: string }) { const { onCopy, hasCopied } = useClipboard(text || ''); return ( @@ -227,6 +245,7 @@ function CopyButton({ text }: { text: string }) { padding: 0, })} focusRing={false} + aria-label={hasCopied ? 'Copied' : copyLabel} > Date: Tue, 8 Apr 2025 20:26:12 -0400 Subject: [PATCH 04/11] add props --- .../clerk-js/src/ui/components/Checkout/CheckoutComplete.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/clerk-js/src/ui/components/Checkout/CheckoutComplete.tsx b/packages/clerk-js/src/ui/components/Checkout/CheckoutComplete.tsx index 87f0894b40d..738b7a1aab2 100644 --- a/packages/clerk-js/src/ui/components/Checkout/CheckoutComplete.tsx +++ b/packages/clerk-js/src/ui/components/Checkout/CheckoutComplete.tsx @@ -127,6 +127,8 @@ export const CheckoutComplete = ({ checkout }: { checkout: __experimental_Commer From 9e2af150652c0ab05b23c36ea726c5c27f69aaed Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Tue, 8 Apr 2025 20:27:07 -0400 Subject: [PATCH 05/11] add changeset --- .changeset/light-roses-design.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/light-roses-design.md diff --git a/.changeset/light-roses-design.md b/.changeset/light-roses-design.md new file mode 100644 index 00000000000..d9080767278 --- /dev/null +++ b/.changeset/light-roses-design.md @@ -0,0 +1,6 @@ +--- +'@clerk/clerk-js': patch +'@clerk/types': patch +--- + +Add copy and truncation options to `` component. From db23a5f1659f8dfb9e2c98b3fa9b23ebd50cacbd Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Tue, 8 Apr 2025 20:51:18 -0400 Subject: [PATCH 06/11] truncateWithEndVisible --- .../clerk-js/src/ui/elements/LineItems.tsx | 12 +----- .../truncateTextWithEndVisible.test.ts | 41 +++++++++++++++++++ .../ui/utils/truncateTextWithEndVisible.ts | 38 +++++++++++++++++ 3 files changed, 81 insertions(+), 10 deletions(-) create mode 100644 packages/clerk-js/src/ui/utils/__tests__/truncateTextWithEndVisible.test.ts create mode 100644 packages/clerk-js/src/ui/utils/truncateTextWithEndVisible.ts diff --git a/packages/clerk-js/src/ui/elements/LineItems.tsx b/packages/clerk-js/src/ui/elements/LineItems.tsx index 41caa2109b4..3a1b64ab9f5 100644 --- a/packages/clerk-js/src/ui/elements/LineItems.tsx +++ b/packages/clerk-js/src/ui/elements/LineItems.tsx @@ -5,6 +5,7 @@ import { Box, Button, Dd, descriptors, Dl, Dt, Icon, Span } from '../customizabl import { useClipboard } from '../hooks'; import { Check, Copy } from '../icons'; import { common } from '../styledSystem'; +import { truncateWithEndVisible } from '../utils/truncateTextWithEndVisible'; /* ------------------------------------------------------------------------------------------------- * LineItems.Root @@ -185,16 +186,7 @@ function Description({ text, prefix, suffix, truncateText = false, copyText = fa minWidth: '0', })} > - - {text.slice(0, -5)} - - {text.slice(-5)} + {truncateWithEndVisible(text)} ) : ( { + test('should return empty string when input is empty', () => { + expect(truncateWithEndVisible('')).toBe(''); + }); + + test('should return original string when length is less than maxLength', () => { + expect(truncateWithEndVisible('short')).toBe('short'); + expect(truncateWithEndVisible('123456789', 10)).toBe('123456789'); + }); + + test('should truncate string with default parameters', () => { + expect(truncateWithEndVisible('this is a very long string')).toBe('this is a ve...tring'); + }); + + test('should truncate string with custom maxLength', () => { + expect(truncateWithEndVisible('this is a very long string', 15)).toBe('this is...tring'); + }); + + test('should truncate string with custom endChars', () => { + expect(truncateWithEndVisible('this is a very long string', 20, 3)).toBe('this is a very...ing'); + }); + + test('should handle edge case where maxLength is too small', () => { + expect(truncateWithEndVisible('1234567890', 5, 3)).toBe('...890'); + }); + + test('should handle email addresses', () => { + expect(truncateWithEndVisible('test@example.com', 10)).toBe('te...e.com'); + }); + + test('should handle very long strings', () => { + const longString = 'a'.repeat(1000); + expect(truncateWithEndVisible(longString, 20)).toBe('aaaaaaaaaaaa...aaaaa'); + }); + + test('should handle strings with spaces', () => { + expect(truncateWithEndVisible('hello world this is a test', 15)).toBe('hello w... test'); + }); +}); diff --git a/packages/clerk-js/src/ui/utils/truncateTextWithEndVisible.ts b/packages/clerk-js/src/ui/utils/truncateTextWithEndVisible.ts new file mode 100644 index 00000000000..e6b54e285b4 --- /dev/null +++ b/packages/clerk-js/src/ui/utils/truncateTextWithEndVisible.ts @@ -0,0 +1,38 @@ +/** + * Truncates a string to show the beginning and the last N characters, + * with an ellipsis in the middle. + * + * @param {string} str - The string to truncate + * @param {number} maxLength - Maximum total length of the truncated string (including ellipsis) + * @param {number} endChars - Number of characters to preserve at the end + * @return {string} - Truncated string with ellipsis in the middle + * + * @example + * truncateWithEndVisible('this is a very long string') // returns 'this is a ve...tring' + * truncateWithEndVisible('test@email.com', 10) // returns 'te...e.com' + */ +export function truncateWithEndVisible(str: string, maxLength = 20, endChars = 5): string { + const ELLIPSIS = '...'; + const ELLIPSIS_LENGTH = ELLIPSIS.length; + + if (!str || str.length <= maxLength) { + return str; + } + + if (maxLength <= endChars + ELLIPSIS_LENGTH) { + return ELLIPSIS + str.slice(-endChars); + } + + const chars = Array.from(str); + const totalChars = chars.length; + + if (totalChars <= maxLength) { + return str; + } + + const beginLength = maxLength - endChars - ELLIPSIS_LENGTH; + const beginPortion = chars.slice(0, beginLength).join(''); + const endPortion = chars.slice(-endChars).join(''); + + return beginPortion + ELLIPSIS + endPortion; +} From d15096a94dbbc0ff58498f5c05aaa71e2541deaa Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Tue, 8 Apr 2025 20:55:11 -0400 Subject: [PATCH 07/11] update comments --- packages/clerk-js/src/ui/elements/LineItems.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/clerk-js/src/ui/elements/LineItems.tsx b/packages/clerk-js/src/ui/elements/LineItems.tsx index 3a1b64ab9f5..4d600d6ba8b 100644 --- a/packages/clerk-js/src/ui/elements/LineItems.tsx +++ b/packages/clerk-js/src/ui/elements/LineItems.tsx @@ -123,17 +123,17 @@ function Title({ title, description }: TitleProps) { interface DescriptionProps { text: string | LocalizationKey; /** - * When true, the text will be truncated to 15 characters. + * When true, the text will be truncated with an ellipsis in the middle and the last 5 characters will be visible. * @default `false` */ truncateText?: boolean; /** - * When true, there will be a button to copy the text. + * When true, there will be a button to copy the providedtext. * @default `false` */ copyText?: boolean; /** - * The visually hidden label of the copy button. + * The visually hidden label for the copy button. * @default `Copy` */ copyLabel?: string; From f5ef6afeb91c9a14486b7b02f1227ba4b7e7326d Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Tue, 8 Apr 2025 20:57:27 -0400 Subject: [PATCH 08/11] focus --- packages/clerk-js/src/ui/elements/LineItems.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/clerk-js/src/ui/elements/LineItems.tsx b/packages/clerk-js/src/ui/elements/LineItems.tsx index 4d600d6ba8b..71151437f31 100644 --- a/packages/clerk-js/src/ui/elements/LineItems.tsx +++ b/packages/clerk-js/src/ui/elements/LineItems.tsx @@ -235,6 +235,11 @@ function CopyButton({ text, copyLabel = 'Copy' }: { text: string; copyLabel?: st width: t.sizes.$4, height: t.sizes.$4, padding: 0, + borderRadius: t.radii.$sm, + '&:focus-visible': { + outline: '2px solid', + outlineColor: t.colors.$neutralAlpha200, + }, })} focusRing={false} aria-label={hasCopied ? 'Copied' : copyLabel} From 5b809782f8acd3726c86ba1b52e38dbb753a98d8 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Tue, 8 Apr 2025 21:02:16 -0400 Subject: [PATCH 09/11] cleanup --- packages/clerk-js/src/ui/elements/LineItems.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/clerk-js/src/ui/elements/LineItems.tsx b/packages/clerk-js/src/ui/elements/LineItems.tsx index 71151437f31..f5093b5f7be 100644 --- a/packages/clerk-js/src/ui/elements/LineItems.tsx +++ b/packages/clerk-js/src/ui/elements/LineItems.tsx @@ -224,7 +224,7 @@ function Description({ text, prefix, suffix, truncateText = false, copyText = fa } function CopyButton({ text, copyLabel = 'Copy' }: { text: string; copyLabel?: string }) { - const { onCopy, hasCopied } = useClipboard(text || ''); + const { onCopy, hasCopied } = useClipboard(text); return ( ); From 8d0ee9dc71cab76d1c951cbac3a5470805791595 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 9 Apr 2025 09:15:51 -0400 Subject: [PATCH 10/11] copy truncated text --- .../clerk-js/src/ui/elements/LineItems.tsx | 31 +++++++++++++------ 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/packages/clerk-js/src/ui/elements/LineItems.tsx b/packages/clerk-js/src/ui/elements/LineItems.tsx index f5093b5f7be..063ccc56fa8 100644 --- a/packages/clerk-js/src/ui/elements/LineItems.tsx +++ b/packages/clerk-js/src/ui/elements/LineItems.tsx @@ -178,16 +178,7 @@ function Description({ text, prefix, suffix, truncateText = false, copyText = fa /> ) : null} {typeof text === 'string' && truncateText ? ( - ({ - ...common.textVariants(t).body, - display: 'flex', - minWidth: '0', - })} - > - {truncateWithEndVisible(text)} - + ) : ( ({ + ...common.textVariants(t).body, + display: 'flex', + minWidth: '0', + })} + onCopy={async e => { + e.preventDefault(); + await onCopy(); + }} + > + {truncateWithEndVisible(text)} + + ); +} + function CopyButton({ text, copyLabel = 'Copy' }: { text: string; copyLabel?: string }) { const { onCopy, hasCopied } = useClipboard(text); From fa885c1c540c6183f14d7bc732b74459ba135a7d Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 9 Apr 2025 09:48:20 -0400 Subject: [PATCH 11/11] Update bundlewatch.config.json --- packages/clerk-js/bundlewatch.config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/clerk-js/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json index 3a9f48d6527..645dc8025e8 100644 --- a/packages/clerk-js/bundlewatch.config.json +++ b/packages/clerk-js/bundlewatch.config.json @@ -3,7 +3,7 @@ { "path": "./dist/clerk.js", "maxSize": "590kB" }, { "path": "./dist/clerk.browser.js", "maxSize": "72.5KB" }, { "path": "./dist/clerk.headless*.js", "maxSize": "55KB" }, - { "path": "./dist/ui-common*.js", "maxSize": "98KB" }, + { "path": "./dist/ui-common*.js", "maxSize": "99KB" }, { "path": "./dist/vendors*.js", "maxSize": "36KB" }, { "path": "./dist/coinbase*.js", "maxSize": "35.5KB" }, { "path": "./dist/createorganization*.js", "maxSize": "5KB" },