Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions src/Truncate/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
---
title: 'Truncate'
type: 'component'
components:
- Truncate
status: 'New'
designStatus: 'Done'
devStatus: 'Done'
---

A Truncate component can help you crop multiline text. There will be three dots at the end of the text.

### Basic Usage

```jsx live
<Truncate lines={2}>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@PKulkoRaccoonGang Even though lines={2} in the "Basic Usage" example on the docs site, I'm seeing it with like 2.5 lines on initial render, until I click "Show code example"; then, it reverts to 2 lines as expected:

image

image

Any ideas what might be causing this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This behavior is caused by the width of the container, which is different for each theme. When using truncation by the consumer, there will be no such problems.

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.
</Truncate>
```

### With the custom ellipsis

```jsx live
<Truncate lines={2} ellipsis="🎉🎉🎉" whiteSpace>
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.
</Truncate>
```

### With the onTruncate

```jsx live
<Truncate lines={2} onTruncate={() => 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.
</Truncate>
```

### Example usage in Card

```jsx live
() => {
const isExtraSmall = useMediaQuery({ maxWidth: breakpoints.extraSmall.maxWidth });

return (
<Card style={{ width: isExtraSmall ? "100%" : "18rem" }} isClickable>
<Card.ImageCap
src="https://source.unsplash.com/360x200/?nature,flower"
srcAlt="Card image"
/>
<Card.Header
title={
<Truncate lines={2}>
Using Enhanced Capabilities In Your Course
</Truncate>}
/>
<Card.Section>
<Truncate lines={4}>
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.
</Truncate>
</Card.Section>
<Card.Footer
textElement={
<Truncate lines={2}>
Using Enhanced Capabilities In Your Course
</Truncate>}
>
<Button style={{ minWidth: 100 }}>Action 1</Button>
</Card.Footer>
</Card>
)
}
```
50 changes: 50 additions & 0 deletions src/Truncate/Truncate.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { constructString, cropText, truncateLines } from './utils';

describe('utils', () => {
describe('cropText', () => {
it('return the contents of the sliced text', () => {
const text = 'Learners, course teams, researchers, developers';
const cropDecrement = 0.70;
const croppedText = cropText(text, cropDecrement);
expect(croppedText).toEqual('Learners, course teams, research');
});
});

describe('constructString', () => {
it('return new string text after constructed', () => {
const string = 'Learners, course teams, researchers, developers';
const whiteSpace = true;
const ellipsis = '...';
const finalString = constructString(string, whiteSpace, ellipsis);
expect(finalString).toEqual('Learners, course teams, researchers, developers ...');
});
});

describe('truncateLines', () => {
it('returned truncate lines', () => {
const element = document.createElement('div');
jest.spyOn(document, 'createElement').mockReturnValue({
parentNode: {
removeChild: () => {},
},
scrollHeight: 220,
setAttribute: () => {},
set innerHTML(val) {
this.scrollHeight -= 60;
},
});

const text = 'Learners, course teams, researchers, developers: the edX community includes groups with '
+ 'a range of reasons for using the platform and objectives to accomplish.';
const lines = 2;
const whiteSpace = false;
const ellipsis = '___';
expect(truncateLines(text, element, {
lines,
whiteSpace,
ellipsis,
})).toEqual('Learners, course teams, researchers, developers: the edX community includes groups with a range of '
+ 'reasons for using the platform and objectives to accompl___');
});
});
});
20 changes: 20 additions & 0 deletions src/Truncate/Truncate.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React from 'react';
import { mount } from 'enzyme';
import Truncate from './index';

describe('<Truncate />', () => {
const wrapper = mount(
<Truncate>
Learners, course teams, researchers, developers.
</Truncate>,
);
it('render with className', () => {
wrapper.setProps({ className: 'pgn__truncate' });
expect(wrapper.hasClass('pgn__truncate')).toEqual(true);
});
it('render with onTruncate', () => {
const mockFn = jest.fn();
wrapper.setProps({ onTruncate: mockFn });
expect(mockFn.mock.calls.length).toBe(1);
});
});
63 changes: 63 additions & 0 deletions src/Truncate/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import React, {
useLayoutEffect, useRef, useState,
} from 'react';
import PropTypes from 'prop-types';
import { truncateLines } from './utils';
import { useWindowSize } from '../index';

const DEFAULT_TRUNCATE_LINES = 1;
const DEFAULT_TRUNCATE_ELLIPSIS = '...';
const DEFAULT_TRUNCATE_ELEMENT_TYPE = 'div';

const Truncate = ({
children, lines, ellipsis, elementType, className, whiteSpace, onTruncate,
}) => {
const [truncateText, setTruncateText] = useState('');
const textContainer = useRef();
const { width } = useWindowSize();

useLayoutEffect(() => {
const newTruncateText = truncateLines(children, textContainer.current, {
ellipsis,
whiteSpace,
lines,
});
setTruncateText(newTruncateText);
if (onTruncate) {
onTruncate(truncateText);
}
}, [children, ellipsis, lines, onTruncate, truncateText, whiteSpace, width]);

return React.createElement(elementType, {
ref: textContainer,
className,
}, truncateText);
};

Truncate.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,
};

Truncate.defaultProps = {
lines: DEFAULT_TRUNCATE_LINES,
ellipsis: DEFAULT_TRUNCATE_ELLIPSIS,
whiteSpace: false,
elementType: DEFAULT_TRUNCATE_ELEMENT_TYPE,
className: undefined,
onTruncate: undefined,
};

export default Truncate;
57 changes: 57 additions & 0 deletions src/Truncate/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
const LINE_HEIGHT_VALUE = 40;
const CROP_DECREMENT_STEP = 0.01;

const createCopyElement = (element) => {
const newElement = document.createElement(element.tagName);
newElement.setAttribute(
'style',
`line-height: ${LINE_HEIGHT_VALUE}px; display: inline-block;`,
);
return newElement;
};

const constructString = (text, whiteSpace, ellipsis) => {
const spacer = whiteSpace ? ' ' : '';
return `${text.trim()}${spacer}${ellipsis}`;
};

const cropText = (text, cropDecrement) => {
const sliceIndex = Math.floor(text.length * cropDecrement);
return text.slice(0, sliceIndex);
};

// This function truncates the original line of text by adding ellipsis,
// arbitrating the user's expected number of show lines, HTML element type,
// and whitespaces between the text and the ellipsis.
const truncateLines = (text, element, { lines, whiteSpace, ellipsis }) => {
Comment thread
peterkulko marked this conversation as resolved.
const visibilityArea = LINE_HEIGHT_VALUE * Number(lines);
const newElement = createCopyElement(element);
let truncateText = text;
let cropDecrement = 1;

element.append(newElement);
newElement.innerHTML = constructString(text, whiteSpace, ellipsis);
let newElementTextHeight = newElement.scrollHeight;

if (visibilityArea >= newElementTextHeight) {
newElement.parentNode.removeChild(newElement);
return truncateText;
}

while (newElementTextHeight > visibilityArea) {
cropDecrement -= CROP_DECREMENT_STEP;
truncateText = cropText(text, cropDecrement);
newElement.innerHTML = constructString(truncateText, whiteSpace, ellipsis);
newElementTextHeight = newElement.scrollHeight;
}

newElement.parentNode.removeChild(newElement);
return constructString(truncateText, whiteSpace, ellipsis);
};

module.exports = {
cropText,
truncateLines,
constructString,
createCopyElement,
};
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -193,3 +193,4 @@ export { default as Bubble } from './Bubble';
export { default as Dropzone } from './Dropzone';

export { default as messages } from './i18n';
export { default as Truncate } from './Truncate';