@instructure/ui-truncate-list 8.27.1-snapshot-13

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.
@@ -0,0 +1,37 @@
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
+ import { locator } from '@instructure/ui-test-locator'
25
+ import { find, findAll } from '@instructure/ui-test-queries'
26
+
27
+ import { TruncateList } from './index'
28
+
29
+ // @ts-expect-error ts-migrate(2339) FIXME: Property 'selector' does not exist on type 'typeof... Remove this comment to see the full error message
30
+ export const TruncateListLocator = locator(TruncateList.selector, {
31
+ findAllListItems: (...args: any[]) => {
32
+ return findAll('li', ...args)
33
+ },
34
+ findMenuTriggerItem: (...args: any[]) => {
35
+ return find('li[class*=-truncateList__menuTrigger]', ...args)
36
+ }
37
+ })
@@ -0,0 +1,311 @@
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
+ /** @jsx jsx */
26
+ import { Children, Component } from 'react'
27
+
28
+ import { debounce } from '@instructure/debounce'
29
+ import type { Debounced } from '@instructure/debounce'
30
+ import { px } from '@instructure/ui-utils'
31
+ import { testable } from '@instructure/ui-testable'
32
+ import { omitProps } from '@instructure/ui-react-utils'
33
+ import { getBoundingClientRect } from '@instructure/ui-dom-utils'
34
+
35
+ import { withStyle, jsx } from '@instructure/emotion'
36
+
37
+ import generateStyle from './styles'
38
+
39
+ import { propTypes, allowedProps } from './props'
40
+ import type { TruncateListProps, TruncateListState } from './props'
41
+
42
+ /**
43
+ ---
44
+ category: components/utilities
45
+ ---
46
+ @tsProps
47
+ **/
48
+ @withStyle(generateStyle, null)
49
+ @testable()
50
+ class TruncateList extends Component<TruncateListProps, TruncateListState> {
51
+ static readonly componentId = 'TruncateList'
52
+
53
+ static propTypes = propTypes
54
+ static allowedProps = allowedProps
55
+ static defaultProps = {
56
+ itemSpacing: '0',
57
+ debounce: 300
58
+ }
59
+
60
+ ref: HTMLUListElement | null = null
61
+
62
+ private _menuTriggerRef: HTMLLIElement | null = null
63
+
64
+ private _debouncedHandleResize?: Debounced
65
+ private _resizeListener?: ResizeObserver
66
+
67
+ handleRef = (el: HTMLUListElement | null) => {
68
+ const { elementRef } = this.props
69
+
70
+ this.ref = el
71
+
72
+ if (typeof elementRef === 'function') {
73
+ elementRef(el)
74
+ }
75
+ }
76
+
77
+ constructor(props: TruncateListProps) {
78
+ super(props)
79
+
80
+ this.state = {
81
+ isMeasuring: false,
82
+ menuTriggerWidth: undefined
83
+ }
84
+ }
85
+
86
+ componentDidMount() {
87
+ this.props.makeStyles?.()
88
+
89
+ const { width: origWidth } = getBoundingClientRect(this.ref)
90
+
91
+ this._debouncedHandleResize = debounce(
92
+ this.handleResize,
93
+ this.props.debounce,
94
+ {
95
+ leading: true,
96
+ trailing: true
97
+ }
98
+ )
99
+
100
+ this._resizeListener = new ResizeObserver((entries) => {
101
+ for (const entry of entries) {
102
+ const { width } = entry.contentRect
103
+
104
+ if (origWidth !== width) {
105
+ this._debouncedHandleResize?.()
106
+ }
107
+ }
108
+ })
109
+
110
+ // On first render we only render the trigger to measure it
111
+ const menuTriggerWidth = this.props.fixMenuTriggerWidth
112
+ ? px(this.props.fixMenuTriggerWidth)
113
+ : this.calcMenuTriggerWidth()
114
+
115
+ this.setState({ menuTriggerWidth }, () => {
116
+ this._resizeListener?.observe(this.ref!)
117
+
118
+ this._debouncedHandleResize?.()
119
+ })
120
+ }
121
+
122
+ componentDidUpdate(
123
+ prevProps: TruncateListProps,
124
+ prevState: TruncateListState
125
+ ) {
126
+ this.props.makeStyles?.()
127
+
128
+ if (
129
+ prevProps.fixMenuTriggerWidth !== this.props.fixMenuTriggerWidth ||
130
+ prevProps.itemSpacing !== this.props.itemSpacing
131
+ ) {
132
+ this._debouncedHandleResize?.()
133
+ }
134
+
135
+ if (
136
+ this.ref &&
137
+ prevState.isMeasuring &&
138
+ prevState.isMeasuring !== this.state.isMeasuring
139
+ ) {
140
+ const menuTriggerWidth = this.calcMenuTriggerWidth()
141
+
142
+ if (
143
+ menuTriggerWidth &&
144
+ this.state.menuTriggerWidth !== menuTriggerWidth
145
+ ) {
146
+ this.setState({ menuTriggerWidth }, () => {
147
+ this._debouncedHandleResize?.()
148
+ })
149
+ }
150
+ }
151
+ }
152
+
153
+ componentWillUnmount() {
154
+ if (this._resizeListener) {
155
+ this._resizeListener.disconnect()
156
+ }
157
+
158
+ if (this._debouncedHandleResize) {
159
+ this._debouncedHandleResize.cancel()
160
+ }
161
+ }
162
+
163
+ get childrenArray() {
164
+ return Children.toArray(this.props.children)
165
+ }
166
+
167
+ get visibleChildren() {
168
+ const { visibleItemsCount } = this.props
169
+ const { isMeasuring, menuTriggerWidth } = this.state
170
+
171
+ // for the first render we need to measure the trigger width
172
+ if (typeof menuTriggerWidth === 'undefined') {
173
+ return []
174
+ }
175
+
176
+ const visibleChildren =
177
+ typeof visibleItemsCount === 'undefined'
178
+ ? this.childrenArray
179
+ : this.childrenArray.splice(0, visibleItemsCount)
180
+
181
+ return isMeasuring ? this.childrenArray : visibleChildren
182
+ }
183
+
184
+ get hiddenChildren() {
185
+ const { isMeasuring } = this.state
186
+
187
+ return isMeasuring
188
+ ? []
189
+ : this.childrenArray.splice(
190
+ this.visibleChildren.length,
191
+ this.childrenArray.length
192
+ )
193
+ }
194
+
195
+ calcMenuTriggerWidth() {
196
+ const { fixMenuTriggerWidth } = this.props
197
+
198
+ if (!this._menuTriggerRef) {
199
+ return 0
200
+ }
201
+
202
+ if (fixMenuTriggerWidth) {
203
+ return px(fixMenuTriggerWidth)
204
+ }
205
+
206
+ const { children } = this._menuTriggerRef
207
+ let width = 0
208
+
209
+ Array.from(children).forEach((child) => {
210
+ width += getBoundingClientRect(child).width
211
+ })
212
+
213
+ return width
214
+ }
215
+
216
+ measureItems = () => {
217
+ const { fixMenuTriggerWidth, itemSpacing } = this.props
218
+
219
+ const itemSpacingPx = px(itemSpacing!)
220
+ const menuTriggerWidthPx = px(
221
+ fixMenuTriggerWidth || this.state.menuTriggerWidth!
222
+ )
223
+
224
+ let visibleItemsCount = 0
225
+
226
+ if (this.ref) {
227
+ const { width: navWidth } = getBoundingClientRect(this.ref)
228
+
229
+ const itemWidths = Array.from(this.ref.getElementsByTagName('li')).map(
230
+ (item) => {
231
+ const { width } = getBoundingClientRect(item)
232
+ return width
233
+ }
234
+ )
235
+
236
+ let currentWidth = 0
237
+
238
+ for (let i = 0; i < itemWidths.length; i++) {
239
+ currentWidth += itemWidths[i]
240
+
241
+ // for the last item we don't need to calculate with the menu trigger
242
+ const maxWidth =
243
+ i === itemWidths.length - 1
244
+ ? navWidth
245
+ : navWidth - (menuTriggerWidthPx + itemSpacingPx)
246
+
247
+ if (currentWidth <= maxWidth) {
248
+ visibleItemsCount++
249
+ } else {
250
+ break
251
+ }
252
+ }
253
+ }
254
+
255
+ return { visibleItemsCount }
256
+ }
257
+
258
+ handleResize = () => {
259
+ this.setState({ isMeasuring: true }, () => {
260
+ const { visibleItemsCount } = this.measureItems()
261
+
262
+ if (typeof this.props.onUpdate === 'function') {
263
+ this.props.onUpdate({ visibleItemsCount })
264
+ }
265
+ this.setState({ isMeasuring: false })
266
+ })
267
+ }
268
+
269
+ render() {
270
+ const { styles, className, style, renderHiddenItemMenu } = this.props
271
+ const { visibleChildren, hiddenChildren } = this
272
+
273
+ return (
274
+ <ul
275
+ ref={this.handleRef}
276
+ {...omitProps(this.props, allowedProps)}
277
+ // we have to pass style and className
278
+ // (e.g. if emotion style is provided, it will be passed as a className)
279
+ className={className}
280
+ style={style}
281
+ css={styles?.truncateList}
282
+ >
283
+ {visibleChildren.map((child, index) => {
284
+ return (
285
+ <li key={index} css={styles?.listItem}>
286
+ {child}
287
+ </li>
288
+ )
289
+ })}
290
+
291
+ {/* On first render we only render the trigger to measure it */}
292
+ {typeof renderHiddenItemMenu === 'function' &&
293
+ hiddenChildren &&
294
+ hiddenChildren.length > 0 && (
295
+ <li
296
+ key="menuTrigger"
297
+ css={[styles?.listItem, styles?.menuTrigger]}
298
+ ref={(e) => {
299
+ this._menuTriggerRef = e
300
+ }}
301
+ >
302
+ {renderHiddenItemMenu(hiddenChildren)}
303
+ </li>
304
+ )}
305
+ </ul>
306
+ )
307
+ }
308
+ }
309
+
310
+ export { TruncateList }
311
+ export default TruncateList
@@ -0,0 +1,124 @@
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 React from 'react'
26
+ import PropTypes from 'prop-types'
27
+
28
+ import type { WithStyleProps, ComponentStyle } from '@instructure/emotion'
29
+ import type {
30
+ OtherHTMLAttributes,
31
+ PropValidators
32
+ } from '@instructure/shared-types'
33
+
34
+ type TruncateListOwnProps = {
35
+ /**
36
+ * List of items in the truncated list
37
+ */
38
+ children?: React.ReactNode
39
+
40
+ /**
41
+ * Sets the number of navigation items that are visible.
42
+ * If not set, the list is not truncated.
43
+ */
44
+ visibleItemsCount?: number
45
+
46
+ /**
47
+ * When there are list items hidden, an optional element
48
+ * (dropdown menu, link, etc.) can be provided to display them
49
+ * (renders at the end of the list).
50
+ */
51
+ renderHiddenItemMenu?: (
52
+ hiddenChildren: Exclude<React.ReactNode, boolean | null | undefined>[]
53
+ ) => React.ReactElement
54
+
55
+ /**
56
+ * Called whenever the navigation items are updated or the size of
57
+ * the navigation changes. Passes in the `visibleItemsCount` as
58
+ * a parameter.
59
+ */
60
+ onUpdate?: ({ visibleItemsCount }: { visibleItemsCount: number }) => void
61
+
62
+ /**
63
+ * The spacing between list items (in 'rem', 'em' or 'px')
64
+ */
65
+ itemSpacing?: string
66
+
67
+ /**
68
+ * Fix width of the Menu trigger (in 'rem', 'em' or 'px')
69
+ */
70
+ fixMenuTriggerWidth?: string
71
+
72
+ /**
73
+ * The rate (in ms) the component responds to container resizing or
74
+ * an update to one of its child items
75
+ */
76
+ debounce?: number
77
+
78
+ /**
79
+ * Provides a reference to the underlying ul element
80
+ */
81
+ elementRef?: (element: HTMLUListElement | null) => void
82
+ }
83
+
84
+ type PropKeys = keyof TruncateListOwnProps
85
+
86
+ type AllowedPropKeys = Readonly<Array<PropKeys>>
87
+
88
+ type TruncateListProps = TruncateListOwnProps &
89
+ WithStyleProps<null, TruncateListStyle> &
90
+ OtherHTMLAttributes<TruncateListOwnProps>
91
+
92
+ type TruncateListStyle = ComponentStyle<
93
+ 'truncateList' | 'listItem' | 'menuTrigger'
94
+ >
95
+
96
+ type TruncateListState = {
97
+ isMeasuring: boolean
98
+ menuTriggerWidth?: number
99
+ }
100
+
101
+ const propTypes: PropValidators<PropKeys> = {
102
+ children: PropTypes.node,
103
+ visibleItemsCount: PropTypes.number,
104
+ onUpdate: PropTypes.func,
105
+ renderHiddenItemMenu: PropTypes.func,
106
+ itemSpacing: PropTypes.string,
107
+ fixMenuTriggerWidth: PropTypes.string,
108
+ debounce: PropTypes.number,
109
+ elementRef: PropTypes.func
110
+ }
111
+
112
+ const allowedProps: AllowedPropKeys = [
113
+ 'children',
114
+ 'renderHiddenItemMenu',
115
+ 'visibleItemsCount',
116
+ 'itemSpacing',
117
+ 'fixMenuTriggerWidth',
118
+ 'debounce',
119
+ 'onUpdate',
120
+ 'elementRef'
121
+ ]
122
+
123
+ export type { TruncateListProps, TruncateListStyle, TruncateListState }
124
+ export { propTypes, allowedProps }
@@ -0,0 +1,81 @@
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 type { TruncateListProps, TruncateListStyle } from './props'
26
+
27
+ /**
28
+ * ---
29
+ * private: true
30
+ * ---
31
+ * Generates the style object from the theme and provided additional information
32
+ * @param {Object} componentTheme The theme variable object.
33
+ * @param {Object} props the props of the component, the style is applied to
34
+ * @param {Object} state the state of the component, the style is applied to
35
+ * @return {Object} The final style object, which will be used in the component
36
+ */
37
+ const generateStyle = (
38
+ _componentTheme: null,
39
+ props: TruncateListProps
40
+ ): TruncateListStyle => {
41
+ const { itemSpacing, fixMenuTriggerWidth } = props
42
+
43
+ const listItemStyle = {
44
+ minWidth: '0.0625rem',
45
+ flexShrink: 0,
46
+ flexGrow: 0
47
+ }
48
+
49
+ return {
50
+ truncateList: {
51
+ label: 'truncateList',
52
+ boxSizing: 'border-box',
53
+ listStyleType: 'none',
54
+ margin: '0',
55
+ padding: '0',
56
+ display: 'flex',
57
+ justifyContent: 'flex-start',
58
+ alignItems: 'center'
59
+ },
60
+ listItem: {
61
+ label: 'truncateList__listItem',
62
+ ...listItemStyle,
63
+
64
+ '& + &': {
65
+ paddingInlineStart: itemSpacing
66
+ }
67
+ },
68
+ menuTrigger: {
69
+ label: 'truncateList__menuTrigger',
70
+ maxWidth: '100%',
71
+ ...(fixMenuTriggerWidth && { width: fixMenuTriggerWidth }),
72
+ paddingInlineStart: itemSpacing,
73
+
74
+ '&:first-of-type': {
75
+ paddingInlineStart: 0
76
+ }
77
+ }
78
+ }
79
+ }
80
+
81
+ export default generateStyle
package/src/index.ts ADDED
@@ -0,0 +1,26 @@
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
+ export { TruncateList } from './TruncateList'
26
+ export type { TruncateListProps } from './TruncateList/props'
@@ -0,0 +1,25 @@
1
+ {
2
+ "extends": "../../tsconfig.build.json",
3
+ "compilerOptions": {
4
+ "rootDir": "./src",
5
+ "outDir": "./types",
6
+ "composite": true
7
+ },
8
+ "include": ["src"],
9
+ "references": [
10
+ { "path": "../console/tsconfig.build.json" },
11
+ { "path": "../debounce/tsconfig.build.json" },
12
+ { "path": "../emotion/tsconfig.build.json" },
13
+ { "path": "../shared-types/tsconfig.build.json" },
14
+ { "path": "../ui-dom-utils/tsconfig.build.json" },
15
+ { "path": "../ui-react-utils/tsconfig.build.json" },
16
+ { "path": "../ui-testable/tsconfig.build.json" },
17
+ { "path": "../ui-utils/tsconfig.build.json" },
18
+ { "path": "../ui-babel-preset/tsconfig.build.json" },
19
+ { "path": "../ui-color-utils/tsconfig.build.json" },
20
+ { "path": "../ui-test-locator/tsconfig.build.json" },
21
+ { "path": "../ui-test-queries/tsconfig.build.json" },
22
+ { "path": "../ui-test-utils/tsconfig.build.json" },
23
+ { "path": "../ui-themes/tsconfig.build.json" }
24
+ ]
25
+ }