Skip to content

feat: truncate component rework#4037

Open
MaxFrank13 wants to merge 6 commits intoopenedx:release-23.xfrom
MaxFrank13:mfrank/truncate-component-refactor
Open

feat: truncate component rework#4037
MaxFrank13 wants to merge 6 commits intoopenedx:release-23.xfrom
MaxFrank13:mfrank/truncate-component-refactor

Conversation

@MaxFrank13
Copy link
Member

@MaxFrank13 MaxFrank13 commented Dec 11, 2025

Description

Corresponding issue: #3311

This refactors the Truncate component to use CSS properties instead of complex JS. The -webkit-line-clamp property has support for most common browsers (source). Eventually, line-clamp should have support but that is not currently the case.

This was discussed in Paragon Working Group (session notes).

Both title and aria-label are used for accessibility in the same way it was used before. The assembleStringFromChildrenArray utility function has been moved to a utils.ts file and thus converted to TypeScript.

The deprecated component as well as it's utils file still exist under the Truncate directory. The deprecated component is still accessible at Truncate.Deprecated. The utils file has been renamed to utils.deprecated. It didn't make sense to convert the entire utils file to TS when only the one utility function (assembleStringFromChildrenArray) was needed.

The tests for the deprecated component are under TruncateDeprecated.test.jsx.

src/
└── Truncate/
    ├── index.js                  
    ├── index.scss                
    ├── README.md                 
    ├── Truncate.test.jsx         
    ├── Truncate.tsx              
    ├── TruncateDeprecated.jsx    
    ├── TruncateDeprecated.test.jsx 
    ├── utils.deprecated.js       
    ├── utils.deprecated.test.js  
    ├── utils.test.ts             
    └── utils.ts 

Deploy Preview

https://deploy-preview-4037--paragon-openedx-v23.netlify.app/components/truncate/

Merge Checklist

  • If your update includes visual changes, have they been reviewed by a designer? Send them a link to the Netlify deploy preview, if applicable.
  • Does your change adhere to the documented style conventions?
  • Do any prop types have missing descriptions in the Props API tables in the documentation site (check deploy preview)?
  • Were your changes tested using all available themes (see theme switcher in the header of the deploy preview, under the "Settings" icon)?
  • Were your changes tested in the example app?
  • Is there adequate test coverage for your changes?
  • Consider whether this change needs to reviewed/QA'ed for accessibility (a11y). If so, please request an a11y review for the PR in the #wg-paragon Open edX Slack channel.

Post-merge Checklist

  • Verify your changes were released to NPM at the expected version.
  • If you'd like, share your contribution in #show-and-tell.
  • 🎉 🙌 Celebrate! Thanks for your contribution.

@netlify
Copy link

netlify bot commented Dec 11, 2025

Deploy Preview for paragon-openedx-v23 ready!

Built without sensitive environment variables

Name Link
🔨 Latest commit 3c4364b
🔍 Latest deploy log https://app.netlify.com/projects/paragon-openedx-v23/deploys/698ddcb05f67b000084c6873
😎 Deploy Preview https://deploy-preview-4037--paragon-openedx-v23.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@MaxFrank13 MaxFrank13 force-pushed the mfrank/truncate-component-refactor branch from d13ac64 to cca4d3a Compare December 12, 2025 17:22
@MaxFrank13 MaxFrank13 marked this pull request as ready for review December 12, 2025 18:12
@codecov
Copy link

codecov bot commented Dec 12, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 94.52%. Comparing base (fb678e3) to head (3c4364b).
⚠️ Report is 11 commits behind head on release-23.x.

Additional details and impacted files
@@               Coverage Diff                @@
##           release-23.x    #4037      +/-   ##
================================================
+ Coverage         94.39%   94.52%   +0.13%     
================================================
  Files               242      245       +3     
  Lines              4296     4330      +34     
  Branches            981      990       +9     
================================================
+ Hits               4055     4093      +38     
+ Misses              237      233       -4     
  Partials              4        4              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@brian-smith-tcril
