(null)
+
+ const width = ref(0)
+ const cutMidIndex = ref(0)
+ const finalCutIndex = ref(0)
+ const needEllipsis = ref(false)
+
+ const enableMeasure = computed(() => props.hiddenSymbol || slots.suffix)
+
+ const measure = async () => {
+ if (!enableMeasure.value) return
+ measureStatus.value = MeasureStatus.START
+ needEllipsis.value = false
+ await nextTick()
+ // Get the basic height of ellipsis lineClamp
+ const baseRowsEllipsisHeight = baseRowsEllipsisRef.value?.getHeight() || 0
+ // Get the height of `lineClamp - 1` + symbol height
+ const descRowsEllipsisHeight =
+ props.lineClamp === 1 ? 0 : descRowsEllipsisRef.value?.getHeight() || 0
+
+ const symbolRowEllipsisHeight =
+ symbolRowEllipsisRef.value?.getHeight() || 0
+
+ const rowsWithEllipsisHeight =
+ descRowsEllipsisHeight + symbolRowEllipsisHeight
+
+ const maxRowsHeight = Math.max(
+ baseRowsEllipsisHeight,
+ rowsWithEllipsisHeight
+ )
+
+ needEllipsis.value = !!baseRowsEllipsisRef.value?.isExceed()
+ measureStatus.value = needEllipsis.value
+ ? MeasureStatus.MEASURING
+ : MeasureStatus.NO_NEED_ELLIPSIS
+
+ if (needEllipsis.value) {
+ const content = slots.default?.() || []
+ const nodesLen = getNodesLen(content)
+ finalCutIndex.value = await getFinalCutIndex(
+ [0, nodesLen],
+ maxRowsHeight + 1
+ )
+ measureStatus.value = MeasureStatus.MEASURED
+ }
+ }
+
+ // Get cutting position using binary search
+ const getFinalCutIndex = async (
+ ellipsisCutIndex: [number, number],
+ ellipsisHeight: number
+ ) => {
+ const [minIndex, maxIndex] = ellipsisCutIndex
+ if (minIndex === maxIndex) return minIndex
+ cutMidIndex.value = Math.ceil((minIndex + maxIndex) / 2)
+ await nextTick()
+ const midHeight = cutMidRef.value?.getHeight() || 0
+
+ const isOverflow = midHeight > ellipsisHeight
+ const targetMidIndex = cutMidIndex.value
+
+ if (maxIndex - minIndex === 1) {
+ return isOverflow ? minIndex : maxIndex
+ }
+
+ if (isOverflow) {
+ return getFinalCutIndex([minIndex, targetMidIndex], ellipsisHeight)
+ } else {
+ return getFinalCutIndex([targetMidIndex, maxIndex], ellipsisHeight)
+ }
+ }
+
+ const ns = useNamespace('ellipsis')
+
+ const kls = computed(() => [
+ ns.b(),
+ ns.is('single-line', !enableMeasure.value && props.lineClamp === 1),
+ ns.is('multiple-line', !enableMeasure.value && props.lineClamp > 1),
+ ])
+
+ const multipleStyle = computed(() =>
+ !enableMeasure.value && props.lineClamp > 1
+ ? { WebkitLineClamp: props.lineClamp }
+ : {}
+ )
+
+ const enableTooltip = computed(() => {
+ if (props.expanded) return false
+ if (!containerRef.value) return false
+ if (enableMeasure.value) return needEllipsis.value
+ const textEle = containerRef.value
+ if (props.lineClamp === 1)
+ return textEle.offsetWidth < textEle.scrollWidth
+ if (props.lineClamp > 1)
+ return textEle.offsetHeight < textEle.scrollHeight
+ return false
+ })
+
+ useResizeObserver(
+ computed(() => (enableMeasure.value ? containerRef.value : null)),
+ (entries) => {
+ width.value = entries[0].contentRect.width
+ }
+ )
+
+ watch([width, () => props.lineClamp, () => props.hiddenSymbol], measure)
+
+ // fix: https://github.com/vuejs/core/issues/3257
+ let slotWatchStop: (() => void) | null = null
+ const slotsWatchFn = () => {
+ if (slotWatchStop) slotWatchStop()
+ slotWatchStop = watch(
+ [() => slots.default?.(), () => slots.suffix?.({ needEllipsis: true })],
+ measure
+ )
+ }
+
+ return () => {
+ if (!slots.default) return null
+ const { lineClamp } = props
+ const content = slots.default()
+ const suffix = slots.suffix?.({ needEllipsis: needEllipsis.value })
+ const ariaLabel = enableMeasure.value ? getAriaLabel(content) : undefined
+
+ slotsWatchFn()
+
+ const renderChildren = (
+ nodes: (string | VNode)[],
+ isEllipsis: boolean
+ ) => {
+ if (!isEllipsis) return [nodes, suffix]
+ return [
+ nodes,
+ {ELLIPSIS_SYMBOL},
+ suffix,
+ ]
+ }
+
+ const renderFinalContent = () => {
+ if (!enableMeasure.value) return content
+
+ if (measureStatus.value === MeasureStatus.NO_NEED_ELLIPSIS) {
+ return renderChildren(content, false)
+ }
+
+ if (measureStatus.value === MeasureStatus.MEASURED) {
+ return renderChildren(
+ props.expanded ? content : sliceNodes(content, finalCutIndex.value),
+ needEllipsis.value && !props.expanded
+ )
+ }
+ return (
+
+ {renderChildren(content, true)}
+
+ )
+ }
+
+ const container = (
+
+ {renderFinalContent()}
+ {measureStatus.value === MeasureStatus.START && (
+ <>
+ {/** With `lineClamp` */}
+
+ {renderChildren(content, false)}
+
+
+ {/** With `lineClamp - 1` */}
+
+ {renderChildren(content, false)}
+
+
+ {/** With `symbol row` */}
+
+ {renderChildren([], true)}
+
+ >
+ )}
+ {measureStatus.value === MeasureStatus.MEASURING && (
+
+ {renderChildren(sliceNodes(content, cutMidIndex.value), true)}
+
+ )}
+
+ )
+
+ if (props.tooltip) {
+ const tooltipProps =
+ typeof props.tooltip === 'object' ? props.tooltip : {}
+
+ return (
+
+ {{
+ default: () => container,
+ content: () => content,
+ }}
+
+ )
+ } else return container
+ }
+ },
+})
+
+export type EllipsisProps = ExtractPropTypes
+export type EllipsisInstance = InstanceType
+
+export default Ellipsis
diff --git a/packages/components/ellipsis/src/measure.vue b/packages/components/ellipsis/src/measure.vue
new file mode 100644
index 0000000000..8b41249355
--- /dev/null
+++ b/packages/components/ellipsis/src/measure.vue
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
diff --git a/packages/components/ellipsis/style/css.ts b/packages/components/ellipsis/style/css.ts
new file mode 100644
index 0000000000..c8ed37c08c
--- /dev/null
+++ b/packages/components/ellipsis/style/css.ts
@@ -0,0 +1,3 @@
+import '@element-plus/components/base/style/css'
+import '@element-plus/components/tooltip/style/css'
+import '@element-plus/theme-chalk/el-ellipsis.css'
diff --git a/packages/components/ellipsis/style/index.ts b/packages/components/ellipsis/style/index.ts
new file mode 100644
index 0000000000..705458ba47
--- /dev/null
+++ b/packages/components/ellipsis/style/index.ts
@@ -0,0 +1,3 @@
+import '@element-plus/components/base/style'
+import '@element-plus/components/tooltip/style'
+import '@element-plus/theme-chalk/src/ellipsis.scss'
diff --git a/packages/components/index.ts b/packages/components/index.ts
index 8ce350fa3a..789ef870ae 100644
--- a/packages/components/index.ts
+++ b/packages/components/index.ts
@@ -73,6 +73,7 @@ export * from './watermark'
export * from './tour'
export * from './anchor'
export * from './segmented'
+export * from './ellipsis'
// plugins
export * from './infinite-scroll'
diff --git a/packages/element-plus/component.ts b/packages/element-plus/component.ts
index 9a27b864dc..822c707925 100644
--- a/packages/element-plus/component.ts
+++ b/packages/element-plus/component.ts
@@ -106,6 +106,7 @@ import { ElWatermark } from '@element-plus/components/watermark'
import { ElTour, ElTourStep } from '@element-plus/components/tour'
import { ElAnchor, ElAnchorLink } from '@element-plus/components/anchor'
import { ElSegmented } from '@element-plus/components/segmented'
+import { ElEllipsis } from '@element-plus/components/ellipsis'
import type { Plugin } from 'vue'
@@ -214,4 +215,5 @@ export default [
ElAnchor,
ElAnchorLink,
ElSegmented,
+ ElEllipsis,
] as Plugin[]
diff --git a/packages/theme-chalk/src/ellipsis.scss b/packages/theme-chalk/src/ellipsis.scss
new file mode 100644
index 0000000000..e0ad26fef8
--- /dev/null
+++ b/packages/theme-chalk/src/ellipsis.scss
@@ -0,0 +1,25 @@
+@use 'sass:map';
+
+@use 'mixins/mixins' as *;
+@use 'mixins/var' as *;
+@use 'common/var' as *;
+
+@include b(ellipsis) {
+ margin: 0;
+ padding: 0;
+ font-size: getCssVar('font-size', 'base');
+ color: getCssVar('text-color', 'regular');
+ word-break: break-word;
+
+ @include when(single-line) {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ @include when(multiple-line) {
+ display: -webkit-box;
+ overflow: hidden;
+ -webkit-box-orient: vertical;
+ }
+}
\ No newline at end of file
diff --git a/packages/theme-chalk/src/index.scss b/packages/theme-chalk/src/index.scss
index 42bc269804..49af1f672c 100644
--- a/packages/theme-chalk/src/index.scss
+++ b/packages/theme-chalk/src/index.scss
@@ -107,3 +107,4 @@
@use './anchor.scss';
@use './anchor-link.scss';
@use './segmented.scss';
+@use './ellipsis.scss';
diff --git a/typings/components.d.ts b/typings/components.d.ts
index d770af1c5d..09f5869aec 100644
--- a/typings/components.d.ts
+++ b/typings/components.d.ts
@@ -104,6 +104,7 @@ declare module '@vue/runtime-core' {
ElAnchor: typeof import('../packages/element-plus')['ElAnchor']
ElAnchorLink: typeof import('../packages/element-plus')['ElAnchorLink']
ElSegmented: typeof import('../packages/element-plus')['ElSegmented']
+ ElEllipsis: typeof import('../packages/element-plus')['ElEllipsis']
}
interface ComponentCustomProperties {