"use client"
import useMergedRef from "@react-hook/merged-ref"
import React, {
ElementRef,
forwardRef,
useCallback,
useLayoutEffect,
useRef,
useState,
} from "react"
import { useCallbackRef, useIsOverflowed } from "../../hooks"
import {
LineClampVariantProps,
lineClampVariants,
} from "../../styles/utils/lineclamp"
import { classNames } from "../../utils/classNames"
import { Flex, FlexProps } from "../Flex"
import { MARKDOWN_SELECTOR } from "../Markdown/constants"
import { Text } from "../Text"
import { UnstyledButton } from "../UnstyledButton"
type BaseTextOverflowProps = {
children: React.ReactNode
className?: string
open?: boolean
onOpenChange?: (open: boolean) => unknown
inlineSeeMore?: boolean
} & LineClampVariantProps &
FlexProps
type ControlledTextOverflowProps = {
open: boolean
} & BaseTextOverflowProps
type UncontrolledTextOverflowProps = {
open?: never
} & BaseTextOverflowProps
export type TextOverflowProps =
| ControlledTextOverflowProps
| UncontrolledTextOverflowProps
const isControlledTextOverflow = (
props: TextOverflowProps,
): props is ControlledTextOverflowProps => {
return "open" in props
}
/**
* Renders a See more/See less button when the text overflows.
*/
export const TextOverflow = (props: TextOverflowProps) => {
if (isControlledTextOverflow(props)) {
return
}
return
}
const UncontrolledTextOverflowProps = ({
onOpenChange,
...rest
}: UncontrolledTextOverflowProps) => {
const [open, setOpen] = useState(false)
const handleOnOpenChange = useCallback(() => {
setOpen(!open)
onOpenChange?.(!open)
}, [onOpenChange, open])
return (
)
}
const ControlledTextOverflow = ({
children,
className,
open,
onOpenChange,
lines = 1,
inlineSeeMore,
...rest
}: ControlledTextOverflowProps) => {
const [ref, setRef] = useCallbackRef()
const isOverflowed = useIsOverflowed(ref)
const showSeeMore = isOverflowed || open
return (
{children}
{showSeeMore && (
onOpenChange?.(!open)}
>
{open ? "See less" : "See more"}
)}
)
}
const OverflowContainer = forwardRef<
ElementRef<"div">,
{ children: React.ReactNode; open?: boolean } & LineClampVariantProps
>(function OverflowContainer({ children, lines, open }, ref) {
const containerRef = useRef(null)
const mergedRef = useMergedRef(ref, containerRef)
const [width, setWidth] = useState(undefined)
return (
<>
{children}
{children}
>
)
})
const MeasuringContainer = ({
enabled,
children,
lines,
onMeasure,
}: {
enabled: boolean
children: React.ReactNode
lines?: number
onMeasure: (width: number | undefined) => void
}) => {
const [containerRef, setContainerRef] = useCallbackRef()
// Calculate width based on markdown content
useLayoutEffect(() => {
if (!enabled || !containerRef.current) {
return
}
const markdownContent =
containerRef.current.querySelector(MARKDOWN_SELECTOR)
if (!markdownContent) {
onMeasure(undefined)
return
}
const childrenLines = Array.from(markdownContent.children).slice(0, lines)
if (!childrenLines.length) {
onMeasure(undefined)
return
}
const TRUNCATE_BUFFER = 12
const measuredWidth =
Math.max(
...childrenLines.map(el => el.getBoundingClientRect().width || 0),
) + TRUNCATE_BUFFER
onMeasure(measuredWidth)
}, [enabled, lines, onMeasure, containerRef])
return (
{children}
)
}