Copy link
Contributor

On slack you mentioned needing to use inline styles. I wasn't able to completely get away from them but I was able to at least minimize it down to setting a css var

diff --git a/src/Truncate/Truncate.tsx b/src/Truncate/Truncate.tsx
index fa421b68f6..ab9dd95753 100644
--- a/src/Truncate/Truncate.tsx
+++ b/src/Truncate/Truncate.tsx
@@ -9,11 +9,6 @@ interface TruncateProps {
 }
 
 function Truncate({ children, lines = 1 }: TruncateProps) {
-  const style = {
-    lineClamp: lines,
-    WebkitLineClamp: lines,
-  };
-
   const initialText = Array.isArray(children) ? assembleStringFromChildrenArray(children) : String(children);
 
   return (
@@ -21,7 +16,7 @@ function Truncate({ children, lines = 1 }: TruncateProps) {
       title={initialText}
       aria-label={initialText}
       className="truncate-text"
-      style={style}
+      style={{ "--truncate-prop-lines": lines } as React.CSSProperties}
       data-testid="truncate-element"
     >
       {children}
diff --git a/src/Truncate/index.scss b/src/Truncate/index.scss
index a2f9fbde1f..418641008b 100644
--- a/src/Truncate/index.scss
+++ b/src/Truncate/index.scss
@@ -2,6 +2,6 @@
   overflow: hidden;
   display: -webkit-box;
   -webkit-box-orient: vertical;
-  -webkit-line-clamp: 1;
-  line-clamp: 1;
+  -webkit-line-clamp: var(--truncate-prop-lines);
+  line-clamp: var(--truncate-prop-lines);
 }

As for the naming of the variable, this is definitely setting a new pattern so I'd love to hear opinions. My reasoning behind --truncate-prop-lines is:

  • I want to make sure it's clear that this CSS var isn't related to tokens (I think just not having the --pgn- prefix helps with that)
  • I want someone reading the scss file to know where the var is coming from

@MaxFrank13
Copy link
Member Author

I want to make sure it's clear that this CSS var isn't related to tokens (I think just not having the --pgn- prefix helps with that)

Yes I think this makes sense and would be a clear way of indicating that this is a local variable and not a design token.

W.r.t the style conventions, it also mentions that all CSS classes should be prefixed with pgn__. I feel like this differs from the variable question. Should the CSS classes added in this PR have the pgn__ prefix? My inclination is to think yes because they are classes from Paragon for a specific Paragon component, and are not utility classes.

@bradenmacdonald
Copy link
Contributor

@MaxFrank13 What's the status of this PR?

@MaxFrank13
Copy link
Member Author

@MaxFrank13 What's the status of this PR?

Hey @bradenmacdonald! Just getting back from leave today. I believe this left off with us still considering what the best naming convention would be for the variable and class. If there are no objections to what Brian and I have mentioned in the earlier comments, then I can go ahead and make those changes. Curious if you have any thoughts on this?

Copy link
Contributor

@bradenmacdonald bradenmacdonald left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MaxFrank13 Great!

Yeah, I tend to agree that the CSS classes should have the pgn__ prefix, and the CSS var is better as just --truncate-prop-lines, since it's locally scoped to this component anyways.

*/
export function assembleStringFromChildrenArray(
children: Array<React.ReactNode>,
elementsData: Array<ElementDataEntry> = [],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: Modifying a passed array is a bit of an anti-pattern, unless the name of the function makes it obvious.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah good call out! I've refactored so that we no longer use a passed array but instead initialize one within the function. In order for the recursion to work, this required returning the array as well as the string result. Because of this, the usage in Truncate.tsx had to be modified as well. I believe it's working exactly the same but would invite any criticisms there as well!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! I haven't tested this though. You also have a bunch of lint warnings now.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah! Something weird going on with VScode and the lint extension. Weird it only happens in Paragon repo for me. I can still run the npm run lint command though so I just did it that way 🤷

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants