From 58806652a0e53459c7937ab066b76d2989874d26 Mon Sep 17 00:00:00 2001 From: MaxFrank13 Date: Thu, 11 Dec 2025 20:08:29 +0000 Subject: [PATCH 1/4] feat: truncate component rework --- .stylelintrc.json | 2 +- src/Truncate/README.md | 36 +++++----- src/Truncate/Truncate.test.jsx | 84 +++++++++++++++++------- src/Truncate/Truncate.tsx | 30 +++++++++ src/Truncate/TruncateDeprecated.jsx | 71 ++++++++++++++++++++ src/Truncate/TruncateDeprecated.test.jsx | 27 ++++++++ src/Truncate/index.jsx | 76 +-------------------- src/Truncate/index.scss | 6 ++ src/Truncate/utils.d.ts | 1 + src/Truncate/utils.js | 1 + src/Truncate/utilsNew.ts | 80 ++++++++++++++++++++++ src/index.scss | 1 + 12 files changed, 299 insertions(+), 116 deletions(-) create mode 100644 src/Truncate/Truncate.tsx create mode 100644 src/Truncate/TruncateDeprecated.jsx create mode 100644 src/Truncate/TruncateDeprecated.test.jsx create mode 100644 src/Truncate/index.scss create mode 100644 src/Truncate/utils.d.ts create mode 100644 src/Truncate/utilsNew.ts diff --git a/.stylelintrc.json b/.stylelintrc.json index 370aefe972..fd24feef96 100644 --- a/.stylelintrc.json +++ b/.stylelintrc.json @@ -11,7 +11,7 @@ "ignoreProperties": ["animation", "filter"] }], "value-no-vendor-prefix": [true, { - "ignoreValues": ["fill-available"] + "ignoreValues": ["fill-available", "box"] }], "function-no-unknown": null, "number-leading-zero": "never", diff --git a/src/Truncate/README.md b/src/Truncate/README.md index fd847ddd84..3e2c5147f4 100644 --- a/src/Truncate/README.md +++ b/src/Truncate/README.md @@ -5,11 +5,9 @@ components: - Truncate categories: - Content -status: 'Deprecate Soon' +status: 'New' designStatus: 'Done' devStatus: 'Done' -notes: | - Plan to replace with native css implementation as per https://github.com/openedx/paragon/issues/3311 --- A Truncate component can help you crop multiline text. There will be three dots at the end of the text. @@ -17,34 +15,34 @@ A Truncate component can help you crop multiline text. There will be three dots ## Basic Usage ```jsx live - + Learners, course teams, researchers, developers: the edX community includes groups with a range of reasons for using the platform and objectives to accomplish. To help members of each group learn about what edX offers, reach goals, and solve problems, edX provides a variety of information resources. Learners, course teams, researchers, developers: the edX community includes groups with a range of reasons for using the platform and objectives to accomplish. To help members of each group learn about what edX offers, reach goals, and solve problems, edX provides a variety of information resources. - + ``` ### With the custom ellipsis ```jsx live - + Learners, course teams, researchers, developers: the edX community includes groups with a range of reasons for using the platform and objectives to accomplish. To help members of each group learn about what edX offers, reach goals, and solve problems, edX provides a variety of information resources. - + ``` ### With the onTruncate ```jsx live - console.log('onTruncate')}> + Learners, course teams, researchers, developers: the edX community includes groups with a range of reasons for using the platform and objectives to accomplish. To help members of each group learn about what edX offers, reach goals, and solve problems, edX provides a variety of information resources. - + ``` ### Example usage in Card @@ -61,22 +59,22 @@ A Truncate component can help you crop multiline text. There will be three dots /> + Using Enhanced Capabilities In Your Course - } + } /> - + Learners, course teams, researchers, developers: the edX community includes groups with a range of reasons for using the platform and objectives to accomplish. To help members of each group learn about what edX offers, reach goals, and solve problems, edX provides a variety of information resources. - + + Using Enhanced Capabilities In Your Course - } + } > @@ -87,10 +85,12 @@ A Truncate component can help you crop multiline text. There will be three dots ### HTML markdown support -**Note**: `Truncate` supports only plain `HTML` children and not `jsx`. +**Note**: `Truncate` supports only plain `HTML` children and not `jsx`. + +QUESTION FOR KEVIN: Would this fail screen readers anyway? ```jsx live - + Learners, course teams, researchers, developers: the edX community includes groups with a range of reasons for using the platform and objectives to accomplish. - + ``` diff --git a/src/Truncate/Truncate.test.jsx b/src/Truncate/Truncate.test.jsx index d1cc120c85..de612e940c 100644 --- a/src/Truncate/Truncate.test.jsx +++ b/src/Truncate/Truncate.test.jsx @@ -1,27 +1,65 @@ import React from 'react'; +import '@testing-library/jest-dom'; import { render, screen } from '@testing-library/react'; -import Truncate from '.'; - -describe('', () => { - render( - - Learners, course teams, researchers, developers. - , - ); - it('render with className', () => { - const element = screen.getByText(/Learners, course teams, researchers, developers./i); - expect(element).toBeTruthy(); - expect(element.className).toContain('pgn__truncate'); - expect(element.getAttribute('aria-label')).toBe('Learners, course teams, researchers, developers.'); - expect(element.getAttribute('title')).toBe('Learners, course teams, researchers, developers.'); +import Truncate from './Truncate'; +import { assembleStringFromChildrenArray } from './utils'; + +jest.mock('./utils', () => ({ + assembleStringFromChildrenArray: jest.fn( + (children) => `Assembled text from ${children.length} elements` + ), +})); + +describe('Truncate Component', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should render with default line clamp of 1', () => { + const testContent = "This is a test string."; + render({testContent}); + + const element = screen.getByTestId('truncate-element'); + expect(element.style.WebkitLineClamp).toBe('1'); + + expect(element).toHaveAttribute('title', testContent); + expect(element).toHaveAttribute('aria-label', testContent); + }); + + it('should render with custom line clamp value', () => { + const testContent = "Another long string here."; + const customLines = 5; + render({testContent}); + + const element = screen.getByTestId('truncate-element'); + + expect(element.style.WebkitLineClamp).toBe(String(customLines)); + + expect(element).toHaveAttribute('title', testContent); + expect(element).toHaveAttribute('aria-label', testContent); + }); + + it('should not call assembleStringFromChildrenArray if children is a string', () => { + const testContent = "Simple string content."; + render({testContent}); + + expect(assembleStringFromChildrenArray).not.toHaveBeenCalled(); }); - it('render with onTruncate', () => { - const mockFn = jest.fn(); - render( - - Learners, course teams, researchers, developers. - , - ); - expect(mockFn).toHaveBeenCalledTimes(2); + + it('should call assembleStringFromChildrenArray if children is complex', () => { + // Complex children structure (an array of elements) + const complexChildren = [ + Part A, + Part B, + "Part C", + ]; + + (assembleStringFromChildrenArray).mockReturnValue('This is the mocked full string.'); + + render({complexChildren}); + + expect(assembleStringFromChildrenArray).toHaveBeenCalledTimes(1); + + expect(assembleStringFromChildrenArray).toHaveBeenCalledWith(complexChildren); }); -}); +}); \ No newline at end of file diff --git a/src/Truncate/Truncate.tsx b/src/Truncate/Truncate.tsx new file mode 100644 index 0000000000..a2416a476c --- /dev/null +++ b/src/Truncate/Truncate.tsx @@ -0,0 +1,30 @@ +import React from "react"; +import { assembleStringFromChildrenArray } from './utils'; + +interface TruncateProps { + /** The expected text to which the ellipsis would be applied. */ + children: React.ReactNode; + /** The number of lines the text to be truncated to. */ + lines?: number; +} + +function Truncate({ children, lines = 1}: TruncateProps) { + const style = { + WebkitLineClamp: lines, + } + const initialText = typeof children === 'string' ? children : assembleStringFromChildrenArray(children); + + return ( +

