feat: update typography

zkwolf-typography
tangjinzhou 2021-03-14 22:50:17 +08:00
parent 2515077386
commit 2191656479
11 changed files with 248 additions and 158 deletions

View File

@ -0,0 +1,8 @@
import { computed, inject } from 'vue';
import { defaultConfigProvider } from '../../config-provider';
export default (name: string, props: Record<any, any>) => {
const configProvider = inject('configProvider', defaultConfigProvider);
const prefixCls = computed(() => configProvider.getPrefixCls(name, props.prefixCls));
return { configProvider, prefixCls };
};

View File

@ -11,29 +11,26 @@ import Typography, { TypographyProps } from './Typography';
import ResizeObserver from '../vc-resize-observer';
import Tooltip from '../tooltip';
import copy from '../_util/copy-to-clipboard';
import { defaultConfigProvider } from '../config-provider';
import CheckOutlined from '@ant-design/icons-vue/CheckOutlined';
import CopyOutlined from '@ant-design/icons-vue/CopyOutlined';
import EditOutlined from '@ant-design/icons-vue/EditOutlined';
import {
defineComponent,
inject,
VNodeTypes,
Text,
VNode,
reactive,
ref,
onMounted,
onUpdated,
onBeforeUnmount,
watch,
watchEffect,
nextTick,
CSSProperties,
toRaw,
computed,
} from 'vue';
import { filterEmpty } from '../_util/props-util';
import { AutoSizeType } from '../input/ResizableTextArea';
import useConfigInject from '../_util/hooks/useConfigInject';
export type BaseType = 'secondary' | 'success' | 'warning' | 'danger';
@ -63,7 +60,7 @@ interface EditConfig {
tooltip?: boolean | VNodeTypes;
onStart?: () => void;
onChange?: (value: string) => void;
maxLength?: number;
maxlength?: number;
autoSize?: boolean | AutoSizeType;
}
@ -74,7 +71,7 @@ export interface EllipsisConfig {
symbol?: VNodeTypes;
onExpand?: EventHandlerNonNull;
onEllipsis?: (ellipsis: boolean) => void;
tooltip?: VNodeTypes;
tooltip?: boolean | VNodeTypes;
}
export interface BlockProps extends TypographyProps {
@ -91,6 +88,7 @@ export interface BlockProps extends TypographyProps {
delete?: boolean;
strong?: boolean;
keyboard?: boolean;
content?: string;
}
interface Locale {
@ -110,7 +108,7 @@ const Base = defineComponent<InternalBlockProps>({
name: 'Base',
inheritAttrs: false,
setup(props, { slots, attrs }) {
const configProvider = inject('configProvider', defaultConfigProvider);
const { prefixCls } = useConfigInject('typography', props);
const state = reactive({
edit: false,
@ -133,25 +131,20 @@ const Base = defineComponent<InternalBlockProps>({
const contentRef = ref();
const editIcon = ref();
const ellipsis = computed(
(): EllipsisConfig => {
const ellipsis = props.ellipsis;
if (!ellipsis) return {};
return {
rows: 1,
expandable: false,
...(typeof ellipsis === 'object' ? ellipsis : null),
};
},
);
onMounted(() => {
state.prevProps = { ...props, children: getChildren() };
state.clientRendered = true;
resizeOnNextFrame();
});
onUpdated(() => {
const ellipsis = getEllipsis();
const prevEllipsis = getEllipsis(state.prevProps);
const children = getChildren();
if (
JSON.stringify(children) !== JSON.stringify(toRaw(state.prevProps.children)) ||
ellipsis.rows !== prevEllipsis.rows
) {
resizeOnNextFrame();
}
state.prevProps = { ...props, children };
});
onBeforeUnmount(() => {
@ -160,26 +153,30 @@ const Base = defineComponent<InternalBlockProps>({
});
watch(
() => props.ellipsis,
[() => ellipsis.value.rows, () => props.content],
() => {
resizeOnNextFrame();
nextTick(() => {
resizeOnNextFrame();
});
},
{ flush: 'post', deep: true, immediate: true },
);
watchEffect(() => {
const children = getChildren();
warning(
!props.editable || children.every(item => item.type === Text),
'Typography',
'When `editable` is enabled, the `children` should use string.',
);
if (!('content' in props)) {
warning(
!props.editable,
'Typography',
'When `editable` is enabled, please use `content` instead of children',
);
warning(
!props.ellipsis,
'Typography',
'When `ellipsis` is enabled, please use `content` instead of children',
);
}
});
function getChildren() {
return filterEmpty(slots.default?.());
}
function saveTypographyRef(node: VNode) {
contentRef.value = node;
}
@ -189,23 +186,14 @@ const Base = defineComponent<InternalBlockProps>({
}
function getChildrenText(): string {
const children = getChildren();
return children.length !== 0
? children
.filter(item => item.type === Text)
.map(item => item.children)
.reduce((cur, prev) => cur + prev, '')
: '';
return props.ellipsis || props.editable ? props.content : contentRef.value.text;
}
// =============== Expand ===============
function onExpandClick(e: MouseEvent) {
const { onExpand } = getEllipsis();
const { onExpand } = ellipsis.value;
state.expanded = true;
if (onExpand) {
onExpand(e);
}
onExpand?.(e);
}
// ================ Edit ================
function onEditClick() {
@ -213,10 +201,8 @@ const Base = defineComponent<InternalBlockProps>({
}
function onEditChange(value: string) {
const { onChange } = getEditable();
if (onChange) {
onChange(value);
}
const { onChange } = editable.value;
onChange?.(value);
triggerEdit(false);
}
@ -226,7 +212,8 @@ const Base = defineComponent<InternalBlockProps>({
}
// ================ Copy ================
function onCopyClick() {
function onCopyClick(e: MouseEvent) {
e.preventDefault();
const { copyable } = props;
const copyConfig = {
@ -250,30 +237,18 @@ const Base = defineComponent<InternalBlockProps>({
}, 3000);
});
}
function getEditable($props?: BlockProps): EditConfig {
const editable = ($props || props).editable;
const editable = computed(() => {
const editable = props.editable;
if (!editable) return { editing: state.edit };
return {
editing: state.edit,
...(typeof editable === 'object' ? editable : null),
};
}
function getEllipsis($props?: BlockProps): EllipsisConfig {
const ellipsis = ($props || props).ellipsis;
if (!ellipsis) return {};
return {
rows: 1,
expandable: false,
...(typeof ellipsis === 'object' ? ellipsis : null),
};
}
});
function triggerEdit(edit: boolean) {
const { onStart } = getEditable();
const { onStart } = editable.value;
if (edit && onStart) {
onStart();
}
@ -295,13 +270,13 @@ const Base = defineComponent<InternalBlockProps>({
});
}
function canUseCSSEllipsis(): boolean {
const { rows, expandable, suffix } = getEllipsis();
const canUseCSSEllipsis = computed(() => {
const { rows, expandable, suffix, onEllipsis, tooltip } = ellipsis.value;
if (suffix) return false;
if (suffix || tooltip) return false;
// Can't use css ellipsis since we need to provide the place for button
if (props.editable || props.copyable || expandable || !state.clientRendered) {
if (props.editable || props.copyable || expandable || onEllipsis) {
return false;
}
@ -310,40 +285,33 @@ const Base = defineComponent<InternalBlockProps>({
}
return isLineClampSupport;
}
});
function syncEllipsis() {
const syncEllipsis = () => {
const { ellipsisText, isEllipsis } = state;
const children = getChildren();
const { rows, suffix, onEllipsis } = getEllipsis();
const { rows, suffix, onEllipsis } = ellipsis.value;
if (!rows || rows < 0 || !contentRef.value?.$el || state.expanded) return;
// Do not measure if css already support ellipsis
if (canUseCSSEllipsis()) return;
if (canUseCSSEllipsis.value) return;
warning(
children.every(item => item.type === Text),
'Typography',
'`ellipsis` should use string as children only.',
);
const { content, text, ellipsis } = measure(
const { content, text, ellipsis: ell } = measure(
contentRef.value?.$el,
{ rows, suffix },
children,
props.content,
renderOperations(true),
ELLIPSIS_STR,
);
if (ellipsisText !== text || state.isEllipsis !== ellipsis) {
if (ellipsisText !== text || state.isEllipsis !== ell) {
state.ellipsisText = text;
state.ellipsisContent = content;
state.isEllipsis = ellipsis;
if (isEllipsis !== ellipsis && onEllipsis) {
onEllipsis(ellipsis);
state.isEllipsis = ell;
if (isEllipsis !== ell && onEllipsis) {
onEllipsis(ell);
}
}
}
};
function wrapperDecorations(
{ mark, code, underline, delete: del, strong, keyboard }: BlockProps,
@ -368,8 +336,7 @@ const Base = defineComponent<InternalBlockProps>({
}
function renderExpand(forceRender?: boolean) {
const { expandable, symbol } = getEllipsis();
const prefixCls = getPrefixCls();
const { expandable, symbol } = ellipsis.value;
if (!expandable) return null;
@ -386,7 +353,7 @@ const Base = defineComponent<InternalBlockProps>({
return (
<a
key="expand"
class={`${prefixCls}-expand`}
class={`${prefixCls.value}-expand`}
onClick={onExpandClick}
aria-label={state.expandStr}
>
@ -398,7 +365,6 @@ const Base = defineComponent<InternalBlockProps>({
function renderEdit() {
if (!props.editable) return;
const prefixCls = getPrefixCls();
const { icon, tooltip } = props.editable as EditConfig;
const title = tooltip || state.editStr;
@ -408,7 +374,7 @@ const Base = defineComponent<InternalBlockProps>({
<Tooltip key="edit" title={tooltip === false ? '' : title}>
<TransButton
ref={saveEditIconRef}
class={`${prefixCls}-edit`}
class={`${prefixCls.value}-edit`}
onClick={onEditClick}
aria-label={ariaLabel}
>
@ -421,8 +387,6 @@ const Base = defineComponent<InternalBlockProps>({
function renderCopy() {
if (!props.copyable) return;
const prefixCls = getPrefixCls();
const { tooltips } = props.copyable as CopyConfig;
let tooltipNodes = toArray(tooltips) as VNodeTypes[];
if (tooltipNodes.length === 0) {
@ -435,7 +399,10 @@ const Base = defineComponent<InternalBlockProps>({
return (
<Tooltip key="copy" title={tooltips === false ? '' : title}>
<TransButton
class={[`${prefixCls}-copy`, { [`${prefixCls}-copy-success`]: state.copied }]}
class={[
`${prefixCls.value}-copy`,
{ [`${prefixCls.value}-copy-success`]: state.copied },
]}
onClick={onCopyClick}
aria-label={ariaLabel}
>
@ -446,19 +413,16 @@ const Base = defineComponent<InternalBlockProps>({
}
function renderEditInput() {
const prefixCls = getPrefixCls();
const { class: className, style } = attrs;
const { maxLength, autoSize } = getEditable();
const value = getChildrenText();
const { maxlength, autoSize } = editable.value;
return (
<Editable
class={className}
style={style}
prefixCls={prefixCls}
value={value}
maxlength={maxLength}
prefixCls={prefixCls.value}
value={props.content}
maxlength={maxlength}
autoSize={autoSize}
onSave={onEditChange}
onCancel={onEditCancel}
@ -470,14 +434,14 @@ const Base = defineComponent<InternalBlockProps>({
return [renderExpand(forceRenderExpanded), renderEdit(), renderCopy()].filter(node => node);
}
function getPrefixCls() {
const getPrefixCls = configProvider.getPrefixCls;
return getPrefixCls('typography', props.prefixCls);
}
return () => {
const { editing } = getEditable();
const children = filterEmpty(slots.default?.());
const { editing } = editable.value;
const children =
props.ellipsis || props.editable
? props.content
: slots.default
? slots.default()
: props.content;
if (editing) {
return renderEditInput();
@ -486,9 +450,11 @@ const Base = defineComponent<InternalBlockProps>({
<LocaleReceiver
componentName="Text"
children={(locale: Locale) => {
const { type, disabled, title, ...restProps } = props;
const { class: className, style } = attrs;
const { rows, suffix } = getEllipsis();
const { type, disabled, title, content, class: className, style, ...restProps } = {
...props,
...attrs,
};
const { rows, suffix, tooltip } = ellipsis.value;
const { edit, copy: copyStr, copied, expand } = locale;
@ -510,7 +476,7 @@ const Base = defineComponent<InternalBlockProps>({
'keyboard',
]);
const cssEllipsis = canUseCSSEllipsis();
const cssEllipsis = canUseCSSEllipsis.value;
const cssTextOverflow = rows === 1 && cssEllipsis;
const cssLineClamp = rows && rows > 1 && cssEllipsis;
@ -520,10 +486,8 @@ const Base = defineComponent<InternalBlockProps>({
// Only use js ellipsis when css ellipsis not support
if (rows && state.isEllipsis && !state.expanded && !cssEllipsis) {
ariaLabel = title;
if (!title && children.every(item => item.type === Text)) {
ariaLabel = children
.map(item => item.children)
.reduce((cur, prev) => cur + prev, '');
if (!title) {
ariaLabel = content;
}
// We move full content to outer element to avoid repeat read the content by accessibility
textNode = (
@ -533,6 +497,14 @@ const Base = defineComponent<InternalBlockProps>({
{suffix}
</span>
);
// 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 = (
<>
@ -544,23 +516,21 @@ const Base = defineComponent<InternalBlockProps>({
textNode = wrapperDecorations(props, textNode);
const prefixCls = getPrefixCls();
return (
<ResizeObserver onResize={resizeOnNextFrame} disabled={!rows}>
<Typography
ref={saveTypographyRef}
class={[
{ [`${prefixCls}-${type}`]: type },
{ [`${prefixCls}-disabled`]: disabled },
{ [`${prefixCls}-ellipsis`]: rows },
{ [`${prefixCls}-ellipsis-single-line`]: cssTextOverflow },
{ [`${prefixCls}-ellipsis-multiple-line`]: cssLineClamp },
{ [`${prefixCls.value}-${type}`]: type },
{ [`${prefixCls.value}-disabled`]: disabled },
{ [`${prefixCls.value}-ellipsis`]: rows },
{ [`${prefixCls.value}-ellipsis-single-line`]: cssTextOverflow },
{ [`${prefixCls.value}-ellipsis-multiple-line`]: cssLineClamp },
className,
]}
style={{
...(style as CSSProperties),
WebkitLineClamp: cssLineClamp ? rows : null,
WebkitLineClamp: cssLineClamp ? rows : undefined,
}}
aria-label={ariaLabel}
{...textProps}
@ -582,7 +552,7 @@ export const baseProps = () => ({
copyable: PropTypes.oneOfType([PropTypes.looseBool, PropTypes.object]),
prefixCls: PropTypes.string,
component: PropTypes.string,
type: PropTypes.oneOf(['secondary', 'danger', 'warning']),
type: PropTypes.oneOf(['secondary', 'success', 'danger', 'warning']),
disabled: PropTypes.looseBool,
ellipsis: PropTypes.oneOfType([PropTypes.looseBool, PropTypes.object]),
code: PropTypes.looseBool,
@ -591,6 +561,7 @@ export const baseProps = () => ({
delete: PropTypes.looseBool,
strong: PropTypes.looseBool,
keyboard: PropTypes.looseBool,
content: PropTypes.string,
});
Base.props = baseProps();

View File

@ -25,10 +25,10 @@ const Link: FunctionalComponent<LinkProps> = (props, { slots, attrs }) => {
// @ts-ignore
delete mergedProps.navigate;
return <Base {...mergedProps}>{slots.default?.()}</Base>;
return <Base {...mergedProps} v-slots={slots}></Base>;
};
Link.displayName = 'ATypographyText';
Link.displayName = 'ATypographyLink';
Link.inheritAttrs = false;
Link.props = Omit({ ...baseProps(), ellipsis: PropTypes.looseBool }, ['component']);

View File

@ -9,7 +9,7 @@ const Paragraph: FunctionalComponent<BlockProps> = (props, { slots, attrs }) =>
...attrs,
};
return <Base {...paragraphProps}>{slots.default?.()}</Base>;
return <Base {...paragraphProps} v-slots={slots}></Base>;
};
Paragraph.displayName = 'ATypographyParagraph';

View File

@ -27,7 +27,7 @@ const Text: FunctionalComponent<TextProps> = (props, { slots, attrs }) => {
...attrs,
};
return <Base {...textProps}>{slots.default?.()}</Base>;
return <Base {...textProps} v-slots={slots}></Base>;
};
Text.displayName = 'ATypographyText';

View File

@ -25,7 +25,7 @@ const Title: FunctionalComponent<TitleProps> = (props, { slots, attrs }) => {
...attrs,
};
return <Base {...titleProps}>{slots.default?.()}</Base>;
return <Base {...titleProps} v-slots={slots}></Base>;
};
Title.displayName = 'ATypographyTitle';

View File

@ -1,9 +1,11 @@
import { defaultConfigProvider } from '../config-provider';
import Text from './Text';
import Title from './Title';
import Paragraph from './Paragraph';
import PropTypes from '../_util/vue-types';
import { defineComponent, HTMLAttributes, inject, App, Plugin } from 'vue';
import { defineComponent, HTMLAttributes, App, Plugin, computed } from 'vue';
import useConfigInject from '../_util/hooks/useConfigInject';
import Link from './Link';
import classNames from '../_util/classNames';
export interface TypographyProps extends HTMLAttributes {
prefixCls?: string;
@ -18,14 +20,22 @@ const Typography = defineComponent<InternalTypographyProps>({
Text: Text,
Title: Title,
Paragraph: Paragraph,
setup(props, { slots }) {
const { getPrefixCls } = inject('configProvider', defaultConfigProvider);
Link: Link,
inheritAttrs: false,
setup(props, { slots, attrs }) {
const { prefixCls } = useConfigInject('typography', props);
return () => {
const { prefixCls: customizePrefixCls, component: Component = 'article' as any } = props;
const prefixCls = getPrefixCls('typography', customizePrefixCls);
return <Component class={prefixCls}>{slots.default?.()}</Component>;
const {
prefixCls: _prefixCls,
class: _className,
component: Component = 'article' as any,
...restProps
} = { ...props, ...attrs };
return (
<Component class={classNames(prefixCls.value, attrs.class)} {...restProps}>
{slots.default?.()}
</Component>
);
};
},
});
@ -40,6 +50,7 @@ Typography.install = function(app: App) {
app.component(Typography.Text.displayName, Text);
app.component(Typography.Title.displayName, Title);
app.component(Typography.Paragraph.displayName, Paragraph);
app.component(Typography.Link.displayName, Link);
return app;
};
@ -48,4 +59,5 @@ export default Typography as typeof Typography &
readonly Text: typeof Text;
readonly Title: typeof Title;
readonly Paragraph: typeof Paragraph;
readonly Link: typeof Link;
};

View File

@ -22,6 +22,11 @@
&&-danger {
color: @error-color;
a&:active,
a&:focus,
a&:hover {
color: ~`colorPalette('@{error-color}', 5) `;
}
}
&&-disabled {
@ -60,7 +65,8 @@
h1&,
h2&,
h3&,
h4& h5& {
h4&,
h5& {
.@{typography-prefix-cls} + & {
margin-top: @typography-title-margin-top;
}
@ -92,16 +98,26 @@
a&,
a {
.operation-unit();
text-decoration: @link-decoration;
&:active,
&:hover {
text-decoration: @link-hover-decoration;
}
&[disabled] {
&[disabled],
&.@{typography-prefix-cls}-disabled {
color: @disabled-color;
cursor: not-allowed;
pointer-events: none;
&:active,
&:hover {
color: @disabled-color;
}
&:active {
pointer-events: none;
}
}
}
@ -109,8 +125,8 @@
margin: 0 0.2em;
padding: 0.2em 0.4em 0.1em;
font-size: 85%;
background: rgba(0, 0, 0, 0.06);
border: 1px solid rgba(0, 0, 0, 0.06);
background: rgba(150, 150, 150, 0.1);
border: 1px solid rgba(100, 100, 100, 0.2);
border-radius: 3px;
}
@ -178,11 +194,6 @@
bottom: 8px;
color: @text-color-secondary;
pointer-events: none;
.@{typography-prefix-cls}-rtl & {
right: auto;
left: 10px;
}
}
// Fix Editable Textarea flash in Firefox
@ -215,6 +226,38 @@
list-style-type: decimal;
}
// pre & block
pre,
blockquote {
margin: 1em 0;
}
pre {
padding: 0.4em 0.6em;
white-space: pre-wrap;
word-wrap: break-word;
background: rgba(150, 150, 150, 0.1);
border: 1px solid rgba(100, 100, 100, 0.2);
border-radius: 3px;
// Compatible for marked
code {
display: inline;
margin: 0;
padding: 0;
font-size: inherit;
font-family: inherit;
background: transparent;
border: 0;
}
}
blockquote {
padding: 0 0 0 0.6em;
border-left: 4px solid rgba(100, 100, 100, 0.2);
opacity: 0.85;
}
// ============ Ellipsis ============
&-ellipsis-single-line {
overflow: hidden;
@ -230,9 +273,11 @@
&-ellipsis-multiple-line {
display: -webkit-box;
overflow: hidden;
-webkit-line-clamp: 3;
/*! autoprefixer: ignore next */
-webkit-box-orient: vertical;
overflow: hidden;
}
}
@import './rtl';

View File

@ -0,0 +1,54 @@
@import '../../style/themes/index';
@import '../../style/mixins/index';
@typography-prefix-cls: ~'@{ant-prefix}-typography';
.@{typography-prefix-cls} {
&-rtl {
direction: rtl;
}
// Operation
&-expand,
&-edit,
&-copy {
.@{typography-prefix-cls}-rtl & {
margin-right: 4px;
margin-left: 0;
}
}
&-expand {
.@{typography-prefix-cls}-rtl & {
float: left;
}
}
// Text input area
&-edit-content {
div& {
&.@{typography-prefix-cls}-rtl {
right: -@input-padding-horizontal - 1px;
left: auto;
}
}
&-confirm {
.@{typography-prefix-cls}-rtl & {
right: auto;
left: 10px;
}
}
}
// list
ul,
ol {
li {
.@{typography-prefix-cls}-rtl& {
margin: 0 20px 0 0;
padding: 0 4px 0 0;
}
}
}
}

View File

@ -5,7 +5,7 @@
</template>
<script>
import { defineComponent } from 'vue';
import demo from '../v2-doc/src/docs/affix/demo/basic.vue';
import demo from '../v2-doc/src/docs/typography/demo/text.vue';
// import Affix from '../components/affix';
export default defineComponent({
components: {

2
v2-doc

@ -1 +1 @@
Subproject commit 0468ad3010f71ad6b267c66c4f5e28c89c19d83e
Subproject commit a295627e3b35302ee9da1215fb102a42210e3ba3