@instructure/ui-truncate-text 11.6.0 → 11.6.1-snapshot-129
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -277
- package/es/TruncateText/{index.js → v1/index.js} +1 -1
- package/es/TruncateText/v2/index.js +264 -0
- package/es/TruncateText/v2/props.js +26 -0
- package/es/TruncateText/v2/styles.js +58 -0
- package/es/TruncateText/v2/utils/cleanData.js +158 -0
- package/es/TruncateText/v2/utils/cleanString.js +53 -0
- package/es/TruncateText/v2/utils/measureText.js +74 -0
- package/es/TruncateText/v2/utils/truncate.js +341 -0
- package/es/{index.js → exports/a.js} +1 -1
- package/{src/index.ts → es/exports/b.js} +1 -2
- package/lib/TruncateText/{index.js → v1/index.js} +1 -1
- package/lib/TruncateText/v2/index.js +271 -0
- package/lib/TruncateText/v2/props.js +31 -0
- package/lib/TruncateText/v2/styles.js +64 -0
- package/lib/TruncateText/v2/utils/cleanData.js +164 -0
- package/lib/TruncateText/v2/utils/cleanString.js +59 -0
- package/lib/TruncateText/v2/utils/measureText.js +79 -0
- package/lib/TruncateText/v2/utils/truncate.js +350 -0
- package/lib/{index.js → exports/a.js} +2 -2
- package/lib/exports/b.js +12 -0
- package/package.json +42 -20
- package/src/TruncateText/{index.tsx → v1/index.tsx} +1 -1
- package/src/TruncateText/v2/README.md +230 -0
- package/src/TruncateText/v2/index.tsx +314 -0
- package/src/TruncateText/v2/props.ts +113 -0
- package/src/TruncateText/v2/styles.ts +60 -0
- package/src/TruncateText/v2/utils/cleanData.ts +178 -0
- package/src/TruncateText/v2/utils/cleanString.ts +61 -0
- package/src/TruncateText/v2/utils/measureText.ts +86 -0
- package/src/TruncateText/v2/utils/truncate.ts +451 -0
- package/src/exports/a.ts +25 -0
- package/src/exports/b.ts +25 -0
- package/tsconfig.build.tsbuildinfo +1 -1
- package/types/TruncateText/v1/index.d.ts.map +1 -0
- package/types/TruncateText/v1/props.d.ts.map +1 -0
- package/types/TruncateText/v1/styles.d.ts.map +1 -0
- package/types/TruncateText/v1/theme.d.ts.map +1 -0
- package/types/TruncateText/v1/utils/cleanData.d.ts.map +1 -0
- package/types/TruncateText/v1/utils/cleanString.d.ts.map +1 -0
- package/types/TruncateText/v1/utils/measureText.d.ts.map +1 -0
- package/types/TruncateText/v1/utils/truncate.d.ts.map +1 -0
- package/types/TruncateText/v2/index.d.ts +47 -0
- package/types/TruncateText/v2/index.d.ts.map +1 -0
- package/types/TruncateText/v2/props.d.ts +61 -0
- package/types/TruncateText/v2/props.d.ts.map +1 -0
- package/types/TruncateText/v2/styles.d.ts +15 -0
- package/types/TruncateText/v2/styles.d.ts.map +1 -0
- package/types/TruncateText/v2/utils/cleanData.d.ts +19 -0
- package/types/TruncateText/v2/utils/cleanData.d.ts.map +1 -0
- package/types/TruncateText/v2/utils/cleanString.d.ts +16 -0
- package/types/TruncateText/v2/utils/cleanString.d.ts.map +1 -0
- package/types/TruncateText/v2/utils/measureText.d.ts +13 -0
- package/types/TruncateText/v2/utils/measureText.d.ts.map +1 -0
- package/types/TruncateText/v2/utils/truncate.d.ts +35 -0
- package/types/TruncateText/v2/utils/truncate.d.ts.map +1 -0
- package/types/exports/a.d.ts +3 -0
- package/types/exports/a.d.ts.map +1 -0
- package/types/exports/b.d.ts +3 -0
- package/types/exports/b.d.ts.map +1 -0
- package/types/TruncateText/index.d.ts.map +0 -1
- package/types/TruncateText/props.d.ts.map +0 -1
- package/types/TruncateText/styles.d.ts.map +0 -1
- package/types/TruncateText/theme.d.ts.map +0 -1
- package/types/TruncateText/utils/cleanData.d.ts.map +0 -1
- package/types/TruncateText/utils/cleanString.d.ts.map +0 -1
- package/types/TruncateText/utils/measureText.d.ts.map +0 -1
- package/types/TruncateText/utils/truncate.d.ts.map +0 -1
- package/types/index.d.ts +0 -3
- package/types/index.d.ts.map +0 -1
- /package/es/TruncateText/{props.js → v1/props.js} +0 -0
- /package/es/TruncateText/{styles.js → v1/styles.js} +0 -0
- /package/es/TruncateText/{theme.js → v1/theme.js} +0 -0
- /package/es/TruncateText/{utils → v1/utils}/cleanData.js +0 -0
- /package/es/TruncateText/{utils → v1/utils}/cleanString.js +0 -0
- /package/es/TruncateText/{utils → v1/utils}/measureText.js +0 -0
- /package/es/TruncateText/{utils → v1/utils}/truncate.js +0 -0
- /package/lib/TruncateText/{props.js → v1/props.js} +0 -0
- /package/lib/TruncateText/{styles.js → v1/styles.js} +0 -0
- /package/lib/TruncateText/{theme.js → v1/theme.js} +0 -0
- /package/lib/TruncateText/{utils → v1/utils}/cleanData.js +0 -0
- /package/lib/TruncateText/{utils → v1/utils}/cleanString.js +0 -0
- /package/lib/TruncateText/{utils → v1/utils}/measureText.js +0 -0
- /package/lib/TruncateText/{utils → v1/utils}/truncate.js +0 -0
- /package/src/TruncateText/{README.md → v1/README.md} +0 -0
- /package/src/TruncateText/{props.ts → v1/props.ts} +0 -0
- /package/src/TruncateText/{styles.ts → v1/styles.ts} +0 -0
- /package/src/TruncateText/{theme.ts → v1/theme.ts} +0 -0
- /package/src/TruncateText/{utils → v1/utils}/cleanData.ts +0 -0
- /package/src/TruncateText/{utils → v1/utils}/cleanString.ts +0 -0
- /package/src/TruncateText/{utils → v1/utils}/measureText.ts +0 -0
- /package/src/TruncateText/{utils → v1/utils}/truncate.ts +0 -0
- /package/types/TruncateText/{index.d.ts → v1/index.d.ts} +0 -0
- /package/types/TruncateText/{props.d.ts → v1/props.d.ts} +0 -0
- /package/types/TruncateText/{styles.d.ts → v1/styles.d.ts} +0 -0
- /package/types/TruncateText/{theme.d.ts → v1/theme.d.ts} +0 -0
- /package/types/TruncateText/{utils → v1/utils}/cleanData.d.ts +0 -0
- /package/types/TruncateText/{utils → v1/utils}/cleanString.d.ts +0 -0
- /package/types/TruncateText/{utils → v1/utils}/measureText.d.ts +0 -0
- /package/types/TruncateText/{utils → v1/utils}/truncate.d.ts +0 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
---
|
|
2
|
+
describes: TruncateText
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
A component for truncating text content.
|
|
6
|
+
|
|
7
|
+
> For best results, avoid using TruncateText inside parent containers that are inline (`display: inline/inline-block`) or that default to inline display (span, link, etc.).
|
|
8
|
+
|
|
9
|
+
### Single line
|
|
10
|
+
|
|
11
|
+
```javascript
|
|
12
|
+
---
|
|
13
|
+
type: example
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
<div>
|
|
17
|
+
<View
|
|
18
|
+
as="div"
|
|
19
|
+
padding="xx-small none"
|
|
20
|
+
maxWidth="480px"
|
|
21
|
+
withVisualDebug
|
|
22
|
+
>
|
|
23
|
+
<Heading level="h1">
|
|
24
|
+
<TruncateText>
|
|
25
|
+
{lorem.paragraph()}
|
|
26
|
+
</TruncateText>
|
|
27
|
+
</Heading>
|
|
28
|
+
<Text
|
|
29
|
+
as="p"
|
|
30
|
+
weight="bold"
|
|
31
|
+
size="large"
|
|
32
|
+
transform="uppercase"
|
|
33
|
+
letterSpacing="expanded"
|
|
34
|
+
>
|
|
35
|
+
<TruncateText>
|
|
36
|
+
{lorem.paragraph()}
|
|
37
|
+
</TruncateText>
|
|
38
|
+
</Text>
|
|
39
|
+
<Text as="p">
|
|
40
|
+
<TruncateText>
|
|
41
|
+
{lorem.paragraph()}
|
|
42
|
+
</TruncateText>
|
|
43
|
+
</Text>
|
|
44
|
+
|
|
45
|
+
<div>
|
|
46
|
+
<TruncateText
|
|
47
|
+
onUpdate={(truncated, text) => {
|
|
48
|
+
console.log(truncated, text)
|
|
49
|
+
}}
|
|
50
|
+
>
|
|
51
|
+
<span>
|
|
52
|
+
Regular sized text with <Link href="#">A Text Link </Link>and <Text weight="bold">some bold text.</Text>
|
|
53
|
+
</span>
|
|
54
|
+
</TruncateText>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
</View>
|
|
58
|
+
</div>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Multiple lines
|
|
62
|
+
|
|
63
|
+
You can set the number of lines to display before truncation begins with the `maxLines` prop. Setting `maxLines` to `auto` will determine the number of lines that will fit.
|
|
64
|
+
|
|
65
|
+
```javascript
|
|
66
|
+
---
|
|
67
|
+
type: example
|
|
68
|
+
---
|
|
69
|
+
<div>
|
|
70
|
+
<View
|
|
71
|
+
as="div"
|
|
72
|
+
padding="small none"
|
|
73
|
+
maxWidth="480px"
|
|
74
|
+
withVisualDebug
|
|
75
|
+
>
|
|
76
|
+
<Text as="p" size="large">
|
|
77
|
+
<TruncateText
|
|
78
|
+
maxLines={2}
|
|
79
|
+
truncate="word"
|
|
80
|
+
ellipsis=" (...)"
|
|
81
|
+
>
|
|
82
|
+
{lorem.paragraph()}
|
|
83
|
+
</TruncateText>
|
|
84
|
+
<Link href="#">Read More</Link>
|
|
85
|
+
</Text>
|
|
86
|
+
|
|
87
|
+
<Text as="p" lineHeight="double">
|
|
88
|
+
<TruncateText
|
|
89
|
+
maxLines={4}
|
|
90
|
+
truncate="word"
|
|
91
|
+
ellipsis=" (...)"
|
|
92
|
+
>
|
|
93
|
+
Esse aliqua minim veniam duis consectetur non sunt ea deserunt qui cillum laboris officia. Minim nulla commodo dolore reprehenderit commodo occaecat veniam ad consectetur mollit consectetur partur consectetur eiusmod dolor incididunt incididunt.
|
|
94
|
+
</TruncateText>
|
|
95
|
+
<Link href="#">Read More</Link>
|
|
96
|
+
</Text>
|
|
97
|
+
</View>
|
|
98
|
+
<br />
|
|
99
|
+
<View
|
|
100
|
+
as="div"
|
|
101
|
+
padding="small none"
|
|
102
|
+
maxWidth="480px"
|
|
103
|
+
withVisualDebug
|
|
104
|
+
>
|
|
105
|
+
<Text as="p">
|
|
106
|
+
<TruncateText maxLines={4} ellipsis=" (...)">
|
|
107
|
+
<span>Esse aliqua minim veniam duis consectetur non sunt ea deserunt qui cillum laboris officia. <Link href="#">http://instructure.github.io/instructure-ui/#ui-elements</Link> occaecat veniam ad consectetur mollit consectetur partur consectetur eiusmod dolor incididunt incididunt.</span>
|
|
108
|
+
</TruncateText>
|
|
109
|
+
</Text>
|
|
110
|
+
|
|
111
|
+
<Text as="p">
|
|
112
|
+
<TruncateText maxLines={4} ellipsis=" (...)">
|
|
113
|
+
<span>Qui cillum laboris officia. <strong>supercalifragilisticexpialidocious</strong> occaecat veniam ad consectetur mollit consectetur partur consectetur eiusmod dolor incididunt incididunt. Esse aliqua minim veniam duis consectetur non sunt ea deserunt.</span>
|
|
114
|
+
</TruncateText>
|
|
115
|
+
</Text>
|
|
116
|
+
</View>
|
|
117
|
+
<br />
|
|
118
|
+
<div style={{height: '78px', border: 'solid 1px red'}}>
|
|
119
|
+
<Text>
|
|
120
|
+
<TruncateText maxLines="auto" ellipsis=" (...)">
|
|
121
|
+
Esse aliqua minim veniam duis consectetur non sunt ea deserunt qui cillum laboris officia. Minim nulla commodo dolore reprehenderit commodo occaecat veniam ad consectetur mollit consectetur partur consectetur eiusmod dolor incididunt incididunt.
|
|
122
|
+
</TruncateText>
|
|
123
|
+
</Text>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Truncate middle
|
|
130
|
+
|
|
131
|
+
You can set the position of the truncation using the `position` prop.
|
|
132
|
+
|
|
133
|
+
```javascript
|
|
134
|
+
---
|
|
135
|
+
type: example
|
|
136
|
+
---
|
|
137
|
+
<div>
|
|
138
|
+
<View
|
|
139
|
+
as="div"
|
|
140
|
+
padding="small none"
|
|
141
|
+
maxWidth="480px"
|
|
142
|
+
withVisualDebug
|
|
143
|
+
>
|
|
144
|
+
<Text as="p">
|
|
145
|
+
<TruncateText position="middle">
|
|
146
|
+
<span>This line of text should be truncated from the middle of the string <strong>instead of the end.</strong></span>
|
|
147
|
+
</TruncateText>
|
|
148
|
+
</Text>
|
|
149
|
+
</View>
|
|
150
|
+
<br />
|
|
151
|
+
<View
|
|
152
|
+
as="div"
|
|
153
|
+
padding="small none"
|
|
154
|
+
maxWidth="480px"
|
|
155
|
+
withVisualDebug
|
|
156
|
+
>
|
|
157
|
+
<Link href="#">
|
|
158
|
+
<TruncateText
|
|
159
|
+
position="middle"
|
|
160
|
+
truncate="word"
|
|
161
|
+
ellipsis=".../"
|
|
162
|
+
>
|
|
163
|
+
@instructure/ui-elements/somefakedir/tomakethislonger/lib/longer/TruncateText
|
|
164
|
+
</TruncateText>
|
|
165
|
+
</Link>
|
|
166
|
+
</View>
|
|
167
|
+
</div>
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Using tooltips
|
|
171
|
+
|
|
172
|
+
It's best practice to make the complete text of a truncated element available via a [Tooltip](Tooltip).
|
|
173
|
+
|
|
174
|
+
```js
|
|
175
|
+
---
|
|
176
|
+
type: example
|
|
177
|
+
---
|
|
178
|
+
const Example = (props) => {
|
|
179
|
+
const [isTruncated, setIsTruncated] = useState(false)
|
|
180
|
+
|
|
181
|
+
const handleUpdate = (newIsTruncated) => {
|
|
182
|
+
if (isTruncated !== newIsTruncated) {
|
|
183
|
+
setIsTruncated(newIsTruncated)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const renderLink = () => (
|
|
188
|
+
<Link href="#">
|
|
189
|
+
<TruncateText onUpdate={handleUpdate}>{props.message}</TruncateText>
|
|
190
|
+
</Link>
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
return (
|
|
194
|
+
<View as="div" padding="xx-small none" maxWidth="230px" withVisualDebug>
|
|
195
|
+
{isTruncated ? (
|
|
196
|
+
<Tooltip
|
|
197
|
+
renderTip={props.message}
|
|
198
|
+
mountNode={() => document.getElementById('main')}
|
|
199
|
+
>
|
|
200
|
+
{renderLink()}
|
|
201
|
+
</Tooltip>
|
|
202
|
+
) : (
|
|
203
|
+
renderLink()
|
|
204
|
+
)}
|
|
205
|
+
</View>
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
render(
|
|
209
|
+
<Example message="A tooltip will display only when this text is truncated" />
|
|
210
|
+
)
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Guidelines
|
|
214
|
+
|
|
215
|
+
```js
|
|
216
|
+
---
|
|
217
|
+
type: embed
|
|
218
|
+
---
|
|
219
|
+
<Guidelines>
|
|
220
|
+
<Figure recommendation="yes" title="Do">
|
|
221
|
+
<Figure.Item>Use a <Link href="/#Tooltip">Tooltip</Link> for all truncated items</Figure.Item>
|
|
222
|
+
<Figure.Item>Use when trying to restrict the number of lines that are visible</Figure.Item>
|
|
223
|
+
<Figure.Item>Use end ellipsis if the unique identifier is at the beginning of the string</Figure.Item>
|
|
224
|
+
<Figure.Item>Use middle ellipsis if the unique identifier is at the end of the string</Figure.Item>
|
|
225
|
+
</Figure>
|
|
226
|
+
<Figure recommendation="no" title="Don't">
|
|
227
|
+
<Figure.Item>Use in <Link href="/#Button">Buttons</Link>, <Link href="/#Navigation">Nav Items</Link>, <Link href="/#TabList">TabLists</Link></Figure.Item>
|
|
228
|
+
</Figure>
|
|
229
|
+
</Guidelines>
|
|
230
|
+
```
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The MIT License (MIT)
|
|
3
|
+
*
|
|
4
|
+
* Copyright (c) 2015 - present Instructure, Inc.
|
|
5
|
+
*
|
|
6
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
* in the Software without restriction, including without limitation the rights
|
|
9
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
* furnished to do so, subject to the following conditions:
|
|
12
|
+
*
|
|
13
|
+
* The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
* copies or substantial portions of the Software.
|
|
15
|
+
*
|
|
16
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
* SOFTWARE.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { Children, Component, type JSX } from 'react'
|
|
26
|
+
|
|
27
|
+
import { debounce } from '@instructure/debounce'
|
|
28
|
+
import type { Debounced } from '@instructure/debounce'
|
|
29
|
+
import { canUseDOM, getBoundingClientRect } from '@instructure/ui-dom-utils'
|
|
30
|
+
import {
|
|
31
|
+
safeCloneElement,
|
|
32
|
+
ensureSingleChild
|
|
33
|
+
} from '@instructure/ui-react-utils'
|
|
34
|
+
import { logError as error } from '@instructure/console'
|
|
35
|
+
import { withStyle } from '@instructure/emotion'
|
|
36
|
+
|
|
37
|
+
import generateStyle from './styles'
|
|
38
|
+
|
|
39
|
+
import truncate from './utils/truncate'
|
|
40
|
+
import { allowedProps, TruncateTextState } from './props'
|
|
41
|
+
import type { TruncateTextProps } from './props'
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
---
|
|
45
|
+
category: components
|
|
46
|
+
---
|
|
47
|
+
**/
|
|
48
|
+
@withStyle(generateStyle)
|
|
49
|
+
class TruncateText extends Component<TruncateTextProps, TruncateTextState> {
|
|
50
|
+
static readonly componentId = 'TruncateText'
|
|
51
|
+
|
|
52
|
+
static allowedProps = allowedProps
|
|
53
|
+
|
|
54
|
+
static defaultProps = {
|
|
55
|
+
maxLines: 1,
|
|
56
|
+
ellipsis: '\u2026',
|
|
57
|
+
truncate: 'character',
|
|
58
|
+
position: 'end',
|
|
59
|
+
ignore: [' ', '.', ','],
|
|
60
|
+
debounce: 0
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
ref: Element | null = null
|
|
64
|
+
private _text?: JSX.Element
|
|
65
|
+
private _debounced?: Debounced<typeof this.update>
|
|
66
|
+
private _stage: HTMLSpanElement | null = null
|
|
67
|
+
private _wasTruncated?: boolean
|
|
68
|
+
private _resizeListener?: ResizeObserver
|
|
69
|
+
private _prevWidth: number | null = null
|
|
70
|
+
|
|
71
|
+
constructor(props: TruncateTextProps) {
|
|
72
|
+
super(props)
|
|
73
|
+
this.state = this.initialState
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
get _ref() {
|
|
77
|
+
console.warn(
|
|
78
|
+
'_ref property is deprecated and will be removed in v9, please use ref instead'
|
|
79
|
+
)
|
|
80
|
+
return this.ref
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
get initialState() {
|
|
84
|
+
return {
|
|
85
|
+
isTruncated: false,
|
|
86
|
+
needsSecondRender: true,
|
|
87
|
+
truncatedElement: undefined,
|
|
88
|
+
truncatedText: undefined
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
componentDidMount() {
|
|
93
|
+
const { children, makeStyles } = this.props
|
|
94
|
+
|
|
95
|
+
makeStyles?.()
|
|
96
|
+
|
|
97
|
+
if (children) {
|
|
98
|
+
this.checkChildren()
|
|
99
|
+
const txt = ensureSingleChild(children)
|
|
100
|
+
this._text = txt ? txt : undefined
|
|
101
|
+
|
|
102
|
+
this.truncate()
|
|
103
|
+
|
|
104
|
+
this._debounced = debounce(this.update, this.props.debounce, {
|
|
105
|
+
leading: true,
|
|
106
|
+
trailing: true
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
this._prevWidth = getBoundingClientRect(this.ref)?.width
|
|
110
|
+
this._resizeListener = new ResizeObserver((entries) => {
|
|
111
|
+
// requestAnimationFrame call is needed becuase some truncatetext test cases
|
|
112
|
+
// failed due to ResizeObserver was not able to deliver all observations within a single animation frame
|
|
113
|
+
// see: https://stackoverflow.com/questions/49384120/resizeobserver-loop-limit-exceeded
|
|
114
|
+
requestAnimationFrame(() => {
|
|
115
|
+
for (const entry of entries) {
|
|
116
|
+
const { width } = entry.contentRect
|
|
117
|
+
|
|
118
|
+
if (this._prevWidth !== width) {
|
|
119
|
+
this._prevWidth = width
|
|
120
|
+
this.props.debounce === 0 ? this.update() : this._debounced!()
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
})
|
|
125
|
+
this._resizeListener.observe(this.ref!)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
componentWillUnmount() {
|
|
130
|
+
if (this._resizeListener) {
|
|
131
|
+
this._resizeListener.disconnect()
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (this._debounced) {
|
|
135
|
+
this._debounced.cancel()
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
shallowCompare(obj1: any, obj2: any) {
|
|
140
|
+
const keys1 = Object.keys(obj1)
|
|
141
|
+
const keys2 = Object.keys(obj2)
|
|
142
|
+
if (keys1.length !== keys2.length) {
|
|
143
|
+
return false
|
|
144
|
+
}
|
|
145
|
+
for (const key of keys1) {
|
|
146
|
+
if (obj1[key] !== obj2[key]) {
|
|
147
|
+
return false
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return true
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
componentDidUpdate(prevProps: TruncateTextProps) {
|
|
154
|
+
const { children, onUpdate, makeStyles } = this.props
|
|
155
|
+
makeStyles?.()
|
|
156
|
+
const { isTruncated, needsSecondRender, truncatedText } = this.state
|
|
157
|
+
|
|
158
|
+
if (children) {
|
|
159
|
+
// for some reason in React 19 prevPros and this.props are a different
|
|
160
|
+
// object even if their contents are the same, so we cannot use !==
|
|
161
|
+
if (!this.shallowCompare(prevProps, this.props)) {
|
|
162
|
+
if (prevProps.children !== this.props.children) {
|
|
163
|
+
// reset internal text variable if children change
|
|
164
|
+
this.checkChildren()
|
|
165
|
+
const txt = ensureSingleChild(children)
|
|
166
|
+
this._text = txt ? txt : undefined
|
|
167
|
+
}
|
|
168
|
+
// require the double render whenever props change
|
|
169
|
+
this.setState(this.initialState)
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (!needsSecondRender && (isTruncated || this._wasTruncated)) {
|
|
174
|
+
onUpdate?.(isTruncated, truncatedText)
|
|
175
|
+
this._wasTruncated = isTruncated
|
|
176
|
+
} else {
|
|
177
|
+
this.truncate()
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
checkChildren() {
|
|
183
|
+
error(
|
|
184
|
+
!(() => {
|
|
185
|
+
let isTooDeep = false
|
|
186
|
+
const text = ensureSingleChild(this.props.children)!
|
|
187
|
+
Children.forEach(text.props.children, (child) => {
|
|
188
|
+
if (child.props) {
|
|
189
|
+
Children.forEach(child.props.children, (grandChild) => {
|
|
190
|
+
// currently we don't support node trees deeper than 2 levels
|
|
191
|
+
// truncation will still occur on their text content, but their actual node structure will be lost
|
|
192
|
+
if (grandChild.props) {
|
|
193
|
+
isTooDeep = true
|
|
194
|
+
}
|
|
195
|
+
})
|
|
196
|
+
}
|
|
197
|
+
})
|
|
198
|
+
return isTooDeep
|
|
199
|
+
})(),
|
|
200
|
+
`[TruncateText] Some children are too deep in the node tree and will not render.`
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
update = () => {
|
|
205
|
+
if (this.ref) {
|
|
206
|
+
this.setState(this.initialState)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
truncate() {
|
|
211
|
+
if (!this.state.needsSecondRender) {
|
|
212
|
+
return
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (canUseDOM) {
|
|
216
|
+
const result = truncate(this._stage!, {
|
|
217
|
+
...this.props,
|
|
218
|
+
parent: this.ref ? this.ref : undefined,
|
|
219
|
+
lineHeight: this.props.styles?.lineHeight as number
|
|
220
|
+
})
|
|
221
|
+
if (result) {
|
|
222
|
+
const element = this.renderChildren(
|
|
223
|
+
result.isTruncated,
|
|
224
|
+
result.data,
|
|
225
|
+
result.constraints.width
|
|
226
|
+
)
|
|
227
|
+
this.setState({
|
|
228
|
+
needsSecondRender: false,
|
|
229
|
+
isTruncated: result.isTruncated,
|
|
230
|
+
truncatedElement: element,
|
|
231
|
+
truncatedText: result.text
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
} else {
|
|
235
|
+
const textContent = this.ref?.textContent
|
|
236
|
+
? this.ref?.textContent
|
|
237
|
+
: undefined
|
|
238
|
+
// if dom isn't available, use original children
|
|
239
|
+
this.setState({
|
|
240
|
+
needsSecondRender: false,
|
|
241
|
+
isTruncated: false,
|
|
242
|
+
truncatedElement: this._text,
|
|
243
|
+
truncatedText: textContent
|
|
244
|
+
})
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
renderChildren(truncated: boolean, data: string[][], width: number) {
|
|
249
|
+
if (!truncated) {
|
|
250
|
+
return this._text
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const childElements = []
|
|
254
|
+
// iterate over each node used in the truncated string
|
|
255
|
+
for (let i = 0; i < data.length; i++) {
|
|
256
|
+
const item = data[i]
|
|
257
|
+
const element = this._text!.props.children[i]
|
|
258
|
+
const nodeText = item.join('')
|
|
259
|
+
|
|
260
|
+
if (element && element.props) {
|
|
261
|
+
// if node is an html element and not just a string
|
|
262
|
+
childElements.push(safeCloneElement(element, element.props, nodeText))
|
|
263
|
+
} else {
|
|
264
|
+
childElements.push(nodeText)
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
// this spacer element is set to the max width the full text could
|
|
268
|
+
// potentially be without this, text in `width: auto` elements won't expand
|
|
269
|
+
// to accommodate more text, once truncated
|
|
270
|
+
// Breadcrumb is modifying this element's display to inline to prevent layout issues
|
|
271
|
+
// TODO: find a better way to handle this
|
|
272
|
+
childElements.push(
|
|
273
|
+
<span
|
|
274
|
+
css={this.props.styles?.spacer}
|
|
275
|
+
style={{ width: width || undefined }}
|
|
276
|
+
key="spacer" // TODO this is a temp solution so tests on the CI pass... for v11_rc
|
|
277
|
+
/>
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
const children = Children.map(childElements, (child) => child)
|
|
281
|
+
return this._text!.props
|
|
282
|
+
? safeCloneElement(this._text!, this._text!.props, children)
|
|
283
|
+
: children
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
render() {
|
|
287
|
+
const { truncatedElement } = this.state
|
|
288
|
+
const { children } = this.props
|
|
289
|
+
return (
|
|
290
|
+
<span
|
|
291
|
+
data-cid="TruncateText"
|
|
292
|
+
css={this.props.styles?.truncateText}
|
|
293
|
+
ref={(el) => {
|
|
294
|
+
this.ref = el
|
|
295
|
+
}}
|
|
296
|
+
>
|
|
297
|
+
{children &&
|
|
298
|
+
(truncatedElement ? null : (
|
|
299
|
+
<span
|
|
300
|
+
ref={(el) => {
|
|
301
|
+
this._stage = el
|
|
302
|
+
}}
|
|
303
|
+
>
|
|
304
|
+
{ensureSingleChild(children)}
|
|
305
|
+
</span>
|
|
306
|
+
))}
|
|
307
|
+
{truncatedElement}
|
|
308
|
+
</span>
|
|
309
|
+
)
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export default TruncateText
|
|
314
|
+
export { TruncateText }
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The MIT License (MIT)
|
|
3
|
+
*
|
|
4
|
+
* Copyright (c) 2015 - present Instructure, Inc.
|
|
5
|
+
*
|
|
6
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
* in the Software without restriction, including without limitation the rights
|
|
9
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
* furnished to do so, subject to the following conditions:
|
|
12
|
+
*
|
|
13
|
+
* The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
* copies or substantial portions of the Software.
|
|
15
|
+
*
|
|
16
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
* SOFTWARE.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { ReactNode } from 'react'
|
|
26
|
+
|
|
27
|
+
import type { TruncateTextTheme } from '@instructure/shared-types'
|
|
28
|
+
import type { WithStyleProps, ComponentStyle } from '@instructure/emotion'
|
|
29
|
+
|
|
30
|
+
type CleanDataOptions = {
|
|
31
|
+
/**
|
|
32
|
+
* Add ellipsis after words or after any character. Default is 'character'
|
|
33
|
+
*/
|
|
34
|
+
truncate?: 'character' | 'word'
|
|
35
|
+
/**
|
|
36
|
+
* A string to use as the ellipsis
|
|
37
|
+
*/
|
|
38
|
+
ellipsis?: string
|
|
39
|
+
/**
|
|
40
|
+
* Characters to ignore at truncated end of string. Default is ' ', '.', ','
|
|
41
|
+
*/
|
|
42
|
+
ignore?: string[]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
type TruncateTextCommonProps = {
|
|
46
|
+
/**
|
|
47
|
+
* Number of lines to allow before truncating. `auto` will fit to parent.
|
|
48
|
+
* Default is 1.
|
|
49
|
+
*/
|
|
50
|
+
maxLines?: 'auto' | number
|
|
51
|
+
/**
|
|
52
|
+
* Where to place the ellipsis within the string. Default is 'end'
|
|
53
|
+
*/
|
|
54
|
+
position?: 'end' | 'middle'
|
|
55
|
+
/**
|
|
56
|
+
* Force truncation of invisible elements (hack; will be removed in favor
|
|
57
|
+
* of a better fix)
|
|
58
|
+
*/
|
|
59
|
+
shouldTruncateWhenInvisible?: boolean
|
|
60
|
+
} & CleanDataOptions
|
|
61
|
+
|
|
62
|
+
type TruncateTextOwnProps = {
|
|
63
|
+
/**
|
|
64
|
+
* The content to be truncated.
|
|
65
|
+
*/
|
|
66
|
+
children: React.ReactNode
|
|
67
|
+
/**
|
|
68
|
+
* Debounce delay in milliseconds
|
|
69
|
+
*/
|
|
70
|
+
debounce?: number
|
|
71
|
+
/**
|
|
72
|
+
* Callback when truncated text has changed
|
|
73
|
+
*/
|
|
74
|
+
onUpdate?: (isTruncated: boolean, truncatedText?: string) => void
|
|
75
|
+
} & TruncateTextCommonProps
|
|
76
|
+
|
|
77
|
+
type PropKeys = keyof TruncateTextOwnProps
|
|
78
|
+
|
|
79
|
+
type AllowedPropKeys = Readonly<Array<PropKeys>>
|
|
80
|
+
|
|
81
|
+
type TruncateTextProps = TruncateTextOwnProps &
|
|
82
|
+
WithStyleProps<TruncateTextTheme, TruncateTextStyle>
|
|
83
|
+
|
|
84
|
+
type TruncateTextStyle = ComponentStyle<
|
|
85
|
+
'truncateText' | 'auto' | 'spacer' | 'lineHeight'
|
|
86
|
+
>
|
|
87
|
+
|
|
88
|
+
type TruncateTextState = {
|
|
89
|
+
isTruncated: boolean
|
|
90
|
+
needsSecondRender: boolean
|
|
91
|
+
truncatedElement?: ReactNode
|
|
92
|
+
truncatedText?: string
|
|
93
|
+
}
|
|
94
|
+
const allowedProps: AllowedPropKeys = [
|
|
95
|
+
'children',
|
|
96
|
+
'maxLines',
|
|
97
|
+
'position',
|
|
98
|
+
'truncate',
|
|
99
|
+
'ellipsis',
|
|
100
|
+
'ignore',
|
|
101
|
+
'debounce',
|
|
102
|
+
'onUpdate',
|
|
103
|
+
'shouldTruncateWhenInvisible'
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
export type {
|
|
107
|
+
CleanDataOptions,
|
|
108
|
+
TruncateTextCommonProps,
|
|
109
|
+
TruncateTextProps,
|
|
110
|
+
TruncateTextState,
|
|
111
|
+
TruncateTextStyle
|
|
112
|
+
}
|
|
113
|
+
export { allowedProps }
|