-
Notifications
You must be signed in to change notification settings - Fork 98
feat: truncate component rework #4037
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: release-23.x
Are you sure you want to change the base?
Changes from all commits
5880665
cca4d3a
b475fd8
947cdc2
942a00d
3c4364b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,27 +1,65 @@ | ||
| import React from 'react'; | ||
| import '@testing-library/jest-dom'; | ||
| import { render, screen } from '@testing-library/react'; | ||
| import Truncate from '.'; | ||
|
|
||
| describe('<Truncate />', () => { | ||
| render( | ||
| <Truncate.Deprecated className="pgn__truncate"> | ||
| Learners, course teams, researchers, developers. | ||
| </Truncate.Deprecated>, | ||
| ); | ||
| 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(<Truncate>{testContent}</Truncate>); | ||
|
|
||
| const element = screen.getByTestId('truncate-element'); | ||
| expect(element.style.getPropertyValue('--truncate-prop-lines')).toBe('1'); | ||
|
|
||
| expect(element).toHaveAttribute('title', testContent); | ||
| expect(element).toHaveAttribute('aria-label', testContent); | ||
| }); | ||
| it('render with onTruncate', () => { | ||
| const mockFn = jest.fn(); | ||
| render( | ||
| <Truncate.Deprecated className="pgn__truncate" onTruncate={mockFn}> | ||
| Learners, course teams, researchers, developers. | ||
| </Truncate.Deprecated>, | ||
| ); | ||
| expect(mockFn).toHaveBeenCalledTimes(2); | ||
|
|
||
| it('should render with custom line clamp value', () => { | ||
| const testContent = 'Another long string here.'; | ||
| const customLines = 5; | ||
| render(<Truncate lines={customLines}>{testContent}</Truncate>); | ||
|
|
||
| const element = screen.getByTestId('truncate-element'); | ||
|
|
||
| expect(element.style.getPropertyValue('--truncate-prop-lines')).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(<Truncate>{testContent}</Truncate>); | ||
|
|
||
| expect(assembleStringFromChildrenArray).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it('should call assembleStringFromChildrenArray if children is complex', () => { | ||
| // Complex children structure (an array of elements) | ||
| const complexChildren = [ | ||
| <span key="a">Part A</span>, | ||
| <strong key="b">Part B</strong>, | ||
| 'Part C', | ||
| ]; | ||
|
|
||
| (assembleStringFromChildrenArray).mockReturnValue('This is the mocked full string.'); | ||
|
|
||
| render(<Truncate>{complexChildren}</Truncate>); | ||
|
|
||
| expect(assembleStringFromChildrenArray).toHaveBeenCalledTimes(1); | ||
|
|
||
| expect(assembleStringFromChildrenArray).toHaveBeenCalledWith(complexChildren); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,33 @@ | ||||||
| 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) { | ||||||
| let initialText: string = ''; | ||||||
| if (Array.isArray(children)) { | ||||||
| const { result } = assembleStringFromChildrenArray(children); | ||||||
| initialText = result; | ||||||
| } else { | ||||||
| initialText = String(children); | ||||||
| } | ||||||
|
Comment on lines
+13
to
+18
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||
|
|
||||||
| return ( | ||||||
| <p | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Having this hardcoded to The other question is what a reasonable default would be, I'm thinking |
||||||
| title={initialText} | ||||||
| aria-label={initialText} | ||||||
| className="truncate-text" | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I checked the deploy preview (https://deploy-preview-4037--paragon-openedx-v23.netlify.app/components/truncate/) and it didn't appear to be working at all. I think this is likely the culprit.
Suggested change
|
||||||
| style={{ '--truncate-prop-lines': lines } as React.CSSProperties} | ||||||
| data-testid="truncate-element" | ||||||
| > | ||||||
| {children} | ||||||
| </p> | ||||||
| ); | ||||||
| } | ||||||
|
|
||||||
| export default Truncate; | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| import React from 'react'; | ||
| import { render, screen } from '@testing-library/react'; | ||
| import Truncate from '.'; | ||
|
|
||
| describe('<Truncate />', () => { | ||
| render( | ||
| <Truncate.Deprecated className="pgn__truncate"> | ||
| Learners, course teams, researchers, developers. | ||
| </Truncate.Deprecated>, | ||
| ); | ||
| 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( | ||
| <Truncate.Deprecated className="pgn__truncate" onTruncate={mockFn}> | ||
| Learners, course teams, researchers, developers. | ||
| </Truncate.Deprecated>, | ||
| ); | ||
| expect(mockFn).toHaveBeenCalledTimes(2); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| import Truncate from './Truncate'; | ||
| import TruncateDeprecated from './TruncateDeprecated'; | ||
|
|
||
| Truncate.Deprecated = TruncateDeprecated; | ||
|
|
||
| export default Truncate; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| .pgn__truncate-text { | ||
| overflow: hidden; | ||
| display: -webkit-box; | ||
| -webkit-box-orient: vertical; | ||
| -webkit-line-clamp: var(--truncate-prop-lines); | ||
| line-clamp: var(--truncate-prop-lines); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| import React from 'react'; | ||
| import { assembleStringFromChildrenArray } from './utils'; | ||
|
|
||
| const mockElement = (type: string, props: Record<string, any>) => React.createElement(type, props); | ||
|
|
||
| describe('utils', () => { | ||
| describe('assembleStringFromChildrenArray', () => { | ||
| it('should correctly assemble a string from a simple array of strings and numbers', () => { | ||
| const inputChildren = ['Hello', 123, ' World!']; | ||
|
|
||
| const { result, elementsData } = assembleStringFromChildrenArray(inputChildren); | ||
|
|
||
| expect(result).toBe('Hello123 World!'); | ||
| expect(elementsData).toHaveLength(3); | ||
|
|
||
| expect(elementsData[0]).toMatchObject({ start: 0, end: 5, type: null }); | ||
| expect(elementsData[1]).toMatchObject({ start: 5, end: 8, type: null }); | ||
| expect(elementsData[2]).toMatchObject({ start: 8, end: 15, type: null }); | ||
| }); | ||
|
|
||
| it('should handle a single React element with a simple string child', () => { | ||
| const elementText = 'test-element-text'; | ||
| const originalProps = { id: 1, children: elementText }; | ||
| const element = mockElement('span', originalProps); | ||
|
|
||
| const { result, elementsData } = assembleStringFromChildrenArray([element]); | ||
|
|
||
| expect(result).toBe(elementText); | ||
| expect(elementsData).toHaveLength(1); | ||
|
|
||
| const dataEntry = elementsData[0]; | ||
| expect(dataEntry.start).toBe(0); | ||
| expect(dataEntry.end).toBe(elementText.length); | ||
| expect(dataEntry.type).toBe('span'); | ||
| // Children is null because the child was a primitive string, not an element | ||
| expect(dataEntry.children).toBeNull(); | ||
| }); | ||
|
|
||
| it('should correctly handle a simple array of mixed strings and elements', () => { | ||
| const element1 = mockElement('a', { children: 'Link' }); | ||
|
|
||
| const { result, elementsData } = assembleStringFromChildrenArray(['Prefix: ', element1, ' Suffix']); | ||
|
|
||
| expect(result).toBe('Prefix: Link Suffix'); | ||
| expect(elementsData).toHaveLength(3); | ||
| expect(elementsData[1].type).toBe('a'); | ||
| }); | ||
|
|
||
| it('should recursively handle nested elements and collect nested data', () => { | ||
| const innerStrong = mockElement('strong', { children: 'Inner' }); | ||
| const outerDiv = mockElement('div', { children: ['Outer ', innerStrong] }); | ||
|
|
||
| const { result, elementsData } = assembleStringFromChildrenArray([outerDiv]); | ||
|
|
||
| expect(result).toBe('Outer Inner'); | ||
|
|
||
| expect(elementsData).toHaveLength(1); | ||
| const divData = elementsData[0]; | ||
| expect(divData.type).toBe('div'); | ||
|
|
||
| expect(divData.children).toHaveLength(2); | ||
| expect(divData.children?.[0].type).toBeNull(); | ||
| expect(divData.children?.[1].type).toBe('strong'); | ||
|
|
||
| expect(divData.children?.[1].start).toBe(6); | ||
| expect(divData.children?.[1].end).toBe(11); | ||
| }); | ||
| }); | ||
| }); |


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This functionality doesn't exist in the new version. My initial thought was that we should just remove this example, but I think the best thing to do would be to have 2 docs site pages (one for the new version and one for the deprecated version) until we fully remove the deprecated component.