+ {children} +

+ ) +} + +export default Truncate; diff --git a/src/Truncate/TruncateDeprecated.jsx b/src/Truncate/TruncateDeprecated.jsx new file mode 100644 index 0000000000..4caa32f36a --- /dev/null +++ b/src/Truncate/TruncateDeprecated.jsx @@ -0,0 +1,71 @@ +// TODO: assess what it would take to move to TypeScript + +import React, { + useLayoutEffect, useRef, useEffect, +} from 'react'; +import PropTypes from 'prop-types'; +import { truncateLines } from './utils'; +import useWindowSize from '../hooks/useWindowSizeHook'; + +const DEFAULT_TRUNCATE_LINES = 1; +const DEFAULT_TRUNCATE_ELLIPSIS = '...'; +const DEFAULT_TRUNCATE_ELEMENT_TYPE = 'div'; + +function TruncateDeprecated({ + children, lines, ellipsis, elementType, className, whiteSpace, onTruncate, +}) { + const textContainer = useRef(); + const { width } = useWindowSize(); + + useLayoutEffect(() => { + if (textContainer.current) { + const [truncated, original] = truncateLines(children, textContainer.current, { + ellipsis, + whiteSpace, + lines, + }); + textContainer.current.setAttribute('title', original); + textContainer.current.setAttribute('aria-label', original); + textContainer.current.innerHTML = ''; + truncated.forEach(element => { + textContainer.current.appendChild(element); + }); + if (onTruncate) { + onTruncate(truncated); + } + } + }, [children, ellipsis, lines, onTruncate, whiteSpace, width]); + + return React.createElement(elementType, { + ref: textContainer, + className, + }); +} + +TruncateDeprecated.propTypes = { + /** The expected text to which the ellipsis would be applied. */ + children: PropTypes.string.isRequired, + /** The number of lines the text to be truncated to. */ + lines: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + /** Text content for the ellipsis - will appear after the truncated lines. */ + ellipsis: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.node]), + /** Adds the whitespace from before the ellipsis. */ + whiteSpace: PropTypes.bool, + /** Custom html element for truncated text. */ + elementType: PropTypes.string, + /** Specifies class name to append to the base element. */ + className: PropTypes.string, + /** Callback fired when a text truncating */ + onTruncate: PropTypes.func, +}; + +TruncateDeprecated.defaultProps = { + lines: DEFAULT_TRUNCATE_LINES, + ellipsis: DEFAULT_TRUNCATE_ELLIPSIS, + whiteSpace: false, + elementType: DEFAULT_TRUNCATE_ELEMENT_TYPE, + className: undefined, + onTruncate: undefined, +}; + +export default TruncateDeprecated; diff --git a/src/Truncate/TruncateDeprecated.test.jsx b/src/Truncate/TruncateDeprecated.test.jsx new file mode 100644 index 0000000000..d1cc120c85 --- /dev/null +++ b/src/Truncate/TruncateDeprecated.test.jsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import Truncate from '.'; + +describe('', () => { + render( + + Learners, course teams, researchers, developers. + , + ); + it('render with className', () => { + const element = screen.getByText(/Learners, course teams, researchers, developers./i); + expect(element).toBeTruthy(); + expect(element.className).toContain('pgn__truncate'); + expect(element.getAttribute('aria-label')).toBe('Learners, course teams, researchers, developers.'); + expect(element.getAttribute('title')).toBe('Learners, course teams, researchers, developers.'); + }); + it('render with onTruncate', () => { + const mockFn = jest.fn(); + render( + + Learners, course teams, researchers, developers. + , + ); + expect(mockFn).toHaveBeenCalledTimes(2); + }); +}); diff --git a/src/Truncate/index.jsx b/src/Truncate/index.jsx index 292b7e2b7f..0499710bd0 100644 --- a/src/Truncate/index.jsx +++ b/src/Truncate/index.jsx @@ -1,78 +1,6 @@ -import React, { - useLayoutEffect, useRef, useEffect, -} from 'react'; -import PropTypes from 'prop-types'; -import { truncateLines } from './utils'; -import useWindowSize from '../hooks/useWindowSizeHook'; +import Truncate from './Truncate'; +import TruncateDeprecated from './TruncateDeprecated'; -const DEFAULT_TRUNCATE_LINES = 1; -const DEFAULT_TRUNCATE_ELLIPSIS = '...'; -const DEFAULT_TRUNCATE_ELEMENT_TYPE = 'div'; - -function TruncateDeprecated({ - children, lines, ellipsis, elementType, className, whiteSpace, onTruncate, -}) { - const textContainer = useRef(); - const { width } = useWindowSize(); - - useLayoutEffect(() => { - if (textContainer.current) { - const [truncated, original] = truncateLines(children, textContainer.current, { - ellipsis, - whiteSpace, - lines, - }); - textContainer.current.setAttribute('title', original); - textContainer.current.setAttribute('aria-label', original); - textContainer.current.innerHTML = ''; - truncated.forEach(element => { - textContainer.current.appendChild(element); - }); - if (onTruncate) { - onTruncate(truncated); - } - } - }, [children, ellipsis, lines, onTruncate, whiteSpace, width]); - - return React.createElement(elementType, { - ref: textContainer, - className, - }); -} - -TruncateDeprecated.propTypes = { - /** The expected text to which the ellipsis would be applied. */ - children: PropTypes.string.isRequired, - /** The number of lines the text to be truncated to. */ - lines: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - /** Text content for the ellipsis - will appear after the truncated lines. */ - ellipsis: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.node]), - /** Adds the whitespace from before the ellipsis. */ - whiteSpace: PropTypes.bool, - /** Custom html element for truncated text. */ - elementType: PropTypes.string, - /** Specifies class name to append to the base element. */ - className: PropTypes.string, - /** Callback fired when a text truncating */ - onTruncate: PropTypes.func, -}; - -TruncateDeprecated.defaultProps = { - lines: DEFAULT_TRUNCATE_LINES, - ellipsis: DEFAULT_TRUNCATE_ELLIPSIS, - whiteSpace: false, - elementType: DEFAULT_TRUNCATE_ELEMENT_TYPE, - className: undefined, - onTruncate: undefined, -}; - -function Truncate() { - useEffect(() => { - // eslint-disable-next-line no-console - console.log('Please use Truncate.Deprecated until a replacement is created'); - }, []); - return null; -} Truncate.Deprecated = TruncateDeprecated; export default Truncate; diff --git a/src/Truncate/index.scss b/src/Truncate/index.scss new file mode 100644 index 0000000000..b6f94dc992 --- /dev/null +++ b/src/Truncate/index.scss @@ -0,0 +1,6 @@ +.truncate-text { + overflow: hidden; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 1; +} diff --git a/src/Truncate/utils.d.ts b/src/Truncate/utils.d.ts new file mode 100644 index 0000000000..294bd932d2 --- /dev/null +++ b/src/Truncate/utils.d.ts @@ -0,0 +1 @@ +export function assembleStringFromChildrenArray(children: React.Node, elementsData: Array = []): string; \ No newline at end of file diff --git a/src/Truncate/utils.js b/src/Truncate/utils.js index 380805c24a..3ae39c1ae8 100644 --- a/src/Truncate/utils.js +++ b/src/Truncate/utils.js @@ -159,4 +159,5 @@ module.exports = { truncateLines, constructChildren, createCopyElement, + assembleStringFromChildrenArray, }; diff --git a/src/Truncate/utilsNew.ts b/src/Truncate/utilsNew.ts new file mode 100644 index 0000000000..934a1d6b29 --- /dev/null +++ b/src/Truncate/utilsNew.ts @@ -0,0 +1,80 @@ +interface ElementDataEntry { + type: React.ElementType | string | null; + props: Record | null; + start: number; + end: number; + children: ElementDataEntry[] | null; +} + +/** + * Retrieves plain string from children array and collects data + * to be able to restore original children in the future. + * + * @param {array} children + * @param {array} elementsData original data to restore children + * @returns string + */ +function assembleStringFromChildrenArray( + children: Array, + elementsData: Array = [], +): string { + + let result = ''; + + children?.forEach(child => { + const isStringOrNumber = typeof child === 'string' || typeof child === 'number'; + const isElement = React.isValidElement(child); + + // Default values if the child is a simple string/number + let currentChildren = null; + let childProps: Record | null = null; + let childType: React.ElementType | string | null = null; + + const start = result.length; + + if (isStringOrNumber) { + result += String(child); + } + else if (isElement) { + childProps = (child as React.ReactElement).props; + childType = (child as React.ReactElement).type; + + const elementChildren = childProps?.children; + const isElementChildrenStringOrNumber = typeof elementChildren === 'string' || typeof elementChildren === 'number'; + + if (isElementChildrenStringOrNumber) { + result += String(elementChildren); + + } else if (elementChildren) { + const nestedChildrenData: ElementDataEntry[] = []; + + // Handle React children normalization (convert single object to array) + const childrenArray = Array.isArray(elementChildren) + ? elementChildren + : [elementChildren]; // If it's a single element, wrap it in an array + + result += assembleStringFromChildrenArray( + childrenArray as Array, + nestedChildrenData, + ); + + currentChildren = nestedChildrenData; // Assign the results of the recursive call + } + } + const end = result.length; + + elementsData.push({ + type: childType, + props: childProps, + start, + end, + children: currentChildren, + }); + }); + + return result; +} + +module.exports = { + assembleStringFromChildrenArray, +}; \ No newline at end of file diff --git a/src/index.scss b/src/index.scss index 6b13fdd902..1ecdf93fd1 100644 --- a/src/index.scss +++ b/src/index.scss @@ -36,6 +36,7 @@ @import "./Stepper"; @import "./StatefulButton"; @import "./Tooltip"; +@import "./Truncate"; @import "./DataTable"; @import "./TransitionReplace"; @import "./ValidationMessage"; From cca4d3a02c04988c11718dff79b7db73303a363e Mon Sep 17 00:00:00 2001 From: MaxFrank13 Date: Fri, 12 Dec 2025 17:21:45 +0000 Subject: [PATCH 2/4] feat: truncate component --- src/Truncate/Truncate.tsx | 6 +- src/Truncate/TruncateDeprecated.jsx | 2 - src/Truncate/{index.jsx => index.js} | 0 src/Truncate/index.scss | 1 + src/Truncate/utils.d.ts | 1 - .../{utils.js => utils.deprecated.js} | 1 - ...utils.test.js => utils.deprecated.test.js} | 0 src/Truncate/utils.test.ts | 74 +++++++++++++++++++ src/Truncate/{utilsNew.ts => utils.ts} | 17 ++--- 9 files changed, 86 insertions(+), 16 deletions(-) rename src/Truncate/{index.jsx => index.js} (100%) delete mode 100644 src/Truncate/utils.d.ts rename src/Truncate/{utils.js => utils.deprecated.js} (99%) rename src/Truncate/{utils.test.js => utils.deprecated.test.js} (100%) create mode 100644 src/Truncate/utils.test.ts rename src/Truncate/{utilsNew.ts => utils.ts} (86%) diff --git a/src/Truncate/Truncate.tsx b/src/Truncate/Truncate.tsx index a2416a476c..290631ea72 100644 --- a/src/Truncate/Truncate.tsx +++ b/src/Truncate/Truncate.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { assembleStringFromChildrenArray } from './utils'; +import { assembleStringFromChildrenArray } from "./utils"; interface TruncateProps { /** The expected text to which the ellipsis would be applied. */ @@ -10,9 +10,11 @@ interface TruncateProps { function Truncate({ children, lines = 1}: TruncateProps) { const style = { + lineClamp: lines, WebkitLineClamp: lines, } - const initialText = typeof children === 'string' ? children : assembleStringFromChildrenArray(children); + + const initialText = Array.isArray(children) ? assembleStringFromChildrenArray(children) : String(children); return (

