feat: update typography

zkwolf-typography
tanjinzhou 2021-03-15 18:06:50 +08:00
parent 2191656479
commit 5771e505c7
7 changed files with 109 additions and 107 deletions

View File

@ -155,5 +155,6 @@ export default function calculateNodeHeight(
minHeight: `${minHeight}px`,
maxHeight: `${maxHeight}px`,
overflowY,
resize: 'none',
};
}

View File

@ -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>

View File

@ -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}

View File

@ -26,7 +26,6 @@ const Text: FunctionalComponent<TextProps> = (props, { slots, attrs }) => {
component: 'span',
...attrs,
};
return <Base {...textProps} v-slots={slots}></Base>;
};

View File

@ -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';

View File

@ -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);
}

View File

@ -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: {