feat: update typography
parent
2191656479
commit
5771e505c7
|
@ -155,5 +155,6 @@ export default function calculateNodeHeight(
|
|||
minHeight: `${minHeight}px`,
|
||||
maxHeight: `${maxHeight}px`,
|
||||
overflowY,
|
||||
resize: 'none',
|
||||
};
|
||||
}
|
||||
|
|
|
@ -26,8 +26,8 @@ import {
|
|||
watchEffect,
|
||||
nextTick,
|
||||
CSSProperties,
|
||||
toRaw,
|
||||
computed,
|
||||
toRaw,
|
||||
} from 'vue';
|
||||
import { AutoSizeType } from '../input/ResizableTextArea';
|
||||
import useConfigInject from '../_util/hooks/useConfigInject';
|
||||
|
@ -37,29 +37,19 @@ export type BaseType = 'secondary' | 'success' | 'warning' | 'danger';
|
|||
const isLineClampSupport = isStyleSupport('webkitLineClamp');
|
||||
const isTextOverflowSupport = isStyleSupport('textOverflow');
|
||||
|
||||
function toArray(value: any) {
|
||||
let ret = value;
|
||||
if (value === undefined) {
|
||||
ret = [];
|
||||
} else if (!Array.isArray(value)) {
|
||||
ret = [value];
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
interface CopyConfig {
|
||||
text?: string;
|
||||
onCopy?: () => void;
|
||||
icon?: VNodeTypes;
|
||||
tooltips?: boolean | VNodeTypes;
|
||||
tooltip?: boolean;
|
||||
}
|
||||
|
||||
interface EditConfig {
|
||||
editing?: boolean;
|
||||
icon?: VNodeTypes;
|
||||
tooltip?: boolean | VNodeTypes;
|
||||
tooltip?: boolean;
|
||||
onStart?: () => void;
|
||||
onChange?: (value: string) => void;
|
||||
onCancel?: () => void;
|
||||
onEnd?: () => void;
|
||||
maxlength?: number;
|
||||
autoSize?: boolean | AutoSizeType;
|
||||
}
|
||||
|
@ -71,7 +61,7 @@ export interface EllipsisConfig {
|
|||
symbol?: VNodeTypes;
|
||||
onExpand?: EventHandlerNonNull;
|
||||
onEllipsis?: (ellipsis: boolean) => void;
|
||||
tooltip?: boolean | VNodeTypes;
|
||||
tooltip?: boolean;
|
||||
}
|
||||
|
||||
export interface BlockProps extends TypographyProps {
|
||||
|
@ -107,7 +97,8 @@ const ELLIPSIS_STR = '...';
|
|||
const Base = defineComponent<InternalBlockProps>({
|
||||
name: 'Base',
|
||||
inheritAttrs: false,
|
||||
setup(props, { slots, attrs }) {
|
||||
emits: ['update:content'],
|
||||
setup(props, { slots, attrs, emit }) {
|
||||
const { prefixCls } = useConfigInject('typography', props);
|
||||
|
||||
const state = reactive({
|
||||
|
@ -127,6 +118,8 @@ const Base = defineComponent<InternalBlockProps>({
|
|||
copyId: undefined,
|
||||
rafId: undefined,
|
||||
prevProps: undefined,
|
||||
|
||||
originContent: '',
|
||||
});
|
||||
|
||||
const contentRef = ref();
|
||||
|
@ -186,7 +179,7 @@ const Base = defineComponent<InternalBlockProps>({
|
|||
}
|
||||
|
||||
function getChildrenText(): string {
|
||||
return props.ellipsis || props.editable ? props.content : contentRef.value.text;
|
||||
return props.ellipsis || props.editable ? props.content : contentRef.value?.$el?.innerText;
|
||||
}
|
||||
|
||||
// =============== Expand ===============
|
||||
|
@ -197,15 +190,21 @@ const Base = defineComponent<InternalBlockProps>({
|
|||
}
|
||||
// ================ Edit ================
|
||||
function onEditClick() {
|
||||
state.originContent = props.content;
|
||||
triggerEdit(true);
|
||||
}
|
||||
|
||||
function onEditChange(value: string) {
|
||||
const { onChange } = editable.value;
|
||||
onChange?.(value);
|
||||
|
||||
onContentChange(value);
|
||||
triggerEdit(false);
|
||||
}
|
||||
function onContentChange(value: string) {
|
||||
const { onChange } = editable.value;
|
||||
if (value !== props.content) {
|
||||
onChange?.(value);
|
||||
emit('update:content', value);
|
||||
}
|
||||
}
|
||||
|
||||
function onEditCancel() {
|
||||
triggerEdit(false);
|
||||
|
@ -290,7 +289,14 @@ const Base = defineComponent<InternalBlockProps>({
|
|||
const syncEllipsis = () => {
|
||||
const { ellipsisText, isEllipsis } = state;
|
||||
const { rows, suffix, onEllipsis } = ellipsis.value;
|
||||
if (!rows || rows < 0 || !contentRef.value?.$el || state.expanded) return;
|
||||
if (
|
||||
!rows ||
|
||||
rows < 0 ||
|
||||
!contentRef.value?.$el ||
|
||||
state.expanded ||
|
||||
props.content === undefined
|
||||
)
|
||||
return;
|
||||
|
||||
// Do not measure if css already support ellipsis
|
||||
if (canUseCSSEllipsis.value) return;
|
||||
|
@ -365,9 +371,9 @@ const Base = defineComponent<InternalBlockProps>({
|
|||
function renderEdit() {
|
||||
if (!props.editable) return;
|
||||
|
||||
const { icon, tooltip } = props.editable as EditConfig;
|
||||
|
||||
const title = tooltip || state.editStr;
|
||||
const { tooltip } = props.editable as EditConfig;
|
||||
const icon = slots.editableIcon ? slots.editableIcon() : <EditOutlined role="button" />;
|
||||
const title = slots.editableTooltip ? slots.editableTooltip() : state.editStr;
|
||||
const ariaLabel = typeof title === 'string' ? title : '';
|
||||
|
||||
return (
|
||||
|
@ -378,7 +384,7 @@ const Base = defineComponent<InternalBlockProps>({
|
|||
onClick={onEditClick}
|
||||
aria-label={ariaLabel}
|
||||
>
|
||||
{icon || <EditOutlined role="button" />}
|
||||
{icon}
|
||||
</TransButton>
|
||||
</Tooltip>
|
||||
);
|
||||
|
@ -387,17 +393,19 @@ const Base = defineComponent<InternalBlockProps>({
|
|||
function renderCopy() {
|
||||
if (!props.copyable) return;
|
||||
|
||||
const { tooltips } = props.copyable as CopyConfig;
|
||||
let tooltipNodes = toArray(tooltips) as VNodeTypes[];
|
||||
if (tooltipNodes.length === 0) {
|
||||
tooltipNodes = [state.copyStr, state.copiedStr];
|
||||
}
|
||||
const title = state.copied ? tooltipNodes[1] : tooltipNodes[0];
|
||||
const { tooltip } = props.copyable as CopyConfig;
|
||||
const defaultTitle = state.copied ? state.copiedStr : state.copyStr;
|
||||
const title = slots.copyableTooltip
|
||||
? slots.copyableTooltip({ copied: state.copied })
|
||||
: defaultTitle;
|
||||
const ariaLabel = typeof title === 'string' ? title : '';
|
||||
const icons = toArray((props.copyable as CopyConfig).icon);
|
||||
const defaultIcon = state.copied ? <CheckOutlined /> : <CopyOutlined />;
|
||||
const icon = slots.copyableIcon
|
||||
? slots.copyableIcon({ copied: !!state.copied })
|
||||
: defaultIcon;
|
||||
|
||||
return (
|
||||
<Tooltip key="copy" title={tooltips === false ? '' : title}>
|
||||
<Tooltip key="copy" title={tooltip === false ? '' : title}>
|
||||
<TransButton
|
||||
class={[
|
||||
`${prefixCls.value}-copy`,
|
||||
|
@ -406,7 +414,7 @@ const Base = defineComponent<InternalBlockProps>({
|
|||
onClick={onCopyClick}
|
||||
aria-label={ariaLabel}
|
||||
>
|
||||
{state.copied ? icons[1] || <CheckOutlined /> : icons[0] || <CopyOutlined />}
|
||||
{icon}
|
||||
</TransButton>
|
||||
</Tooltip>
|
||||
);
|
||||
|
@ -422,9 +430,11 @@ const Base = defineComponent<InternalBlockProps>({
|
|||
style={style}
|
||||
prefixCls={prefixCls.value}
|
||||
value={props.content}
|
||||
originContent={state.originContent}
|
||||
maxlength={maxlength}
|
||||
autoSize={autoSize}
|
||||
onSave={onEditChange}
|
||||
onChange={onContentChange}
|
||||
onCancel={onEditCancel}
|
||||
/>
|
||||
);
|
||||
|
@ -438,7 +448,9 @@ const Base = defineComponent<InternalBlockProps>({
|
|||
const { editing } = editable.value;
|
||||
const children =
|
||||
props.ellipsis || props.editable
|
||||
? 'content' in props
|
||||
? props.content
|
||||
: slots.default?.()
|
||||
: slots.default
|
||||
? slots.default()
|
||||
: props.content;
|
||||
|
@ -450,7 +462,7 @@ const Base = defineComponent<InternalBlockProps>({
|
|||
<LocaleReceiver
|
||||
componentName="Text"
|
||||
children={(locale: Locale) => {
|
||||
const { type, disabled, title, content, class: className, style, ...restProps } = {
|
||||
const { type, disabled, content, class: className, style, ...restProps } = {
|
||||
...props,
|
||||
...attrs,
|
||||
};
|
||||
|
@ -485,26 +497,25 @@ const Base = defineComponent<InternalBlockProps>({
|
|||
|
||||
// Only use js ellipsis when css ellipsis not support
|
||||
if (rows && state.isEllipsis && !state.expanded && !cssEllipsis) {
|
||||
ariaLabel = title;
|
||||
if (!title) {
|
||||
ariaLabel = content;
|
||||
const { title } = restProps;
|
||||
let restContent = title || '';
|
||||
|
||||
if (!title && (typeof children === 'string' || typeof children === 'number')) {
|
||||
restContent = String(children);
|
||||
}
|
||||
|
||||
// show rest content as title on symbol
|
||||
restContent = restContent?.slice(String(state.ellipsisContent || '').length);
|
||||
// We move full content to outer element to avoid repeat read the content by accessibility
|
||||
textNode = (
|
||||
<span title={ariaLabel} aria-hidden="true">
|
||||
<>
|
||||
{toRaw(state.ellipsisContent)}
|
||||
<span title={restContent} aria-hidden="true">
|
||||
{ELLIPSIS_STR}
|
||||
{suffix}
|
||||
</span>
|
||||
{suffix}
|
||||
</>
|
||||
);
|
||||
// If provided tooltip, we need wrap with span to let Tooltip inject events
|
||||
if (tooltip) {
|
||||
textNode = (
|
||||
<Tooltip title={tooltip === true ? children : tooltip}>
|
||||
<span>{textNode}</span>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
} else {
|
||||
textNode = (
|
||||
<>
|
||||
|
@ -516,6 +527,9 @@ const Base = defineComponent<InternalBlockProps>({
|
|||
|
||||
textNode = wrapperDecorations(props, textNode);
|
||||
|
||||
const showTooltip =
|
||||
tooltip && rows && state.isEllipsis && !state.expanded && !cssEllipsis;
|
||||
const title = slots.ellipsisTooltip ? slots.ellipsisTooltip() : tooltip;
|
||||
return (
|
||||
<ResizeObserver onResize={resizeOnNextFrame} disabled={!rows}>
|
||||
<Typography
|
||||
|
@ -535,7 +549,13 @@ const Base = defineComponent<InternalBlockProps>({
|
|||
aria-label={ariaLabel}
|
||||
{...textProps}
|
||||
>
|
||||
{textNode}
|
||||
{showTooltip ? (
|
||||
<Tooltip title={tooltip === true ? children : title}>
|
||||
<span>{textNode}</span>
|
||||
</Tooltip>
|
||||
) : (
|
||||
textNode
|
||||
)}
|
||||
{renderOperations()}
|
||||
</Typography>
|
||||
</ResizeObserver>
|
||||
|
|
|
@ -12,8 +12,11 @@ const Editable = defineComponent({
|
|||
autoSize: PropTypes.oneOfType([PropTypes.looseBool, PropTypes.object]),
|
||||
onSave: PropTypes.func,
|
||||
onCancel: PropTypes.func,
|
||||
onEnd: PropTypes.func,
|
||||
onChange: PropTypes.func,
|
||||
originContent: PropTypes.string,
|
||||
},
|
||||
emits: ['save', 'cancel'],
|
||||
emits: ['save', 'cancel', 'end', 'change'],
|
||||
setup(props, { emit }) {
|
||||
const state = reactive({
|
||||
current: props.value || '',
|
||||
|
@ -21,9 +24,6 @@ const Editable = defineComponent({
|
|||
inComposition: false,
|
||||
cancelFlag: false,
|
||||
});
|
||||
|
||||
let cancelFlag: boolean = false;
|
||||
|
||||
watch(
|
||||
() => props.value,
|
||||
current => {
|
||||
|
@ -49,6 +49,7 @@ const Editable = defineComponent({
|
|||
|
||||
function onChange({ target: { value } }) {
|
||||
state.current = value.replace(/[\r\n]/g, '');
|
||||
emit('change', state.current);
|
||||
}
|
||||
|
||||
function onCompositionStart() {
|
||||
|
@ -61,6 +62,9 @@ const Editable = defineComponent({
|
|||
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
const { keyCode } = e;
|
||||
if (keyCode === KeyCode.ENTER) {
|
||||
e.preventDefault();
|
||||
}
|
||||
// We don't record keyCode when IME is using
|
||||
if (state.inComposition) return;
|
||||
|
||||
|
@ -81,26 +85,24 @@ const Editable = defineComponent({
|
|||
) {
|
||||
if (keyCode === KeyCode.ENTER) {
|
||||
confirmChange();
|
||||
emit('end');
|
||||
} else if (keyCode === KeyCode.ESC) {
|
||||
// avoid chrome trigger blur
|
||||
cancelFlag = true;
|
||||
state.current = props.originContent;
|
||||
emit('cancel');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onBlur() {
|
||||
if (!cancelFlag) {
|
||||
confirmChange();
|
||||
}
|
||||
}
|
||||
|
||||
function confirmChange() {
|
||||
emit('save', state.current.trim());
|
||||
}
|
||||
|
||||
return () => (
|
||||
<div class={[`${props.prefixCls}, ${props.prefixCls}-edit-content`]}>
|
||||
<div class={`${props.prefixCls} ${props.prefixCls}-edit-content`}>
|
||||
<TextArea
|
||||
ref={saveTextAreaRef}
|
||||
maxlength={props.maxlength}
|
||||
|
|
|
@ -26,7 +26,6 @@ const Text: FunctionalComponent<TextProps> = (props, { slots, attrs }) => {
|
|||
component: 'span',
|
||||
...attrs,
|
||||
};
|
||||
|
||||
return <Base {...textProps} v-slots={slots}></Base>;
|
||||
};
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { mount } from '@vue/test-utils';
|
||||
import { asyncExpect, sleep } from '@/tests/utils';
|
||||
import KeyCode from '../../_util/KeyCode';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import copy from '../_util/copy-to-clipboard';
|
||||
import Title from '../Title';
|
||||
import AParagraph from '../Paragraph';
|
||||
import Base from '../Base';
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { createApp, CSSProperties, VNodeTypes } from 'vue';
|
||||
import toArray from '../vc-util/Children/toArray';
|
||||
|
||||
interface MeasureResult {
|
||||
finished: boolean;
|
||||
|
@ -11,7 +10,6 @@ interface Option {
|
|||
}
|
||||
|
||||
// We only handle element & text node.
|
||||
const ELEMENT_NODE = 1;
|
||||
const TEXT_NODE = 3;
|
||||
const COMMENT_NODE = 8;
|
||||
|
||||
|
@ -39,25 +37,10 @@ function styleToString(style: CSSStyleDeclaration) {
|
|||
return styleNames.map(name => `${name}: ${style.getPropertyValue(name)};`).join('');
|
||||
}
|
||||
|
||||
function mergeChildren(children: VNodeTypes[]) {
|
||||
const childList = [];
|
||||
|
||||
children.forEach(child => {
|
||||
const prevChild = childList[childList.length - 1];
|
||||
if (typeof child === 'string' && typeof prevChild === 'string') {
|
||||
childList[childList.length - 1] += child;
|
||||
} else {
|
||||
childList.push(child);
|
||||
}
|
||||
});
|
||||
|
||||
return childList;
|
||||
}
|
||||
|
||||
export default (
|
||||
originEle: HTMLElement,
|
||||
option: Option,
|
||||
content: VNodeTypes,
|
||||
content: string,
|
||||
fixedContent: VNodeTypes[],
|
||||
ellipsisStr: string,
|
||||
): {
|
||||
|
@ -98,13 +81,12 @@ export default (
|
|||
ellipsisContainer.style.webkitLineClamp = 'none';
|
||||
|
||||
// Render in the fake container
|
||||
const contentList: VNodeTypes[] = mergeChildren(toArray(content as []));
|
||||
const vm = createApp({
|
||||
render() {
|
||||
return (
|
||||
<div style={wrapperStyle}>
|
||||
<span style={wrapperStyle}>
|
||||
{contentList}
|
||||
{content}
|
||||
{suffix}
|
||||
</span>
|
||||
<span style={wrapperStyle}>{fixedContent}</span>
|
||||
|
@ -125,8 +107,6 @@ export default (
|
|||
vm.unmount();
|
||||
return { content, text: ellipsisContainer.innerHTML, ellipsis: false };
|
||||
}
|
||||
|
||||
// We should clone the childNode since they're controlled by React and we can't reuse it without warning
|
||||
const childNodes = Array.prototype.slice
|
||||
.apply(ellipsisContainer.childNodes[0].childNodes[0].cloneNode(true).childNodes)
|
||||
.filter(({ nodeType, data }) => nodeType !== COMMENT_NODE && data !== '');
|
||||
|
@ -193,26 +173,26 @@ export default (
|
|||
return measureText(textNode, fullText, startLoc, midLoc, lastSuccessLoc);
|
||||
}
|
||||
|
||||
function measureNode(childNode: ChildNode, index: number): MeasureResult {
|
||||
function measureNode(childNode: ChildNode): MeasureResult {
|
||||
const type = childNode.nodeType;
|
||||
// console.log('type', type);
|
||||
// if (type === ELEMENT_NODE) {
|
||||
// // We don't split element, it will keep if whole element can be displayed.
|
||||
// appendChildNode(childNode);
|
||||
// if (inRange()) {
|
||||
// return {
|
||||
// finished: false,
|
||||
// vNode: contentList[index],
|
||||
// };
|
||||
// }
|
||||
|
||||
if (type === ELEMENT_NODE) {
|
||||
// We don't split element, it will keep if whole element can be displayed.
|
||||
appendChildNode(childNode);
|
||||
if (inRange()) {
|
||||
return {
|
||||
finished: false,
|
||||
vNode: contentList[index],
|
||||
};
|
||||
}
|
||||
|
||||
// Clean up if can not pull in
|
||||
ellipsisContentHolder.removeChild(childNode);
|
||||
return {
|
||||
finished: true,
|
||||
vNode: null,
|
||||
};
|
||||
}
|
||||
// // Clean up if can not pull in
|
||||
// ellipsisContentHolder.removeChild(childNode);
|
||||
// return {
|
||||
// finished: true,
|
||||
// vNode: null,
|
||||
// };
|
||||
// }
|
||||
if (type === TEXT_NODE) {
|
||||
const fullText = childNode.textContent || '';
|
||||
const textNode = document.createTextNode(fullText);
|
||||
|
@ -227,8 +207,8 @@ export default (
|
|||
};
|
||||
}
|
||||
|
||||
childNodes.some((childNode, index) => {
|
||||
const { finished, vNode } = measureNode(childNode, index);
|
||||
childNodes.some(childNode => {
|
||||
const { finished, vNode } = measureNode(childNode);
|
||||
if (vNode) {
|
||||
ellipsisChildren.push(vNode);
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
</template>
|
||||
<script>
|
||||
import { defineComponent } from 'vue';
|
||||
import demo from '../v2-doc/src/docs/typography/demo/text.vue';
|
||||
import demo from '../v2-doc/src/docs/typography/demo/ellipsis.vue';
|
||||
// import Affix from '../components/affix';
|
||||
export default defineComponent({
|
||||
components: {
|
||||
|
|
Loading…
Reference in New Issue