, key: string | null = null) => ({ + type: type, + props: props, + key: key, + $$typeof: Symbol.for('react.element'), +}); + +describe('utils', () => { + describe('assembleStringFromChildrenArray', () => { + it('should correctly assemble a string from a simple array of strings and numbers', () => { + const inputChildren = ['Hello', 123, ' World!']; + const elementsData: ElementDataEntry[] = []; + + const result = assembleStringFromChildrenArray(inputChildren, elementsData); + + expect(result).toBe('Hello123 World!'); + expect(elementsData.length).toBe(3); + + expect(elementsData[0].start).toBe(0); + expect(elementsData[0].end).toBe(5); + expect(elementsData[0].type).toBeNull(); + + expect(elementsData[1].start).toBe(5); + expect(elementsData[1].end).toBe(8); + expect(elementsData[1].type).toBeNull(); + + expect(elementsData[2].start).toBe(8); + expect(elementsData[2].end).toBe(15); + expect(elementsData[2].type).toBeNull(); + }); + + it('should handle a single React element with a simple string child', async () => { + const elementText = 'test-element-text' + const originalProps = { id: 1, children: elementText }; + const element = mockElement('span', originalProps); + const elementsData: ElementDataEntry[]= []; + const result = assembleStringFromChildrenArray([element], elementsData); + + expect(result).toBe(elementText); + + const dataEntry = elementsData[0]; + + expect(dataEntry.start).toBe(0); + expect(dataEntry.end).toBe(elementText.length); + expect(dataEntry.type).toBe('span'); + expect(dataEntry.props).toEqual(originalProps); + expect(dataEntry.children).toBeNull(); // No nested array since child was a string + }); + + it('should correctly handle a simple array of mixed strings and elements', () => { + const element1 = mockElement('a', { children: 'Link' }); + const elementsData: ElementDataEntry[] = []; + + const result = assembleStringFromChildrenArray(['Prefix: ', element1, ' Suffix'], elementsData); + + expect(result).toBe('Prefix: Link Suffix'); + expect(elementsData.length).toBe(3); + }); + + it('should recursively handle nested elements and collect nested data', () => { + const innerStrong = mockElement('strong', { children: 'Inner' }); + const outerDiv = mockElement('div', { children: ['Outer ', innerStrong] }); + const elementsData: ElementDataEntry[] = []; + + const result = assembleStringFromChildrenArray([outerDiv], elementsData); + + expect(result).toBe('Outer Inner'); + expect(elementsData.length).toBe(1); // Only the outer div is tracked at the top level + }); + }); +}); diff --git a/src/Truncate/utilsNew.ts b/src/Truncate/utils.ts similarity index 86% rename from src/Truncate/utilsNew.ts rename to src/Truncate/utils.ts index 934a1d6b29..457f3a67d7 100644 --- a/src/Truncate/utilsNew.ts +++ b/src/Truncate/utils.ts @@ -1,4 +1,6 @@ -interface ElementDataEntry { +import React from 'react'; + +export interface ElementDataEntry { type: React.ElementType | string | null; props: Record | null; start: number; @@ -14,7 +16,7 @@ interface ElementDataEntry { * @param {array} elementsData original data to restore children * @returns string */ -function assembleStringFromChildrenArray( +export function assembleStringFromChildrenArray( children: Array, elementsData: Array = [], ): string { @@ -47,8 +49,7 @@ function assembleStringFromChildrenArray( } else if (elementChildren) { const nestedChildrenData: ElementDataEntry[] = []; - - // Handle React children normalization (convert single object to array) + const childrenArray = Array.isArray(elementChildren) ? elementChildren : [elementChildren]; // If it's a single element, wrap it in an array @@ -58,7 +59,7 @@ function assembleStringFromChildrenArray( nestedChildrenData, ); - currentChildren = nestedChildrenData; // Assign the results of the recursive call + currentChildren = nestedChildrenData; } } const end = result.length; @@ -73,8 +74,4 @@ function assembleStringFromChildrenArray( }); return result; -} - -module.exports = { - assembleStringFromChildrenArray, -}; \ No newline at end of file +}; From b475fd81d05dbaf0ff4a09fd1a19c09bb1b57d78 Mon Sep 17 00:00:00 2001 From: MaxFrank13 Date: Fri, 12 Dec 2025 17:57:00 +0000 Subject: [PATCH 3/4] fix: lint --- src/Truncate/Truncate.test.jsx | 20 ++++++++++---------- src/Truncate/Truncate.tsx | 18 +++++++++--------- src/Truncate/TruncateDeprecated.jsx | 4 ++-- src/Truncate/utils.deprecated.test.js | 2 +- src/Truncate/utils.test.ts | 27 +++++++++++++-------------- src/Truncate/utils.ts | 23 +++++++++++------------ 6 files changed, 46 insertions(+), 48 deletions(-) diff --git a/src/Truncate/Truncate.test.jsx b/src/Truncate/Truncate.test.jsx index de612e940c..15236131e6 100644 --- a/src/Truncate/Truncate.test.jsx +++ b/src/Truncate/Truncate.test.jsx @@ -6,7 +6,7 @@ import { assembleStringFromChildrenArray } from './utils'; jest.mock('./utils', () => ({ assembleStringFromChildrenArray: jest.fn( - (children) => `Assembled text from ${children.length} elements` + (children) => `Assembled text from ${children.length} elements`, ), })); @@ -16,23 +16,23 @@ describe('Truncate Component', () => { }); it('should render with default line clamp of 1', () => { - const testContent = "This is a test string."; + const testContent = 'This is a test string.'; render({testContent}); const element = screen.getByTestId('truncate-element'); - expect(element.style.WebkitLineClamp).toBe('1'); + expect(element.style.WebkitLineClamp).toBe('1'); expect(element).toHaveAttribute('title', testContent); expect(element).toHaveAttribute('aria-label', testContent); }); it('should render with custom line clamp value', () => { - const testContent = "Another long string here."; + const testContent = 'Another long string here.'; const customLines = 5; render({testContent}); const element = screen.getByTestId('truncate-element'); - + expect(element.style.WebkitLineClamp).toBe(String(customLines)); expect(element).toHaveAttribute('title', testContent); @@ -40,18 +40,18 @@ describe('Truncate Component', () => { }); it('should not call assembleStringFromChildrenArray if children is a string', () => { - const testContent = "Simple string content."; + const testContent = 'Simple string content.'; render({testContent}); expect(assembleStringFromChildrenArray).not.toHaveBeenCalled(); }); - + it('should call assembleStringFromChildrenArray if children is complex', () => { // Complex children structure (an array of elements) const complexChildren = [ Part A, Part B, - "Part C", + 'Part C', ]; (assembleStringFromChildrenArray).mockReturnValue('This is the mocked full string.'); @@ -59,7 +59,7 @@ describe('Truncate Component', () => { render({complexChildren}); expect(assembleStringFromChildrenArray).toHaveBeenCalledTimes(1); - + expect(assembleStringFromChildrenArray).toHaveBeenCalledWith(complexChildren); }); -}); \ No newline at end of file +}); diff --git a/src/Truncate/Truncate.tsx b/src/Truncate/Truncate.tsx index 290631ea72..fa421b68f6 100644 --- a/src/Truncate/Truncate.tsx +++ b/src/Truncate/Truncate.tsx @@ -1,18 +1,18 @@ -import React from "react"; -import { assembleStringFromChildrenArray } from "./utils"; +import React from 'react'; +import { assembleStringFromChildrenArray } from './utils'; interface TruncateProps { - /** The expected text to which the ellipsis would be applied. */ - children: React.ReactNode; - /** The number of lines the text to be truncated to. */ - lines?: number; + /** The expected text to which the ellipsis would be applied. */ + children: React.ReactNode; + /** The number of lines the text to be truncated to. */ + lines?: number; } -function Truncate({ children, lines = 1}: TruncateProps) { +function Truncate({ children, lines = 1 }: TruncateProps) { const style = { lineClamp: lines, WebkitLineClamp: lines, - } + }; const initialText = Array.isArray(children) ? assembleStringFromChildrenArray(children) : String(children); @@ -26,7 +26,7 @@ function Truncate({ children, lines = 1}: TruncateProps) { > {children}

- ) + ); } export default Truncate; diff --git a/src/Truncate/TruncateDeprecated.jsx b/src/Truncate/TruncateDeprecated.jsx index d99b25ab05..e4274039f5 100644 --- a/src/Truncate/TruncateDeprecated.jsx +++ b/src/Truncate/TruncateDeprecated.jsx @@ -1,8 +1,8 @@ import React, { - useLayoutEffect, useRef, useEffect, + useLayoutEffect, useRef, } from 'react'; import PropTypes from 'prop-types'; -import { truncateLines } from './utils'; +import { truncateLines } from './utils.deprecated'; import useWindowSize from '../hooks/useWindowSizeHook'; const DEFAULT_TRUNCATE_LINES = 1; diff --git a/src/Truncate/utils.deprecated.test.js b/src/Truncate/utils.deprecated.test.js index 82ff116eb9..9e50f7dd8c 100644 --- a/src/Truncate/utils.deprecated.test.js +++ b/src/Truncate/utils.deprecated.test.js @@ -1,4 +1,4 @@ -import { constructChildren, cropText, truncateLines } from './utils'; +import { constructChildren, cropText, truncateLines } from './utils.deprecated'; const createElementMock = { parentNode: { diff --git a/src/Truncate/utils.test.ts b/src/Truncate/utils.test.ts index 04a885d767..6d3730605b 100644 --- a/src/Truncate/utils.test.ts +++ b/src/Truncate/utils.test.ts @@ -1,11 +1,10 @@ -import { assembleStringFromChildrenArray } from './utils'; -import { ElementDataEntry } from './utils'; +import { assembleStringFromChildrenArray, ElementDataEntry } from './utils'; const mockElement = (type: string, props: Record, key: string | null = null) => ({ - type: type, - props: props, - key: key, - $$typeof: Symbol.for('react.element'), + type, + props, + key, + $$typeof: Symbol.for('react.element'), }); describe('utils', () => { @@ -22,11 +21,11 @@ describe('utils', () => { expect(elementsData[0].start).toBe(0); expect(elementsData[0].end).toBe(5); expect(elementsData[0].type).toBeNull(); - + expect(elementsData[1].start).toBe(5); expect(elementsData[1].end).toBe(8); expect(elementsData[1].type).toBeNull(); - + expect(elementsData[2].start).toBe(8); expect(elementsData[2].end).toBe(15); expect(elementsData[2].type).toBeNull(); @@ -36,11 +35,11 @@ describe('utils', () => { const elementText = 'test-element-text' const originalProps = { id: 1, children: elementText }; const element = mockElement('span', originalProps); - const elementsData: ElementDataEntry[]= []; + const elementsData: ElementDataEntry[] = []; const result = assembleStringFromChildrenArray([element], elementsData); - + expect(result).toBe(elementText); - + const dataEntry = elementsData[0]; expect(dataEntry.start).toBe(0); @@ -49,11 +48,11 @@ describe('utils', () => { expect(dataEntry.props).toEqual(originalProps); expect(dataEntry.children).toBeNull(); // No nested array since child was a string }); - + it('should correctly handle a simple array of mixed strings and elements', () => { const element1 = mockElement('a', { children: 'Link' }); const elementsData: ElementDataEntry[] = []; - + const result = assembleStringFromChildrenArray(['Prefix: ', element1, ' Suffix'], elementsData); expect(result).toBe('Prefix: Link Suffix'); @@ -64,7 +63,7 @@ describe('utils', () => { const innerStrong = mockElement('strong', { children: 'Inner' }); const outerDiv = mockElement('div', { children: ['Outer ', innerStrong] }); const elementsData: ElementDataEntry[] = []; - + const result = assembleStringFromChildrenArray([outerDiv], elementsData); expect(result).toBe('Outer Inner'); diff --git a/src/Truncate/utils.ts b/src/Truncate/utils.ts index 457f3a67d7..a55d53dfd6 100644 --- a/src/Truncate/utils.ts +++ b/src/Truncate/utils.ts @@ -1,11 +1,11 @@ import React from 'react'; export interface ElementDataEntry { - type: React.ElementType | string | null; - props: Record | null; - start: number; - end: number; - children: ElementDataEntry[] | null; + type: React.ElementType | string | null; + props: Record | null; + start: number; + end: number; + children: ElementDataEntry[] | null; } /** @@ -18,9 +18,8 @@ export interface ElementDataEntry { */ export function assembleStringFromChildrenArray( children: Array, - elementsData: Array = [], + elementsData: Array = [], ): string { - let result = ''; children?.forEach(child => { @@ -50,8 +49,8 @@ export function assembleStringFromChildrenArray( } else if (elementChildren) { const nestedChildrenData: ElementDataEntry[] = []; - const childrenArray = Array.isArray(elementChildren) - ? elementChildren + const childrenArray = Array.isArray(elementChildren) + ? elementChildren : [elementChildren]; // If it's a single element, wrap it in an array result += assembleStringFromChildrenArray( @@ -61,9 +60,9 @@ export function assembleStringFromChildrenArray( currentChildren = nestedChildrenData; } - } + } const end = result.length; - + elementsData.push({ type: childType, props: childProps, @@ -74,4 +73,4 @@ export function assembleStringFromChildrenArray( }); return result; -}; +} From 947cdc2548f061619b3d52017137da3154b13c49 Mon Sep 17 00:00:00 2001 From: MaxFrank13 Date: Fri, 12 Dec 2025 18:01:46 +0000 Subject: [PATCH 4/4] fix: lint --- src/Truncate/utils.test.ts | 2 +- src/Truncate/utils.ts | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Truncate/utils.test.ts b/src/Truncate/utils.test.ts index 6d3730605b..86d7ec6a3b 100644 --- a/src/Truncate/utils.test.ts +++ b/src/Truncate/utils.test.ts @@ -32,7 +32,7 @@ describe('utils', () => { }); it('should handle a single React element with a simple string child', async () => { - const elementText = 'test-element-text' + const elementText = 'test-element-text'; const originalProps = { id: 1, children: elementText }; const element = mockElement('span', originalProps); const elementsData: ElementDataEntry[] = []; diff --git a/src/Truncate/utils.ts b/src/Truncate/utils.ts index a55d53dfd6..e0c67bcab2 100644 --- a/src/Truncate/utils.ts +++ b/src/Truncate/utils.ts @@ -25,7 +25,7 @@ export function assembleStringFromChildrenArray( children?.forEach(child => { const isStringOrNumber = typeof child === 'string' || typeof child === 'number'; const isElement = React.isValidElement(child); - + // Default values if the child is a simple string/number let currentChildren = null; let childProps: Record | null = null; @@ -35,8 +35,7 @@ export function assembleStringFromChildrenArray( if (isStringOrNumber) { result += String(child); - } - else if (isElement) { + } else if (isElement) { childProps = (child as React.ReactElement).props; childType = (child as React.ReactElement).type; @@ -45,7 +44,6 @@ export function assembleStringFromChildrenArray( if (isElementChildrenStringOrNumber) { result += String(elementChildren); - } else if (elementChildren) { const nestedChildrenData: ElementDataEntry[] = [];