@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.
Files changed (100) hide show
  1. package/CHANGELOG.md +29 -277
  2. package/es/TruncateText/{index.js → v1/index.js} +1 -1
  3. package/es/TruncateText/v2/index.js +264 -0
  4. package/es/TruncateText/v2/props.js +26 -0
  5. package/es/TruncateText/v2/styles.js +58 -0
  6. package/es/TruncateText/v2/utils/cleanData.js +158 -0
  7. package/es/TruncateText/v2/utils/cleanString.js +53 -0
  8. package/es/TruncateText/v2/utils/measureText.js +74 -0
  9. package/es/TruncateText/v2/utils/truncate.js +341 -0
  10. package/es/{index.js → exports/a.js} +1 -1
  11. package/{src/index.ts → es/exports/b.js} +1 -2
  12. package/lib/TruncateText/{index.js → v1/index.js} +1 -1
  13. package/lib/TruncateText/v2/index.js +271 -0
  14. package/lib/TruncateText/v2/props.js +31 -0
  15. package/lib/TruncateText/v2/styles.js +64 -0
  16. package/lib/TruncateText/v2/utils/cleanData.js +164 -0
  17. package/lib/TruncateText/v2/utils/cleanString.js +59 -0
  18. package/lib/TruncateText/v2/utils/measureText.js +79 -0
  19. package/lib/TruncateText/v2/utils/truncate.js +350 -0
  20. package/lib/{index.js → exports/a.js} +2 -2
  21. package/lib/exports/b.js +12 -0
  22. package/package.json +42 -20
  23. package/src/TruncateText/{index.tsx → v1/index.tsx} +1 -1
  24. package/src/TruncateText/v2/README.md +230 -0
  25. package/src/TruncateText/v2/index.tsx +314 -0
  26. package/src/TruncateText/v2/props.ts +113 -0
  27. package/src/TruncateText/v2/styles.ts +60 -0
  28. package/src/TruncateText/v2/utils/cleanData.ts +178 -0
  29. package/src/TruncateText/v2/utils/cleanString.ts +61 -0
  30. package/src/TruncateText/v2/utils/measureText.ts +86 -0
  31. package/src/TruncateText/v2/utils/truncate.ts +451 -0
  32. package/src/exports/a.ts +25 -0
  33. package/src/exports/b.ts +25 -0
  34. package/tsconfig.build.tsbuildinfo +1 -1
  35. package/types/TruncateText/v1/index.d.ts.map +1 -0
  36. package/types/TruncateText/v1/props.d.ts.map +1 -0
  37. package/types/TruncateText/v1/styles.d.ts.map +1 -0
  38. package/types/TruncateText/v1/theme.d.ts.map +1 -0
  39. package/types/TruncateText/v1/utils/cleanData.d.ts.map +1 -0
  40. package/types/TruncateText/v1/utils/cleanString.d.ts.map +1 -0
  41. package/types/TruncateText/v1/utils/measureText.d.ts.map +1 -0
  42. package/types/TruncateText/v1/utils/truncate.d.ts.map +1 -0
  43. package/types/TruncateText/v2/index.d.ts +47 -0
  44. package/types/TruncateText/v2/index.d.ts.map +1 -0
  45. package/types/TruncateText/v2/props.d.ts +61 -0
  46. package/types/TruncateText/v2/props.d.ts.map +1 -0
  47. package/types/TruncateText/v2/styles.d.ts +15 -0
  48. package/types/TruncateText/v2/styles.d.ts.map +1 -0
  49. package/types/TruncateText/v2/utils/cleanData.d.ts +19 -0
  50. package/types/TruncateText/v2/utils/cleanData.d.ts.map +1 -0
  51. package/types/TruncateText/v2/utils/cleanString.d.ts +16 -0
  52. package/types/TruncateText/v2/utils/cleanString.d.ts.map +1 -0
  53. package/types/TruncateText/v2/utils/measureText.d.ts +13 -0
  54. package/types/TruncateText/v2/utils/measureText.d.ts.map +1 -0
  55. package/types/TruncateText/v2/utils/truncate.d.ts +35 -0
  56. package/types/TruncateText/v2/utils/truncate.d.ts.map +1 -0
  57. package/types/exports/a.d.ts +3 -0
  58. package/types/exports/a.d.ts.map +1 -0
  59. package/types/exports/b.d.ts +3 -0
  60. package/types/exports/b.d.ts.map +1 -0
  61. package/types/TruncateText/index.d.ts.map +0 -1
  62. package/types/TruncateText/props.d.ts.map +0 -1
  63. package/types/TruncateText/styles.d.ts.map +0 -1
  64. package/types/TruncateText/theme.d.ts.map +0 -1
  65. package/types/TruncateText/utils/cleanData.d.ts.map +0 -1
  66. package/types/TruncateText/utils/cleanString.d.ts.map +0 -1
  67. package/types/TruncateText/utils/measureText.d.ts.map +0 -1
  68. package/types/TruncateText/utils/truncate.d.ts.map +0 -1
  69. package/types/index.d.ts +0 -3
  70. package/types/index.d.ts.map +0 -1
  71. /package/es/TruncateText/{props.js → v1/props.js} +0 -0
  72. /package/es/TruncateText/{styles.js → v1/styles.js} +0 -0
  73. /package/es/TruncateText/{theme.js → v1/theme.js} +0 -0
  74. /package/es/TruncateText/{utils → v1/utils}/cleanData.js +0 -0
  75. /package/es/TruncateText/{utils → v1/utils}/cleanString.js +0 -0
  76. /package/es/TruncateText/{utils → v1/utils}/measureText.js +0 -0
  77. /package/es/TruncateText/{utils → v1/utils}/truncate.js +0 -0
  78. /package/lib/TruncateText/{props.js → v1/props.js} +0 -0
  79. /package/lib/TruncateText/{styles.js → v1/styles.js} +0 -0
  80. /package/lib/TruncateText/{theme.js → v1/theme.js} +0 -0
  81. /package/lib/TruncateText/{utils → v1/utils}/cleanData.js +0 -0
  82. /package/lib/TruncateText/{utils → v1/utils}/cleanString.js +0 -0
  83. /package/lib/TruncateText/{utils → v1/utils}/measureText.js +0 -0
  84. /package/lib/TruncateText/{utils → v1/utils}/truncate.js +0 -0
  85. /package/src/TruncateText/{README.md → v1/README.md} +0 -0
  86. /package/src/TruncateText/{props.ts → v1/props.ts} +0 -0
  87. /package/src/TruncateText/{styles.ts → v1/styles.ts} +0 -0
  88. /package/src/TruncateText/{theme.ts → v1/theme.ts} +0 -0
  89. /package/src/TruncateText/{utils → v1/utils}/cleanData.ts +0 -0
  90. /package/src/TruncateText/{utils → v1/utils}/cleanString.ts +0 -0
  91. /package/src/TruncateText/{utils → v1/utils}/measureText.ts +0 -0
  92. /package/src/TruncateText/{utils → v1/utils}/truncate.ts +0 -0
  93. /package/types/TruncateText/{index.d.ts → v1/index.d.ts} +0 -0
  94. /package/types/TruncateText/{props.d.ts → v1/props.d.ts} +0 -0
  95. /package/types/TruncateText/{styles.d.ts → v1/styles.d.ts} +0 -0
  96. /package/types/TruncateText/{theme.d.ts → v1/theme.d.ts} +0 -0
  97. /package/types/TruncateText/{utils → v1/utils}/cleanData.d.ts +0 -0
  98. /package/types/TruncateText/{utils → v1/utils}/cleanString.d.ts +0 -0
  99. /package/types/TruncateText/{utils → v1/utils}/measureText.d.ts +0 -0
  100. /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 }