-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1397 from oasisprotocol/csillag/adaptive-text-sho…
…rtener-and-highlighter Add dynamically resizing label with highlight, for adaptively filling up horizontal space
- Loading branch information
Showing
9 changed files
with
411 additions
and
146 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Use adaptive method when trimming texts with highlights |
154 changes: 154 additions & 0 deletions
154
src/app/components/AdaptiveTrimmer/AdaptiveDynamicTrimmer.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
import { FC, ReactNode, useCallback, useEffect, useRef, useState } from 'react' | ||
import Box from '@mui/material/Box' | ||
import InfoIcon from '@mui/icons-material/Info' | ||
import { MaybeWithTooltip } from './MaybeWithTooltip' | ||
|
||
type AdaptiveDynamicTrimmerProps = { | ||
getFullContent: () => { | ||
content: ReactNode | ||
length: number | ||
} | ||
getShortenedContent: (wantedLength: number) => ReactNode | ||
extraTooltip: ReactNode | ||
} | ||
|
||
/** | ||
* Display content, potentially shortened as needed. | ||
* | ||
* This component will do automatic detection of available space, | ||
* and determine the best way to display content accordingly. | ||
* | ||
* The difference compared to AdaptiveTrimmer is that this component | ||
* expects a function to provide a shortened version of the components. | ||
*/ | ||
export const AdaptiveDynamicTrimmer: FC<AdaptiveDynamicTrimmerProps> = ({ | ||
getFullContent, | ||
getShortenedContent, | ||
extraTooltip, | ||
}) => { | ||
// Initial setup | ||
const textRef = useRef<HTMLDivElement | null>(null) | ||
const { content: fullContent, length: fullLength } = getFullContent() | ||
|
||
// Data about the currently rendered version | ||
const [currentContent, setCurrentContent] = useState<ReactNode>() | ||
const [currentLength, setCurrentLength] = useState(0) | ||
|
||
// Known good - this fits | ||
const [largestKnownGood, setLargestKnownGood] = useState(0) | ||
|
||
// Known bad - this doesn't fit | ||
const [smallestKnownBad, setSmallestKnownBad] = useState(fullLength + 1) | ||
|
||
// Are we exploring our possibilities now? | ||
const [inDiscovery, setInDiscovery] = useState(false) | ||
|
||
const attemptContent = useCallback((content: ReactNode, length: number) => { | ||
setCurrentContent(content) | ||
setCurrentLength(length) | ||
}, []) | ||
|
||
const attemptShortenedContent = useCallback( | ||
(length: number) => { | ||
const content = getShortenedContent(length) | ||
|
||
attemptContent(content, length) | ||
}, | ||
[attemptContent, getShortenedContent], | ||
) | ||
|
||
const initDiscovery = useCallback(() => { | ||
setLargestKnownGood(0) | ||
setSmallestKnownBad(fullLength + 1) | ||
attemptContent(fullContent, fullLength) | ||
setInDiscovery(true) | ||
}, [fullContent, fullLength, attemptContent]) | ||
|
||
useEffect(() => { | ||
initDiscovery() | ||
const handleResize = () => { | ||
initDiscovery() | ||
} | ||
|
||
window.addEventListener('resize', handleResize) | ||
return () => window.removeEventListener('resize', handleResize) | ||
}, [initDiscovery]) | ||
|
||
useEffect(() => { | ||
if (inDiscovery) { | ||
if (!textRef.current) { | ||
return | ||
} | ||
const isOverflow = textRef.current.scrollWidth > textRef.current.clientWidth | ||
|
||
if (isOverflow) { | ||
// This is too much | ||
|
||
// Update known bad length | ||
const newSmallestKnownBad = Math.min(currentLength, smallestKnownBad) | ||
setSmallestKnownBad(newSmallestKnownBad) | ||
|
||
// We should try something smaller | ||
attemptShortenedContent(Math.floor((largestKnownGood + newSmallestKnownBad) / 2)) | ||
} else { | ||
// This is OK | ||
|
||
// Update known good length | ||
const newLargestKnownGood = Math.max(currentLength, largestKnownGood) | ||
setLargestKnownGood(currentLength) | ||
|
||
if (currentLength === fullLength) { | ||
// The whole thing fits, so we are good. | ||
setInDiscovery(false) | ||
} else { | ||
if (currentLength + 1 === smallestKnownBad) { | ||
// This the best we can do, for now | ||
setInDiscovery(false) | ||
} else { | ||
// So far, so good, but we should try something longer | ||
attemptShortenedContent(Math.floor((newLargestKnownGood + smallestKnownBad) / 2)) | ||
} | ||
} | ||
} | ||
} | ||
}, [ | ||
attemptShortenedContent, | ||
currentLength, | ||
fullContent, | ||
fullLength, | ||
inDiscovery, | ||
initDiscovery, | ||
largestKnownGood, | ||
smallestKnownBad, | ||
]) | ||
|
||
const title = | ||
currentLength !== fullLength ? ( | ||
<Box> | ||
<Box>{fullContent}</Box> | ||
{extraTooltip && ( | ||
<Box sx={{ display: 'inline-flex', alignItems: 'center', gap: 2 }}> | ||
<InfoIcon /> | ||
{extraTooltip} | ||
</Box> | ||
)} | ||
</Box> | ||
) : ( | ||
extraTooltip | ||
) | ||
|
||
return ( | ||
<Box | ||
ref={textRef} | ||
sx={{ | ||
overflow: 'hidden', | ||
maxWidth: '100%', | ||
textWrap: 'nowrap', | ||
}} | ||
> | ||
<MaybeWithTooltip title={title} spanSx={{ whiteSpace: 'nowrap' }}> | ||
{currentContent} | ||
</MaybeWithTooltip> | ||
</Box> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
66 changes: 66 additions & 0 deletions
66
src/app/components/HighlightedText/AdaptiveHighlightedText.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
import { FC, ReactNode } from 'react' | ||
import InfoIcon from '@mui/icons-material/Info' | ||
import { HighlightedText, HighlightOptions } from './index' | ||
import { AdaptiveDynamicTrimmer } from '../AdaptiveTrimmer/AdaptiveDynamicTrimmer' | ||
import { HighlightedTrimmedText } from './HighlightedTrimmedText' | ||
|
||
type AdaptiveHighlightedTextProps = { | ||
/** | ||
* The text to display | ||
*/ | ||
text: string | undefined | ||
|
||
/** | ||
* The pattern to search for (and highlight) | ||
*/ | ||
pattern: string | undefined | ||
|
||
/** | ||
* Options for highlighting (case sensitivity, styling, etc.) | ||
* | ||
* (This is optional, sensible defaults are provided.) | ||
*/ | ||
options?: HighlightOptions | ||
|
||
/** | ||
* Extra content to put into the tooltip | ||
*/ | ||
extraTooltip?: ReactNode | ||
} | ||
|
||
/** | ||
* Display a text with a part highlighted, adaptively trimmed to the maximum length around the highlight | ||
*/ | ||
export const AdaptiveHighlightedText: FC<AdaptiveHighlightedTextProps> = ({ | ||
text, | ||
pattern, | ||
options, | ||
extraTooltip, | ||
}) => { | ||
const fullContent = <HighlightedText text={text} pattern={pattern} options={options} /> | ||
|
||
return text ? ( | ||
<AdaptiveDynamicTrimmer | ||
getFullContent={() => ({ | ||
content: fullContent, | ||
length: text.length, | ||
})} | ||
getShortenedContent={wantedLength => ( | ||
<HighlightedTrimmedText | ||
fragmentLength={wantedLength} | ||
text={text} | ||
pattern={pattern} | ||
options={options} | ||
/> | ||
)} | ||
extraTooltip={ | ||
extraTooltip ? ( | ||
<> | ||
<InfoIcon /> | ||
{extraTooltip} | ||
</> | ||
) : undefined | ||
} | ||
/> | ||
) : undefined | ||
} |
Oops, something went wrong.