-
Couldn't load subscription status.
- Fork 60
[UIK-2976] ellipsis improvements #2314
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: release/v16
Are you sure you want to change the base?
Changes from 20 commits
f7caa2b
336813b
12468ee
35f42c5
9dcc69c
a73ed87
63b47af
3000289
a05494e
6b34a84
307b314
3c299d5
70dd5d6
8ca531b
3b787e3
f102c6d
9fdd71b
d1ef679
891350b
abaa758
38b8a4d
1ffb06e
6450ee6
bfda6b4
3854d9f
33fb56a
d774b3f
c5368a7
a4f9165
1ac27a6
69aa581
deced57
3662c1a
ec2feb4
9d13920
4b1f00e
4a5a357
5110dc4
964c26a
46d7231
2a73a9c
8c26a1f
5bcfc8f
f142fb1
247c3cc
48e653d
225c371
e85eba6
306875c
2571b40
54de8ca
94a76bf
47a62a9
cca58d2
351cb35
e0f4acc
8fb3582
49646e7
b56532f
3929e80
4b902e0
ba958a3
dca4dda
8fda3c1
8b631c1
8fa062c
95bed85
4732f40
5b6e5fb
b142a7a
87c2a31
95081b8
f4a5713
f5f1855
4d6b30e
de46914
a1ba4fd
10d4787
b86b35a
6bd7955
9fc8378
eb1844d
0357981
b3d5163
989a594
a982134
49012d7
41b1912
e6041c9
8e330a9
4bdd2a0
0da769c
898895b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| import { useEllipsis, isTextOverflowing } from './useEllipsis'; | ||
| import type { EllipsisSettings } from './useEllipsis'; | ||
| import { useResizeObserver } from './useResizeObserver'; | ||
|
|
||
| export { | ||
| useEllipsis, | ||
| isTextOverflowing, | ||
| useResizeObserver, | ||
| }; | ||
| export type { | ||
| EllipsisSettings, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,117 @@ | ||
| import React from 'react'; | ||
|
|
||
| import { useResizeObserver } from './useResizeObserver'; | ||
|
|
||
| export type EllipsisSettings = { | ||
| trim?: 'end' | 'middle'; | ||
| maxLine?: number; | ||
sheila-semrush marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it seems we need an ability to disable hint for multiline cases (paragraphs of text) |
||
| }; | ||
|
|
||
| function createMeasurerElement(element: HTMLElement, text?: string) { | ||
| const styleElement = window.getComputedStyle(element, null); | ||
| const temporaryElement = document.createElement('temporary-block'); | ||
| temporaryElement.style.display = styleElement.getPropertyValue('display'); | ||
| temporaryElement.style.padding = styleElement.getPropertyValue('padding'); | ||
| temporaryElement.style.position = 'absolute'; | ||
| temporaryElement.style.right = '0%'; | ||
| temporaryElement.style.bottom = '0%'; | ||
| temporaryElement.style.visibility = 'hidden'; | ||
| temporaryElement.style.fontFamily = styleElement.getPropertyValue('font-family'); | ||
| temporaryElement.style.fontSize = styleElement.getPropertyValue('font-size'); | ||
| temporaryElement.style.fontWeight = styleElement.getPropertyValue('font-weight'); | ||
| temporaryElement.style.lineHeight = styleElement.getPropertyValue('line-height'); | ||
| temporaryElement.style.whiteSpace = styleElement.getPropertyValue('white-space'); | ||
| temporaryElement.style.wordWrap = styleElement.getPropertyValue('word-wrap'); | ||
|
|
||
| temporaryElement.style.fontFeatureSettings = | ||
| styleElement.getPropertyValue('font-feature-settings'); | ||
| temporaryElement.style.fontVariantNumeric = styleElement.getPropertyValue('font-variant-numeric'); | ||
|
|
||
| temporaryElement.innerHTML = text ?? element.innerHTML; | ||
| return temporaryElement; | ||
| } | ||
|
|
||
| export function isTextOverflowing(element: HTMLElement | null, multiline: boolean, text?: string): boolean { | ||
| if (!element) return false; | ||
|
|
||
| const { height: currentHeight, width: currentWidth } = element.getBoundingClientRect(); | ||
| const measuringElement = createMeasurerElement(element, text); | ||
| let isOverflowing = false; | ||
|
|
||
| document.body.appendChild(measuringElement); | ||
| if (multiline) { | ||
| measuringElement.style.width = `${currentWidth}px`; | ||
|
|
||
| const width = measuringElement.scrollWidth; | ||
| const height = measuringElement.getBoundingClientRect().height; | ||
|
|
||
| if (Math.ceil(currentHeight) < height || Math.ceil(currentWidth) < width) { | ||
| isOverflowing = true; | ||
| } | ||
| } else { | ||
| measuringElement.style.whiteSpace = 'nowrap'; | ||
| isOverflowing = Math.ceil(currentWidth) < measuringElement.getBoundingClientRect().width; | ||
| } | ||
|
|
||
| document.body.removeChild(measuringElement); | ||
|
|
||
| return isOverflowing; | ||
| } | ||
|
|
||
| export function useEllipsis(ref: React.RefObject<HTMLElement>, props: EllipsisSettings | false): boolean { | ||
| const [isEllipsized, setIsEllipsized] = React.useState(false); | ||
|
|
||
| const maxLine = props === false ? undefined : (props.maxLine ?? 1); | ||
| const trim = props === false ? undefined : (props.trim ?? 'end'); | ||
|
|
||
| React.useEffect(() => { | ||
| if (!ref.current) return; | ||
| if (trim === undefined || maxLine === undefined) { | ||
| setIsEllipsized(false); | ||
| return; | ||
| }; | ||
| ref.current.style.setProperty('overflow', 'hidden'); | ||
| ref.current.style.setProperty('text-overflow', 'hidden'); | ||
| ref.current.style.setProperty('white-space', 'pre'); | ||
|
|
||
| const isEllipsized = isTextOverflowing(ref.current, maxLine > 1); | ||
| setIsEllipsized(isEllipsized); | ||
| }, [ref.current, trim, maxLine]); | ||
|
|
||
| const blockWidth = useResizeObserver(ref, isEllipsized ? undefined : { width: 0 }).width; | ||
|
|
||
| React.useEffect(() => { | ||
| if (!ref.current || !isEllipsized) return; | ||
| if (trim === undefined || maxLine === undefined) { | ||
| return; | ||
| } | ||
|
|
||
| if (trim === 'end') { | ||
| ref.current.style.setProperty('text-overflow', 'ellipsis'); | ||
| } | ||
|
|
||
| if (trim === 'middle') { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it seems we need to always include aria-label with the full text for trim = 'middle', beause otherwise the SR will read the truncated text There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the hint should add aria-label (if not, we will fix it there, what do you think?) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. but it adds aria-label only when visible, i.e. on hover, or no? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
yep, sure |
||
| ref.current.style.setProperty('display', 'flex'); | ||
|
||
|
|
||
| const styleElement = window.getComputedStyle(ref.current); | ||
| const dateSpan = document.createElement('temporary-block'); | ||
| dateSpan.style.setProperty('font-size', styleElement.getPropertyValue('font-size')); | ||
| dateSpan.innerHTML = 'a'; | ||
| document.body.appendChild(dateSpan); | ||
| const symbolWidth = dateSpan.getBoundingClientRect().width; | ||
| dateSpan.remove(); | ||
|
|
||
| const displayedSymbols = Math.round(blockWidth / symbolWidth); | ||
| const evenDisplayedSymbols = displayedSymbols % 2 === 0 ? displayedSymbols : displayedSymbols - 1; | ||
|
|
||
| const text = ref.current.textContent ?? ''; | ||
|
|
||
| const begining = text.substring(0, text.length - evenDisplayedSymbols / 2 - 1); | ||
| const tail = text.substring(text.length - evenDisplayedSymbols / 2 - 1); | ||
|
|
||
| ref.current.innerHTML = `<span style="overflow: hidden; text-overflow: ellipsis">${begining}</span><span>${tail}</span>`; | ||
| } | ||
| }, [ref.current, trim, maxLine, isEllipsized, blockWidth]); | ||
|
|
||
| return isEllipsized; | ||
| } | ||

There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
how about calling it "position" or "placement"? I'm not sure "trim" is a correct term in this context. Or do we want to keep the old names?