Skip to content

Commit

Permalink
fix(Ellipsis): Use React life cycle hooks to avoid render not correct…
Browse files Browse the repository at this point in the history
… in Suspense (#6770)

* fix: ellipsis cut break

* test: add test case

* fix: useResizeEffect ref
  • Loading branch information
zombieJ authored Oct 29, 2024
1 parent c4e181d commit a36e90b
Show file tree
Hide file tree
Showing 5 changed files with 279 additions and 274 deletions.
4 changes: 2 additions & 2 deletions src/components/ellipsis/demos/demo1.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react'
import { Ellipsis, Space } from 'antd-mobile'
import { DemoBlock } from 'demos'
import { DownOutline, UpOutline } from 'antd-mobile-icons'
import { DemoBlock } from 'demos'
import React from 'react'

const content =
'蚂蚁的企业级产品是一个庞大且复杂的体系。这类产品不仅量级巨大且功能复杂,而且变动和并发频繁,常常需要设计与开发能够快速的做出响应。同时这类产品中有存在很多类似的页面以及组件,可以通过抽象得到一些稳定且高复用性的内容。'
Expand Down
273 changes: 60 additions & 213 deletions src/components/ellipsis/ellipsis.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import React, { useMemo, useRef, useState } from 'react'
import type { FC, ReactNode } from 'react'
import runes from 'runes2'
import { mergeProps } from '../../utils/with-default-props'
import React from 'react'
import { NativeProps, withNativeProps } from '../../utils/native-props'
import { useResizeEffect } from '../../utils/use-resize-effect'
import { useIsomorphicLayoutEffect } from 'ahooks'
import { mergeProps } from '../../utils/with-default-props'
import {
PropagationEvent,
withStopPropagation,
} from '../../utils/with-stop-propagation'
import useMeasure from './useMeasure'

const classPrefix = `adm-ellipsis`

Expand All @@ -34,230 +33,78 @@ const defaultProps = {
defaultExpanded: false,
}

type EllipsisedValue = {
leading?: string
tailing?: string
}

export const Ellipsis: FC<EllipsisProps> = p => {
const props = mergeProps(defaultProps, p)
const rootRef = useRef<HTMLDivElement>(null)
const expandElRef = useRef<HTMLAnchorElement>(null)
const collapseElRef = useRef<HTMLAnchorElement>(null)

const [ellipsised, setEllipsised] = useState<EllipsisedValue>({})
const [expanded, setExpanded] = useState(props.defaultExpanded)
const [exceeded, setExceeded] = useState(false)

const chars = useMemo(() => runes(props.content), [props.content])
function getSubString(start: number, end: number) {
return chars.slice(start, end).join('')
}

function calcEllipsised() {
const root = rootRef.current
if (!root) return

const originDisplay = root.style.display
root.style.display = 'block'

const originStyle = window.getComputedStyle(root)
const container = document.createElement('div')

const styleNames: string[] = Array.prototype.slice.apply(originStyle)
styleNames.forEach(name => {
container.style.setProperty(name, originStyle.getPropertyValue(name))
})

root.style.display = originDisplay

container.style.height = 'auto'
container.style.minHeight = 'auto'
container.style.maxHeight = 'auto'
container.style.textOverflow = 'clip'
container.style.webkitLineClamp = 'unset'
container.style.display = 'block'

const lineHeight = pxToNumber(originStyle.lineHeight)
const maxHeight = Math.floor(
lineHeight * (props.rows + 0.5) +
pxToNumber(originStyle.paddingTop) +
pxToNumber(originStyle.paddingBottom)
)

container.innerText = props.content
document.body.appendChild(container)

if (container.offsetHeight <= maxHeight) {
setExceeded(false)
} else {
setExceeded(true)
const end = props.content.length

const collapseEl =
typeof props.collapseText === 'string'
? props.collapseText
: collapseElRef.current?.innerHTML
const expandEl =
typeof props.expandText === 'string'
? props.expandText
: expandElRef.current?.innerHTML
const actionText = expanded ? collapseEl : expandEl

function check(left: number, right: number): EllipsisedValue {
if (right - left <= 1) {
if (props.direction === 'end') {
return {
leading: getSubString(0, left) + '...',
}
} else {
return {
tailing: '...' + getSubString(right, end),
}
}
}
const middle = Math.round((left + right) / 2)
if (props.direction === 'end') {
container.innerHTML = getSubString(0, middle) + '...' + actionText
} else {
container.innerHTML = actionText + '...' + getSubString(middle, end)
}

if (container.offsetHeight <= maxHeight) {
if (props.direction === 'end') {
return check(middle, right)
} else {
return check(left, middle)
}
} else {
if (props.direction === 'end') {
return check(left, middle)
} else {
return check(middle, right)
}
}
}

function checkMiddle(
leftPart: [number, number],
rightPart: [number, number]
): EllipsisedValue {
if (
leftPart[1] - leftPart[0] <= 1 &&
rightPart[1] - rightPart[0] <= 1
) {
return {
leading: getSubString(0, leftPart[0]) + '...',
tailing: '...' + getSubString(rightPart[1], end),
}
}
const leftPartMiddle = Math.floor((leftPart[0] + leftPart[1]) / 2)
const rightPartMiddle = Math.ceil((rightPart[0] + rightPart[1]) / 2)
container.innerHTML =
getSubString(0, leftPartMiddle) +
'...' +
actionText +
'...' +
getSubString(rightPartMiddle, end)
if (container.offsetHeight <= maxHeight) {
return checkMiddle(
[leftPartMiddle, leftPart[1]],
[rightPart[0], rightPartMiddle]
)
} else {
return checkMiddle(
[leftPart[0], leftPartMiddle],
[rightPartMiddle, rightPart[1]]
)
}
}

const middle = Math.floor((0 + end) / 2)
const ellipsised =
props.direction === 'middle'
? checkMiddle([0, middle], [middle, end])
: check(0, end)
setEllipsised(ellipsised)
}
document.body.removeChild(container)
}

useResizeEffect(calcEllipsised, rootRef)
useIsomorphicLayoutEffect(() => {
calcEllipsised()
}, [
props.content,
props.direction,
props.rows,
props.expandText,
props.collapseText,
])

const expandActionElement =
!!props.expandText &&
withStopPropagation(
props.stopPropagationForActionButtons,
<a
ref={expandElRef}
onClick={() => {
setExpanded(true)
}}
>
{props.expandText}
</a>
)

const collapseActionElement =
!!props.collapseText &&
withStopPropagation(
props.stopPropagationForActionButtons,
<a
ref={collapseElRef}
onClick={() => {
setExpanded(false)
}}
>
{props.collapseText}
</a>
)

const renderContent = () => {
if (!exceeded) return props.content

if (expanded)
return (
<>
{props.content}
{collapseActionElement}
</>
const {
content,
direction,
rows,
expandText,
collapseText,
stopPropagationForActionButtons,
onContentClick,
defaultExpanded,
} = props

// ============================ Refs ============================
const rootRef = React.useRef<HTMLDivElement>(null)

// ========================== Expanded ==========================
const [expanded, setExpanded] = React.useState(defaultExpanded)

const expandNode = expandText
? withStopPropagation(
stopPropagationForActionButtons,
<a
onClick={() => {
setExpanded(true)
}}
>
{expandText}
</a>
)
: null

const collapseNode = collapseText
? withStopPropagation(
stopPropagationForActionButtons,
<a
onClick={() => {
setExpanded(false)
}}
>
{collapseText}
</a>
)
return (
<>
{ellipsised.leading}
{expandActionElement}
{ellipsised.tailing}
</>
)
}
: null

// ========================== Ellipsis ==========================
const [measureNodes, forceResize] = useMeasure(
rootRef,
content,
rows,
direction,
expanded,
expandNode,
collapseNode
)

useResizeEffect(forceResize, rootRef)

// =========================== Render ===========================
return withNativeProps(
props,
<div
ref={rootRef}
className={classPrefix}
onClick={e => {
if (e.target === e.currentTarget) {
props.onContentClick(e)
onContentClick(e)
}
}}
>
{renderContent()}
{measureNodes}
</div>
)
}

function pxToNumber(value: string | null): number {
if (!value) return 0
const match = value.match(/^\d*(\.\d*)?/)
return match ? Number(match[0]) : 0
}
24 changes: 12 additions & 12 deletions src/components/ellipsis/tests/__snapshots__/ellipsis.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -4,38 +4,38 @@ exports[`Ellipsis direction end 1`] = `
<div
class="adm-ellipsis"
data-testid="ellipsis"
style=""
>
蚂蚁的企业级产品是一个庞大且复杂的体系。这类产品不仅...
蚂蚁的企业级产品是一个庞大且复杂的体系。这类产品不仅量
...
</div>
`;

exports[`Ellipsis direction middle 1`] = `
<div
class="adm-ellipsis"
data-testid="ellipsis"
style=""
>
蚂蚁的企业级产品是一个庞...
...些稳定且高复用性的内容。
蚂蚁的企业级产品是一个庞
...
...
些稳定且高复用性的内容。
</div>
`;

exports[`Ellipsis direction start 1`] = `
<div
class="adm-ellipsis"
data-testid="ellipsis"
style=""
>
...以及组件,可以通过抽象得到一些稳定且高复用性的内容。
...
面以及组件,可以通过抽象得到一些稳定且高复用性的内容。
</div>
`;

exports[`Ellipsis expand and collapse 1`] = `
<div
class="adm-ellipsis"
data-testid="ellipsis"
style=""
>
蚂蚁的企业级产品是一个庞大且复杂的体系。这类产品不仅量级巨大且功能复杂,而且变动和并发频繁,常常需要设计与开发能够快速的做出响应。同时这类产品中有存在很多类似的页面以及组件,可以通过抽象得到一些稳定且高复用性的内容。
<a>
Expand All @@ -48,9 +48,9 @@ exports[`Ellipsis expand and collapse 2`] = `
<div
class="adm-ellipsis"
data-testid="ellipsis"
style=""
>
蚂蚁的企业级产品是一个庞大且复杂的体系。...
蚂蚁的企业级产品是一个庞大且复杂的体系。这
...
<a>
expand
</a>
Expand All @@ -61,8 +61,8 @@ exports[`Ellipsis multi line 1`] = `
<div
class="adm-ellipsis"
data-testid="ellipsis"
style=""
>
蚂蚁的企业级产品是一个庞大且复杂的体系。这类产品不仅量级巨大且功能复杂,而且变动和并发频繁,常常需要设计与开发能够快速的做出响应。同时这类产品中有存在很多类似的...
蚂蚁的企业级产品是一个庞大且复杂的体系。这类产品不仅量级巨大且功能复杂,而且变动和并发频繁,常常需要设计与开发能够快速的做出响应。同时这类产品中有存在很多类似的页面以及组件,
...
</div>
`;
Loading

0 comments on commit a36e90b

Please sign in to comment.