From 2191656479f792c274ff920960a8009a43d81f62 Mon Sep 17 00:00:00 2001 From: tangjinzhou <415800467@qq.com> Date: Sun, 14 Mar 2021 22:50:17 +0800 Subject: [PATCH] feat: update typography --- components/_util/hooks/useConfigInject.ts | 8 + components/typography/Base.tsx | 233 ++++++++++------------ components/typography/Link.tsx | 4 +- components/typography/Paragraph.tsx | 2 +- components/typography/Text.tsx | 2 +- components/typography/Title.tsx | 2 +- components/typography/Typography.tsx | 30 ++- components/typography/style/index.less | 67 ++++++- components/typography/style/rtl.less | 54 +++++ examples/App.vue | 2 +- v2-doc | 2 +- 11 files changed, 248 insertions(+), 158 deletions(-) create mode 100644 components/_util/hooks/useConfigInject.ts create mode 100644 components/typography/style/rtl.less diff --git a/components/_util/hooks/useConfigInject.ts b/components/_util/hooks/useConfigInject.ts new file mode 100644 index 000000000..c804f3031 --- /dev/null +++ b/components/_util/hooks/useConfigInject.ts @@ -0,0 +1,8 @@ +import { computed, inject } from 'vue'; +import { defaultConfigProvider } from '../../config-provider'; + +export default (name: string, props: Record) => { + const configProvider = inject('configProvider', defaultConfigProvider); + const prefixCls = computed(() => configProvider.getPrefixCls(name, props.prefixCls)); + return { configProvider, prefixCls }; +}; diff --git a/components/typography/Base.tsx b/components/typography/Base.tsx index 9b9b8c9ae..80cbc1568 100644 --- a/components/typography/Base.tsx +++ b/components/typography/Base.tsx @@ -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({ 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({ 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({ }); 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({ } 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({ } 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({ } // ================ Copy ================ - function onCopyClick() { + function onCopyClick(e: MouseEvent) { + e.preventDefault(); const { copyable } = props; const copyConfig = { @@ -250,30 +237,18 @@ const Base = defineComponent({ }, 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({ }); } - 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({ } 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({ } 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({ return ( @@ -398,7 +365,6 @@ const Base = defineComponent({ 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({ @@ -421,8 +387,6 @@ const Base = defineComponent({ 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({ return ( @@ -446,19 +413,16 @@ const Base = defineComponent({ } function renderEditInput() { - const prefixCls = getPrefixCls(); const { class: className, style } = attrs; - const { maxLength, autoSize } = getEditable(); - - const value = getChildrenText(); + const { maxlength, autoSize } = editable.value; return ( ({ 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({ { - 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({ '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({ // 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({ {suffix} ); + // If provided tooltip, we need wrap with span to let Tooltip inject events + if (tooltip) { + textNode = ( + + {textNode} + + ); + } } else { textNode = ( <> @@ -544,23 +516,21 @@ const Base = defineComponent({ textNode = wrapperDecorations(props, textNode); - const prefixCls = getPrefixCls(); - return ( ({ 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(); diff --git a/components/typography/Link.tsx b/components/typography/Link.tsx index f727194e2..53aeaa1c7 100644 --- a/components/typography/Link.tsx +++ b/components/typography/Link.tsx @@ -25,10 +25,10 @@ const Link: FunctionalComponent = (props, { slots, attrs }) => { // @ts-ignore delete mergedProps.navigate; - return {slots.default?.()}; + return ; }; -Link.displayName = 'ATypographyText'; +Link.displayName = 'ATypographyLink'; Link.inheritAttrs = false; Link.props = Omit({ ...baseProps(), ellipsis: PropTypes.looseBool }, ['component']); diff --git a/components/typography/Paragraph.tsx b/components/typography/Paragraph.tsx index b266b6845..3a93837c5 100644 --- a/components/typography/Paragraph.tsx +++ b/components/typography/Paragraph.tsx @@ -9,7 +9,7 @@ const Paragraph: FunctionalComponent = (props, { slots, attrs }) => ...attrs, }; - return {slots.default?.()}; + return ; }; Paragraph.displayName = 'ATypographyParagraph'; diff --git a/components/typography/Text.tsx b/components/typography/Text.tsx index 7dbf12814..fd830425d 100644 --- a/components/typography/Text.tsx +++ b/components/typography/Text.tsx @@ -27,7 +27,7 @@ const Text: FunctionalComponent = (props, { slots, attrs }) => { ...attrs, }; - return {slots.default?.()}; + return ; }; Text.displayName = 'ATypographyText'; diff --git a/components/typography/Title.tsx b/components/typography/Title.tsx index f3ae3cf51..896d629d2 100644 --- a/components/typography/Title.tsx +++ b/components/typography/Title.tsx @@ -25,7 +25,7 @@ const Title: FunctionalComponent = (props, { slots, attrs }) => { ...attrs, }; - return {slots.default?.()}; + return ; }; Title.displayName = 'ATypographyTitle'; diff --git a/components/typography/Typography.tsx b/components/typography/Typography.tsx index 37215d924..63b4c2828 100644 --- a/components/typography/Typography.tsx +++ b/components/typography/Typography.tsx @@ -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({ 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 {slots.default?.()}; + const { + prefixCls: _prefixCls, + class: _className, + component: Component = 'article' as any, + ...restProps + } = { ...props, ...attrs }; + return ( + + {slots.default?.()} + + ); }; }, }); @@ -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; }; diff --git a/components/typography/style/index.less b/components/typography/style/index.less index c4c59ddce..db1d4e4f6 100644 --- a/components/typography/style/index.less +++ b/components/typography/style/index.less @@ -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'; diff --git a/components/typography/style/rtl.less b/components/typography/style/rtl.less new file mode 100644 index 000000000..f2866ad08 --- /dev/null +++ b/components/typography/style/rtl.less @@ -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; + } + } + } +} diff --git a/examples/App.vue b/examples/App.vue index bd9c8b1c1..58666e342 100644 --- a/examples/App.vue +++ b/examples/App.vue @@ -5,7 +5,7 @@