Skip to content

Commit 9fa8e5e

Browse files
authored
Merge pull request #1963 from oasisprotocol/csillag/long-term-fix-adaptive-trimmer
Make adaptive trimmer more resilient
2 parents fdd262e + 5597a47 commit 9fa8e5e

File tree

16 files changed

+641
-169
lines changed

16 files changed

+641
-169
lines changed

.changelog/1963.trivial.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Make adaptive trimmer more resilient

src/app/components/Account/AccountLink.tsx

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,22 +22,11 @@ const WithTypographyAndLink: FC<{
2222
mobile?: boolean
2323
children: ReactNode
2424
labelOnly?: boolean
25-
}> = ({ scope, address, children, mobile, labelOnly }) => {
25+
}> = ({ scope, address, children, labelOnly }) => {
2626
const to = RouteUtils.getAccountRoute(scope, address)
2727
return (
2828
<WithHighlighting address={address}>
29-
<Typography
30-
variant="mono"
31-
component="span"
32-
sx={{
33-
...(mobile
34-
? {
35-
maxWidth: '100%',
36-
overflow: 'hidden',
37-
}
38-
: {}),
39-
}}
40-
>
29+
<Typography variant="mono" component="span">
4130
{labelOnly ? (
4231
children
4332
) : (
@@ -186,13 +175,21 @@ export const AccountLink: FC<Props> = ({
186175
<Box component="span" sx={{ display: 'inline-flex', alignItems: 'center', gap: 3 }}>
187176
<AccountMetadataSourceIndicator source={accountMetadata.source} />
188177
<AdaptiveHighlightedText
178+
idPrefix="account-name"
189179
text={accountName}
190180
pattern={highlightedPartOfName}
191181
extraTooltip={tooltipTitle}
182+
minLength={5}
192183
/>
193184
</Box>
194185
)}
195-
<AdaptiveTrimmer text={address} strategy="middle" tooltipOverride={tooltipTitle} />
186+
<AdaptiveTrimmer
187+
idPrefix="account-address"
188+
text={address}
189+
strategy="middle"
190+
tooltipOverride={tooltipTitle}
191+
minLength={13}
192+
/>
196193
</>
197194
</WithTypographyAndLink>
198195
)

src/app/components/AdaptiveTrimmer/AdaptiveDynamicTrimmer.tsx

Lines changed: 56 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,34 @@
1-
import { FC, ReactNode, useCallback, useEffect, useRef, useState } from 'react'
1+
import { FC, ReactNode, useMemo } from 'react'
22
import Box from '@mui/material/Box'
33
import InfoIcon from '@mui/icons-material/Info'
44
import { MaybeWithTooltip } from '../Tooltip/MaybeWithTooltip'
5+
import { getAdaptiveId, ShorteningResult, useAdaptiveSizing } from './hooks'
56

67
type AdaptiveDynamicTrimmerProps = {
8+
/**
9+
* The ID (prefix) used for debugging
10+
*/
11+
idPrefix: string
12+
13+
/**
14+
* A function to return the full content
15+
*/
716
getFullContent: () => {
817
content: ReactNode
918
length: number
1019
}
11-
getShortenedContent: (wantedLength: number) => ReactNode
20+
21+
/**
22+
* A function to return shortened content
23+
*/
24+
getShortenedContent: (wantedLength: number) => ShorteningResult
25+
26+
/**
27+
* The minimum length we ever want to shorten to.
28+
*
29+
* Default is 2
30+
*/
31+
minLength?: number
1232

1333
/**
1434
* Normally, the tooltip will be the full content. Do you want to add something?
@@ -19,6 +39,11 @@ type AdaptiveDynamicTrimmerProps = {
1939
* Normally, the tooltip will be the full content. Do you want to replace it with something else?
2040
*/
2141
tooltipOverride?: ReactNode
42+
43+
/**
44+
* Do we want to see debug output about the adaptive trimming process?
45+
*/
46+
debugMode?: boolean
2247
}
2348

2449
/**
@@ -31,132 +56,44 @@ type AdaptiveDynamicTrimmerProps = {
3156
* expects a function to provide a shortened version of the components.
3257
*/
3358
export const AdaptiveDynamicTrimmer: FC<AdaptiveDynamicTrimmerProps> = ({
59+
idPrefix,
3460
getFullContent,
3561
getShortenedContent,
3662
extraTooltip,
3763
tooltipOverride,
64+
debugMode = false,
65+
minLength = 2,
3866
}) => {
39-
// Initial setup
40-
const textRef = useRef<HTMLDivElement | null>(null)
41-
const { content: fullContent, length: fullLength } = getFullContent()
42-
43-
// Data about the currently rendered version
44-
const [currentContent, setCurrentContent] = useState<ReactNode>()
45-
const [currentLength, setCurrentLength] = useState(0)
46-
47-
// Known good - this fits
48-
const [largestKnownGood, setLargestKnownGood] = useState(0)
49-
50-
// Known bad - this doesn't fit
51-
const [smallestKnownBad, setSmallestKnownBad] = useState(fullLength + 1)
52-
53-
// Are we exploring our possibilities now?
54-
const [inDiscovery, setInDiscovery] = useState(false)
55-
56-
const attemptContent = useCallback((content: ReactNode, length: number) => {
57-
setCurrentContent(content)
58-
setCurrentLength(length)
59-
}, [])
60-
61-
const attemptShortenedContent = useCallback(
62-
(length: number) => {
63-
const content = getShortenedContent(length)
64-
65-
attemptContent(content, length)
66-
},
67-
[attemptContent, getShortenedContent],
67+
const id = useMemo(() => getAdaptiveId(idPrefix), [idPrefix])
68+
69+
const { currentContent, fullContent, textRef, isTruncated, isFinal } = useAdaptiveSizing(
70+
id,
71+
getFullContent,
72+
getShortenedContent,
73+
debugMode,
74+
minLength,
6875
)
6976

70-
const initDiscovery = useCallback(() => {
71-
setLargestKnownGood(0)
72-
setSmallestKnownBad(fullLength + 1)
73-
attemptContent(fullContent, fullLength)
74-
setInDiscovery(true)
75-
}, [fullContent, fullLength, attemptContent])
76-
77-
useEffect(() => {
78-
initDiscovery()
79-
const handleResize = () => {
80-
initDiscovery()
81-
}
82-
83-
window.addEventListener('resize', handleResize)
84-
return () => window.removeEventListener('resize', handleResize)
85-
}, [initDiscovery])
86-
87-
useEffect(() => {
88-
if (inDiscovery) {
89-
if (!textRef.current) {
90-
return
91-
}
92-
const isOverflow = textRef.current.scrollWidth > textRef.current.clientWidth
93-
94-
if (isOverflow) {
95-
// This is too much
96-
97-
// Update known bad length
98-
const newSmallestKnownBad = Math.min(currentLength, smallestKnownBad)
99-
setSmallestKnownBad(newSmallestKnownBad)
100-
101-
// We should try something smaller
102-
attemptShortenedContent(Math.floor((largestKnownGood + newSmallestKnownBad) / 2))
103-
} else {
104-
// This is OK
105-
106-
// Update known good length
107-
const newLargestKnownGood = Math.max(currentLength, largestKnownGood)
108-
setLargestKnownGood(currentLength)
109-
110-
if (currentLength === fullLength) {
111-
// The whole thing fits, so we are good.
112-
setInDiscovery(false)
113-
} else {
114-
if (currentLength + 1 === smallestKnownBad) {
115-
// This the best we can do, for now
116-
setInDiscovery(false)
117-
} else {
118-
// So far, so good, but we should try something longer
119-
attemptShortenedContent(Math.floor((newLargestKnownGood + smallestKnownBad) / 2))
120-
}
121-
}
122-
}
123-
}
124-
}, [
125-
attemptShortenedContent,
126-
currentLength,
127-
fullContent,
128-
fullLength,
129-
inDiscovery,
130-
initDiscovery,
131-
largestKnownGood,
132-
smallestKnownBad,
133-
])
134-
135-
const title =
136-
currentLength !== fullLength ? (
137-
<Box>
138-
<Box>{tooltipOverride ?? fullContent}</Box>
139-
{extraTooltip && (
140-
<Box sx={{ display: 'inline-flex', alignItems: 'center', gap: 2 }}>
141-
<InfoIcon />
142-
{extraTooltip}
143-
</Box>
144-
)}
145-
</Box>
146-
) : (
147-
extraTooltip
148-
)
77+
const title = isTruncated ? (
78+
<Box>
79+
<Box>{tooltipOverride ?? fullContent}</Box>
80+
{extraTooltip && (
81+
<Box sx={{ display: 'inline-flex', alignItems: 'center', gap: 2 }}>
82+
<InfoIcon />
83+
{extraTooltip}
84+
</Box>
85+
)}
86+
</Box>
87+
) : (
88+
extraTooltip
89+
)
14990

15091
return (
151-
<Box
152-
ref={textRef}
153-
sx={{
154-
overflow: 'hidden',
155-
maxWidth: '100%',
156-
textWrap: 'nowrap',
157-
}}
158-
>
159-
<MaybeWithTooltip title={title} spanSx={{ whiteSpace: 'nowrap' }}>
92+
<Box component={'span'} ref={textRef} sx={{ maxWidth: '100%', overflowX: 'hidden' }}>
93+
<MaybeWithTooltip
94+
title={title}
95+
spanSx={{ textWrap: 'nowrap', whiteSpace: 'nowrap', opacity: isFinal ? 1 : 0 }}
96+
>
16097
{currentContent}
16198
</MaybeWithTooltip>
16299
</Box>

src/app/components/AdaptiveTrimmer/AdaptiveTrimmer.tsx

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,21 @@ import { AdaptiveDynamicTrimmer } from './AdaptiveDynamicTrimmer'
33
import { trimLongString } from 'app/utils/trimLongString'
44

55
type AdaptiveTrimmerProps = {
6+
/**
7+
* ID prefix to use (for debugging)
8+
*/
9+
idPrefix?: string
10+
611
text: string | undefined
712
strategy: 'middle' | 'end'
813

14+
/**
15+
* The minimum length we ever want to shorten to.
16+
*
17+
* Default is 2
18+
*/
19+
minLength?: number
20+
921
/**
1022
* Normally, the tooltip will be the text. Do you want to add something extra?
1123
*/
@@ -15,6 +27,11 @@ type AdaptiveTrimmerProps = {
1527
* Normally, the tooltip will be the text. Do you want to replace it with something else?
1628
*/
1729
tooltipOverride?: ReactNode
30+
31+
/**
32+
* De we want extra debug output about the sizing process?
33+
*/
34+
debugMode?: boolean
1835
}
1936

2037
/**
@@ -27,19 +44,37 @@ type AdaptiveTrimmerProps = {
2744
* supplying it with a generator function which simply trims the given text to the wanted length.
2845
*/
2946
export const AdaptiveTrimmer: FC<AdaptiveTrimmerProps> = ({
47+
idPrefix = 'adaptive-trimmer',
3048
text = '',
3149
strategy = 'end',
3250
extraTooltip,
3351
tooltipOverride,
34-
}) => (
35-
<AdaptiveDynamicTrimmer
36-
getFullContent={() => ({ content: text, length: text.length })}
37-
getShortenedContent={length =>
38-
strategy === 'middle'
39-
? trimLongString(text, Math.floor(length / 2) - 1, Math.floor(length / 2) - 1)!
40-
: trimLongString(text, length, 0)!
41-
}
42-
tooltipOverride={tooltipOverride}
43-
extraTooltip={extraTooltip}
44-
/>
45-
)
52+
minLength,
53+
debugMode,
54+
}) => {
55+
// console.log('Text', text)
56+
return (
57+
<AdaptiveDynamicTrimmer
58+
idPrefix={idPrefix}
59+
getFullContent={() => ({ content: text, length: text.length })}
60+
getShortenedContent={wantedLength => {
61+
if (wantedLength >= text.length) {
62+
return {
63+
content: text,
64+
length: text.length,
65+
}
66+
}
67+
const content =
68+
strategy === 'middle'
69+
? trimLongString(text, Math.floor(wantedLength / 2), Math.ceil(wantedLength / 2) - 1)!
70+
: trimLongString(text, wantedLength, 0)!
71+
const length = content.length
72+
return { content, length }
73+
}}
74+
minLength={minLength}
75+
tooltipOverride={tooltipOverride}
76+
extraTooltip={extraTooltip}
77+
debugMode={debugMode}
78+
/>
79+
)
80+
}

0 commit comments

Comments
 (0)