-
Notifications
You must be signed in to change notification settings - Fork 4.7k
Components: Add truncate #28176
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
Merged
Merged
Components: Add truncate #28176
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| # Truncate | ||
|
|
||
| `Truncate` is a typography primitive that trims text content. For almost all cases, it is recommended that `Text`, `Heading`, or `Subheading` is used to render text content. However, `Truncate` is available for custom implementations. | ||
|
|
||
| ## Usage | ||
|
|
||
| ```jsx live | ||
| import { Truncate } from '@wp-g2/components'; | ||
|
|
||
| function Example() { | ||
| return ( | ||
| <Truncate> | ||
| Where the north wind meets the sea, there's a river full of memory. | ||
| Sleep, my darling, safe and sound, for in this river all is found. | ||
| In her waters, deep and true, lay the answers and a path for you. | ||
| Dive down deep into her sound, but not too far or you'll be drowned | ||
| </Truncate> | ||
| ); | ||
| } | ||
| ``` | ||
|
|
||
| ## Props | ||
|
|
||
| ##### ellipsis | ||
|
|
||
| **Type**: `string` | ||
|
|
||
| The ellipsis string when `truncate` is set. | ||
|
|
||
| ##### ellipsizeMode | ||
|
|
||
| **Type**: `"auto"`,`"head"`,`"tail"`,`"middle"` | ||
|
|
||
| Determines where to truncate. For example, we can truncate text right in the middle. To do this, we need to set `ellipsizeMode` to `middle` and a text `limit`. | ||
|
|
||
| - `auto`: Trims content at the end automatically without a `limit`. | ||
| - `head`: Trims content at the beginning. Requires a `limit`. | ||
| - `middle`: Trims content in the middle. Requires a `limit`. | ||
| - `tail`: Trims content at the end. Requires a `limit`. | ||
|
|
||
| ##### limit | ||
|
|
||
| **Type**: `number` | ||
|
|
||
| Determines the max characters when `truncate` is set. | ||
|
|
||
| ##### numberOfLines | ||
|
|
||
| **Type**: `number` | ||
|
|
||
| Clamps the text content to the specifiec `numberOfLines`, adding the `ellipsis` at the end. | ||
|
|
||
| ```jsx live | ||
| import { Truncate } from '@wp-g2/components'; | ||
|
|
||
| function Example() { | ||
| return ( | ||
| <Truncate numberOfLines={2}> | ||
| Where the north wind meets the sea, there's a river full of memory. | ||
| Sleep, my darling, safe and sound, for in this river all is found. | ||
| In her waters, deep and true, lay the answers and a path for you. | ||
| Dive down deep into her sound, but not too far or you'll be drowned | ||
| </Truncate> | ||
| ); | ||
| } | ||
| ``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| export { default as Truncate } from './truncate'; | ||
|
|
||
| export * from './use-truncate'; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| /** | ||
| * Internal dependencies | ||
| */ | ||
| import { Truncate } from '../index'; | ||
|
|
||
| export default { | ||
| component: Truncate, | ||
| title: 'Components/Truncate', | ||
| }; | ||
|
|
||
| export const _default = () => { | ||
| return ( | ||
| <Truncate numberOfLines={ 2 }> | ||
| Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut | ||
| facilisis dictum tortor, eu tincidunt justo scelerisque tincidunt. | ||
| Duis semper dui id augue malesuada, ut feugiat nisi aliquam. | ||
| Vestibulum venenatis diam sem, finibus dictum massa semper in. Nulla | ||
| facilisi. Nunc vulputate faucibus diam, in lobortis arcu ornare vel. | ||
| In dignissim nunc sed facilisis finibus. Etiam imperdiet mattis | ||
| arcu, sed rutrum sapien blandit gravida. Aenean sollicitudin neque | ||
| eget enim blandit, sit amet rutrum leo vehicula. Nunc malesuada | ||
| ultricies eros ut faucibus. Aliquam erat volutpat. Nulla nec feugiat | ||
| risus. Vivamus iaculis dui aliquet ante ultricies feugiat. | ||
| Vestibulum ante ipsum primis in faucibus orci luctus et ultrices | ||
| posuere cubilia curae; Vivamus nec pretium velit, sit amet | ||
| consectetur ante. Praesent porttitor ex eget fermentum mattis. | ||
| </Truncate> | ||
| ); | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| /** | ||
| * External dependencies | ||
| */ | ||
| import { render } from '@testing-library/react'; | ||
|
|
||
| /** | ||
| * Internal dependencies | ||
| */ | ||
| import { Truncate } from '../index'; | ||
|
|
||
| describe( 'props', () => { | ||
| test( 'should render correctly', () => { | ||
| const { container } = render( | ||
| <Truncate>Some people are worth melting for.</Truncate> | ||
| ); | ||
| expect( container.firstChild.textContent ).toEqual( | ||
| 'Some people are worth melting for.' | ||
| ); | ||
| } ); | ||
|
|
||
| test( 'should render limit', () => { | ||
| const { container } = render( | ||
| <Truncate limit={ 1 } ellipsizeMode="tail"> | ||
| Some | ||
| </Truncate> | ||
| ); | ||
| expect( container.firstChild.textContent ).toEqual( 'S…' ); | ||
| } ); | ||
|
|
||
| test( 'should render custom ellipsis', () => { | ||
| const { container } = render( | ||
| <Truncate ellipsis="!!!" limit={ 5 } ellipsizeMode="tail"> | ||
| Some people are worth melting for. | ||
| </Truncate> | ||
| ); | ||
| expect( container.firstChild.textContent ).toEqual( 'Some !!!' ); | ||
| } ); | ||
|
|
||
| test( 'should render custom ellipsizeMode', () => { | ||
| const { container } = render( | ||
| <Truncate ellipsis="!!!" ellipsizeMode="middle" limit={ 5 }> | ||
| Some people are worth melting for. | ||
| </Truncate> | ||
| ); | ||
| expect( container.firstChild.textContent ).toEqual( 'So!!!r.' ); | ||
| } ); | ||
| } ); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| /** | ||
| * External dependencies | ||
| */ | ||
| import { css } from '@wp-g2/styles'; | ||
|
|
||
| export const Truncate = css` | ||
| display: block; | ||
| overflow: hidden; | ||
| text-overflow: ellipsis; | ||
| white-space: nowrap; | ||
| `; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| /** | ||
| * External dependencies | ||
| */ | ||
| import { isNil } from 'lodash'; | ||
|
|
||
| export const TRUNCATE_ELLIPSIS = '…'; | ||
| export const TRUNCATE_TYPE = { | ||
| auto: 'auto', | ||
| head: 'head', | ||
| middle: 'middle', | ||
| tail: 'tail', | ||
| none: 'none', | ||
| }; | ||
|
|
||
| export const TRUNCATE_DEFAULT_PROPS = { | ||
| ellipsis: TRUNCATE_ELLIPSIS, | ||
| ellipsizeMode: TRUNCATE_TYPE.auto, | ||
| limit: 0, | ||
| numberOfLines: 0, | ||
| }; | ||
|
|
||
| // Source | ||
| // https://github.com/kahwee/truncate-middle | ||
| /** | ||
| * @param {string} word | ||
| * @param {number} headLength | ||
| * @param {number} tailLength | ||
| * @param {string} ellipsis | ||
| */ | ||
| export function truncateMiddle( word, headLength, tailLength, ellipsis ) { | ||
| if ( typeof word !== 'string' ) { | ||
| return ''; | ||
| } | ||
| const wordLength = word.length; | ||
| // Setting default values | ||
| // eslint-disable-next-line no-bitwise | ||
| const frontLength = ~~headLength; // will cast to integer | ||
| // eslint-disable-next-line no-bitwise | ||
| const backLength = ~~tailLength; | ||
| /* istanbul ignore next */ | ||
| const truncateStr = ! isNil( ellipsis ) ? ellipsis : TRUNCATE_ELLIPSIS; | ||
|
|
||
| if ( | ||
| ( frontLength === 0 && backLength === 0 ) || | ||
| frontLength >= wordLength || | ||
| backLength >= wordLength || | ||
| frontLength + backLength >= wordLength | ||
| ) { | ||
| return word; | ||
| } else if ( backLength === 0 ) { | ||
| return word.slice( 0, frontLength ) + truncateStr; | ||
| } | ||
| return ( | ||
| word.slice( 0, frontLength ) + | ||
| truncateStr + | ||
| word.slice( wordLength - backLength ) | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * | ||
| * @param {string} words | ||
| * @param {typeof TRUNCATE_DEFAULT_PROPS} props | ||
| */ | ||
| export function truncateContent( words = '', props ) { | ||
| const mergedProps = { ...TRUNCATE_DEFAULT_PROPS, ...props }; | ||
| const { ellipsis, ellipsizeMode, limit } = mergedProps; | ||
|
|
||
| if ( ellipsizeMode === TRUNCATE_TYPE.none ) { | ||
| return words; | ||
| } | ||
|
|
||
| let truncateHead; | ||
| let truncateTail; | ||
|
|
||
| switch ( ellipsizeMode ) { | ||
| case TRUNCATE_TYPE.head: | ||
| truncateHead = 0; | ||
| truncateTail = limit; | ||
| break; | ||
| case TRUNCATE_TYPE.middle: | ||
| truncateHead = Math.floor( limit / 2 ); | ||
| truncateTail = Math.floor( limit / 2 ); | ||
| break; | ||
| default: | ||
| truncateHead = limit; | ||
| truncateTail = 0; | ||
| } | ||
|
|
||
| const truncatedContent = | ||
| ellipsizeMode !== TRUNCATE_TYPE.auto | ||
| ? truncateMiddle( words, truncateHead, truncateTail, ellipsis ) | ||
| : words; | ||
|
|
||
| return truncatedContent; | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| /** | ||
| * Internal dependencies | ||
| */ | ||
| import { createComponent } from '../utils'; | ||
| import { useTruncate } from './use-truncate'; | ||
|
|
||
| export default createComponent( { | ||
| as: 'span', | ||
| useHook: useTruncate, | ||
| name: 'Truncate', | ||
| } ); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| /** | ||
| * External dependencies | ||
| */ | ||
| import { useContextSystem } from '@wp-g2/context'; | ||
| import { css, cx } from '@wp-g2/styles'; | ||
|
|
||
| /** | ||
| * WordPress dependencies | ||
| */ | ||
| import { useMemo } from '@wordpress/element'; | ||
|
|
||
| /** | ||
| * Internal dependencies | ||
| */ | ||
| import * as styles from './truncate-styles'; | ||
| import { | ||
| TRUNCATE_ELLIPSIS, | ||
| TRUNCATE_TYPE, | ||
| truncateContent, | ||
| } from './truncate-utils'; | ||
|
|
||
| /** | ||
| * @typedef Props | ||
| * @property {string} [ellipsis='...'] String to use to truncate the string with. | ||
| * @property {'auto' | 'head' | 'tail' | 'middle' | 'none'} [ellipsizeMode='auto'] Mode to follow. | ||
| * @property {number} [limit=0] Limit. | ||
| * @property {number} [numberOfLines=0] Number of lines. | ||
| */ | ||
|
|
||
| /** | ||
| * @param {import('@wp-g2/create-styles').ViewOwnProps<Props, 'span'>} props | ||
| */ | ||
| export function useTruncate( props ) { | ||
| const { | ||
| className, | ||
| children, | ||
| ellipsis = TRUNCATE_ELLIPSIS, | ||
| ellipsizeMode = TRUNCATE_TYPE.auto, | ||
| limit = 0, | ||
| numberOfLines = 0, | ||
| ...otherProps | ||
| } = useContextSystem( props, 'Truncate' ); | ||
|
|
||
| const truncatedContent = truncateContent( | ||
| typeof children === 'string' ? /** @type {string} */ ( children ) : '', | ||
| { | ||
| ellipsis, | ||
| ellipsizeMode, | ||
| limit, | ||
| numberOfLines, | ||
| } | ||
| ); | ||
|
|
||
| const shouldTruncate = ellipsizeMode === TRUNCATE_TYPE.auto; | ||
|
|
||
| const classes = useMemo( () => { | ||
| const sx = {}; | ||
|
|
||
| sx.numberOfLines = css` | ||
| -webkit-box-orient: vertical; | ||
| -webkit-line-clamp: ${ numberOfLines }; | ||
| display: -webkit-box; | ||
| overflow: hidden; | ||
| `; | ||
|
|
||
| return cx( | ||
| shouldTruncate && ! numberOfLines && styles.Truncate, | ||
| shouldTruncate && !! numberOfLines && sx.numberOfLines, | ||
| className | ||
| ); | ||
| }, [ className, numberOfLines, shouldTruncate ] ); | ||
|
|
||
| return { ...otherProps, className: classes, children: truncatedContent }; | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
I'm pretty sure this kind of truncating using substrings is language specific. How does this component behave in other languages? cc @swissspidy
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.
Some tests with strings in other languages would be good.
So if I understand this right,
truncateMiddle( 'Hello', 1, 1, '...')will returnH...o.Sounds OK when using Latin alphabet, but with strings like
こんにちは("Kon'nichiwa", "Hello" in Japanese) this will result inこ...は, which probably is less understandable.Not sure how it would work with Arabic, but you'd know that best :-)
Worst case we'd need some words/characters logic like the word count package has
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.
Interesting 🤔 ! if there's a library folks are aware of that could elegantly handle this, I think it would be a worth while addition :D.
For context, So far, we've only used it for
Truncating...handling.I based this component on something I've built in the past. Also heavily drawing inspiration from how
Textand truncation is handled in React Native:https://reactnative.dev/docs/text#ellipsizemode
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.
Adding more thoughts!
Taking a step back, for the use-cases of user interfaces, realistically the only situations were we'd need to
truncateMiddlewould be for long file names.For example:
thequickbrownfoxjumpsoverthelazydog.jpgmaybe becomes
theq...ydog.jpgThis is common for attachment related UIs or slug/tags.
In that case, it doesn't necessarily matter if the word/letters is linguistically understandable.
As long as there's enough to identify either the extension (e.g.
.jpg) or the file / keyword / slug :)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.
Excuse my idiot questions:
@wordpress/wordcountthat is very similar and that works across languages, If we keep this behavior, do you think there's a way to kind of consolidate that logic somehow?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.
I think we should include
middleas long as the use case that @ItsJonQ brought up is one that we'll need available at any time (or if block developers would find it useful for file names).We don't do any word counting in this component, just line counting. Was there a specific place @youknowriad that you would see us using
@wordpress/wordcountfor this component?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.
I guess I misunderstood the code but I thought there was some smart behavior to try to split between "words" or something like that in which case, it would be a similar algorithm to word count to figure out where are the words.
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.
There’s no such logic, as far as I can see. @ItsJonQ maybe you can chime in here to verify.
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.
I don't believe there's logic to split words - just splitting characters.