Skip to content
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

Fix/comment expand error #4502

Merged
merged 15 commits into from
May 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 103 additions & 1 deletion src/common/utils/detect.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { describe, expect, it } from 'vitest'

import { getUserAgent, isMobile } from './detect'
import {
checkIsSafariVersionLessThan17,
getUserAgent,
isMobile,
} from './detect'

describe('utils/detect/getUserAgent', () => {
it('should return the user agent in lowercase', () => {
Expand Down Expand Up @@ -29,3 +33,101 @@ describe('utils/detect/isMobile', () => {
expect(isMobile()).toBe(false)
})
})

describe('utils/detect/checkIsSafariVersionLessThan17', () => {
it('should return true for Safari version less than 17', () => {
const originalUserAgent = navigator.userAgent
Object.defineProperty(navigator, 'userAgent', {
value:
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Safari/605.1.15',
configurable: true,
})

expect(checkIsSafariVersionLessThan17()).toBe(true)

Object.defineProperty(navigator, 'userAgent', {
value: originalUserAgent,
configurable: true,
})
})

it('should return false for Safari version 17 or greater', () => {
const originalUserAgent = navigator.userAgent
Object.defineProperty(navigator, 'userAgent', {
value:
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15',
configurable: true,
})

expect(checkIsSafariVersionLessThan17()).toBe(false)

Object.defineProperty(navigator, 'userAgent', {
value: originalUserAgent,
configurable: true,
})
})

it('should return true for Safari 16 on Apple M series chip', () => {
const originalUserAgent = navigator.userAgent
Object.defineProperty(navigator, 'userAgent', {
value:
'Mozilla/5.0 (Macintosh; Apple Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Safari/605.1.15',
configurable: true,
})

expect(checkIsSafariVersionLessThan17()).toBe(true)

Object.defineProperty(navigator, 'userAgent', {
value: originalUserAgent,
configurable: true,
})
})

it('should return false for Safari 17 on Apple M series chip', () => {
const originalUserAgent = navigator.userAgent
Object.defineProperty(navigator, 'userAgent', {
value:
'Mozilla/5.0 (Macintosh; Apple Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15',
configurable: true,
})

expect(checkIsSafariVersionLessThan17()).toBe(false)

Object.defineProperty(navigator, 'userAgent', {
value: originalUserAgent,
configurable: true,
})
})

it('should return false for non-Safari browsers', () => {
const originalUserAgent = navigator.userAgent
Object.defineProperty(navigator, 'userAgent', {
value:
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
configurable: true,
})

expect(checkIsSafariVersionLessThan17()).toBe(false)

Object.defineProperty(navigator, 'userAgent', {
value: originalUserAgent,
configurable: true,
})
})

it('should return false if version is not found', () => {
const originalUserAgent = navigator.userAgent
Object.defineProperty(navigator, 'userAgent', {
value:
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Safari/605.1.15',
configurable: true,
})

expect(checkIsSafariVersionLessThan17()).toBe(false)

Object.defineProperty(navigator, 'userAgent', {
value: originalUserAgent,
configurable: true,
})
})
})
17 changes: 17 additions & 0 deletions src/common/utils/detect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,20 @@ export const isMobile = () => {
userAgent
)
}

const getSafariVersion = (userAgent: string): string | null => {
const userAgentRegex =
/^Mozilla\/5\.0 \(Macintosh; (Intel|Apple) Mac OS X \d+_\d+(?:_\d+)?\) AppleWebKit\/\d+(?:\.\d+)* \(KHTML, like Gecko\) Version\/(\d+\.\d+(?:\.\d+)*) Safari\/\d+(?:\.\d+)*$/
const match = userAgent.match(userAgentRegex)
return match ? match[2] : null // The capturing group for the version number
}

export const checkIsSafariVersionLessThan17 = () => {
const userAgent = navigator.userAgent
const version = getSafariVersion(userAgent)
if (version) {
const majorVersion = parseInt(version.split('.')[0], 10)
return majorVersion < 17
}
return false
}
2 changes: 2 additions & 0 deletions src/components/CommentBeta/Content/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ const Content = ({
bgColor={bgColor}
textIndent={textIndent}
size={size}
collapseable={false}
isComment
>
<section
className={`${contentClasses} u-content-comment`}
Expand Down
142 changes: 98 additions & 44 deletions src/components/Expandable/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import React, { ReactElement, useEffect, useRef, useState } from 'react'
import { FormattedMessage } from 'react-intl'

import { ReactComponent as IconUp } from '@/public/static/icons/24px/up.svg'
import { capitalizeFirstLetter, stripHtml } from '~/common/utils'
import {
capitalizeFirstLetter,
checkIsSafariVersionLessThan17,
stripHtml,
} from '~/common/utils'
import {
Button,
Icon,
Expand Down Expand Up @@ -32,10 +36,34 @@ interface ExpandableProps {
spacingTop?: 'tight' | 'base'
textIndent?: boolean
isRichShow?: boolean
isComment?: boolean
collapseable?: boolean
bgColor?: 'greyLighter' | 'white'
}

const calculateCommentContentHeight = (element: HTMLElement): number => {
let height = 0
const contentNode = element.firstElementChild?.firstElementChild
if (contentNode) {
contentNode.childNodes.forEach((child) => {
const e = child as HTMLElement
height += e.clientHeight
})
}
return height
}

const calculateContentHeight = (
element: HTMLElement,
isRichShow: boolean,
isComment: boolean
): number => {
if (isRichShow && isComment) {
return calculateCommentContentHeight(element)
}
return element.firstElementChild?.clientHeight || 0
}

export const Expandable: React.FC<ExpandableProps> = ({
children,
content,
Expand All @@ -45,23 +73,34 @@ export const Expandable: React.FC<ExpandableProps> = ({
size,
spacingTop,
textIndent = false,
isRichShow = false,
isRichShow: _isRichShow = false,
isComment = false,
collapseable = true,
bgColor = 'white',
}) => {
const [expandable, setExpandable] = useState(false)
const [firstRender, setFirstRender] = useState(true)
const [expand, setExpand] = useState(true)
const [isOverFlowing, setIsOverFlowing] = useState(false)
const [isExpanded, setIsExpanded] = useState(true)
const [isRichShow, setIsRichShow] = useState(_isRichShow)
const node: React.RefObject<HTMLParagraphElement> | null = useRef(null)
const collapsedContent = stripHtml(content || '')

const [isSafariVersionLessThan17, setIsSafariVersionLessThan17] =
useState(false)
const [needAdjustForSafari, setNeedAdjustForSafari] = useState(false)

useEffect(() => {
// FIXME: Safari version less than 17 has a bug that -webkit-line-clamp overlapping blocks even with overflow: hidden, when mixing <span> and <div>.
if (checkIsSafariVersionLessThan17()) {
setIsSafariVersionLessThan17(true)
}
}, [])

const contentClasses = classNames({
[styles.expandable]: true,
[styles.content]: true,
[styles[`${color}`]]: !!color,
[size ? styles[`text${size}`] : '']: !!size,
[spacingTop
? styles[`spacingTop${capitalizeFirstLetter(spacingTop)}`]
: '']: !!spacingTop,
[styles[`text${size}`]]: !!size,
[styles[`spacingTop${capitalizeFirstLetter(spacingTop || '')}`]]:
!!spacingTop,
[styles.textIndent]: textIndent,
})

Expand All @@ -73,69 +112,86 @@ export const Expandable: React.FC<ExpandableProps> = ({
const richShowMoreButtonClasses = classNames({
[styles.richShowMoreButton]: true,
[styles[`${bgColor}`]]: !!bgColor,
[size ? styles[`text${size}`] : '']: !!size,
[styles[`text${size}`]]: !!size,
})

useIsomorphicLayoutEffect(() => {
setExpandable(false)
setExpand(true)
if (node?.current) {
const height = node.current.firstElementChild?.clientHeight || 0
const lineHeight = window
.getComputedStyle(node.current, null)
.getPropertyValue('line-height')
const lines = Math.max(Math.ceil(height / parseInt(lineHeight, 10)), 0)

if (lines > limit + buffer) {
setExpandable(true)
setExpand(false)
}
const reset = () => {
setIsOverFlowing(false)
setIsExpanded(true)
}

const handleOverflowCheck = () => {
if (!node?.current) {
return
}
const element = node.current
const height = calculateContentHeight(element, isRichShow, isComment)
const lineHeight = window
.getComputedStyle(element, null)
.getPropertyValue('line-height')
const lines = Math.max(Math.ceil(height / parseFloat(lineHeight)), 0)

if (lines > limit + buffer) {
setIsOverFlowing(true)
setIsExpanded(false)
}
}

useIsomorphicLayoutEffect(() => {
// FIXED: reset state to fix the case from overflow to non-overflow condition.
reset()

handleOverflowCheck()
}, [content])

useEffect(() => {
if (firstRender) {
setFirstRender(false)
if (isSafariVersionLessThan17 && isRichShow) {
reset()
setIsRichShow(false)
setNeedAdjustForSafari(true)
}
}, [firstRender])
}, [isSafariVersionLessThan17])

useEffect(() => {
if (needAdjustForSafari) {
handleOverflowCheck()
}
}, [needAdjustForSafari])

const toggleIsExpand = () => {
setIsExpanded(!isExpanded)
}

return (
<section className={contentClasses}>
<div ref={node}>
{(!expandable || (expandable && expand)) && (
<div
className={firstRender ? styles.lineClamp : ''}
style={{ WebkitLineClamp: limit + 2 }}
>
{children}
</div>
{(!isOverFlowing || (isOverFlowing && isExpanded)) && (
<div>{children}</div>
)}
</div>
{expandable && collapseable && expand && !isRichShow && (
{isOverFlowing && isExpanded && collapseable && !isRichShow && (
<section className={styles.collapseWrapper}>
<Button
spacing={[4, 8]}
bgColor="greyLighter"
textColor="grey"
onClick={() => {
setExpand(!expand)
}}
onClick={toggleIsExpand}
>
<TextIcon icon={<Icon icon={IconUp} />} placement="left">
<FormattedMessage defaultMessage="Collapse" id="W/V6+Y" />
</TextIcon>
</Button>
</section>
)}
{expandable && !expand && (
{isOverFlowing && !isExpanded && (
<p className={styles.unexpandWrapper}>
{!isRichShow && (
<Truncate
lines={limit}
ellipsis={
<span
onClick={(e) => {
setExpand(!expand)
toggleIsExpand()
e.stopPropagation()
}}
className={styles.expandButton}
Expand All @@ -159,9 +215,7 @@ export const Expandable: React.FC<ExpandableProps> = ({
</section>
<button
className={richShowMoreButtonClasses}
onClick={() => {
setExpand(!expand)
}}
onClick={toggleIsExpand}
>
<FormattedMessage
defaultMessage="Expand"
Expand Down
2 changes: 1 addition & 1 deletion src/components/Expandable/styles.module.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.expandable {
.content {
position: relative;
white-space: pre-wrap;

Expand Down
Loading