refactor: descriptions (#6213)

* refactor: descriptions

* fix: ts error

Co-authored-by: tangjinzhou <415800467@qq.com>
pull/6215/head
zkwolf 2023-01-27 16:46:58 +08:00 committed by GitHub
parent 2d54e2124f
commit 9d5343ae9c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 326 additions and 303 deletions

View File

@ -1,19 +1,21 @@
import type { Ref } from 'vue';
import { onMounted, onUnmounted, ref } from 'vue';
import type { ScreenMap } from '../../_util/responsiveObserve';
import ResponsiveObserve from '../../_util/responsiveObserve';
import useResponsiveObserve from '../../_util/responsiveObserve';
function useBreakpoint(): Ref<ScreenMap> {
const screens = ref<ScreenMap>({});
let token = null;
const responsiveObserve = useResponsiveObserve();
onMounted(() => {
token = ResponsiveObserve.subscribe(supportScreens => {
token = responsiveObserve.value.subscribe(supportScreens => {
screens.value = supportScreens;
});
});
onUnmounted(() => {
ResponsiveObserve.unsubscribe(token);
responsiveObserve.value.unsubscribe(token);
});
return screens;

View File

@ -1,75 +1,86 @@
import type { GlobalToken } from '../theme/interface';
import { useToken } from '../theme/internal';
import useMemo from './hooks/useMemo';
export type Breakpoint = 'xxxl' | 'xxl' | 'xl' | 'lg' | 'md' | 'sm' | 'xs';
export type BreakpointMap = Record<Breakpoint, string>;
export type ScreenMap = Partial<Record<Breakpoint, boolean>>;
export type ScreenSizeMap = Partial<Record<Breakpoint, number>>;
export const responsiveArray: Breakpoint[] = ['xxxl', 'xxl', 'xl', 'lg', 'md', 'sm', 'xs'];
export const responsiveMap: BreakpointMap = {
xs: '(max-width: 575px)',
sm: '(min-width: 576px)',
md: '(min-width: 768px)',
lg: '(min-width: 992px)',
xl: '(min-width: 1200px)',
xxl: '(min-width: 1600px)',
xxxl: '(min-width: 2000px)',
};
type SubscribeFunc = (screens: ScreenMap) => void;
const subscribers = new Map<Number, SubscribeFunc>();
let subUid = -1;
let screens = {};
const responsiveObserve = {
matchHandlers: {} as {
[prop: string]: {
mql: MediaQueryList;
listener: ((this: MediaQueryList, ev: MediaQueryListEvent) => any) | null;
};
},
dispatch(pointMap: ScreenMap) {
screens = pointMap;
subscribers.forEach(func => func(screens));
return subscribers.size >= 1;
},
subscribe(func: SubscribeFunc): number {
if (!subscribers.size) this.register();
subUid += 1;
subscribers.set(subUid, func);
func(screens);
return subUid;
},
unsubscribe(token: number) {
subscribers.delete(token);
if (!subscribers.size) this.unregister();
},
unregister() {
Object.keys(responsiveMap).forEach((screen: string) => {
const matchMediaQuery = responsiveMap[screen];
const handler = this.matchHandlers[matchMediaQuery];
handler?.mql.removeListener(handler?.listener);
});
subscribers.clear();
},
register() {
Object.keys(responsiveMap).forEach((screen: string) => {
const matchMediaQuery = responsiveMap[screen];
const listener = ({ matches }: { matches: boolean }) => {
this.dispatch({
...screens,
[screen]: matches,
const getResponsiveMap = (token: GlobalToken): BreakpointMap => ({
xs: `(max-width: ${token.screenXSMax}px)`,
sm: `(min-width: ${token.screenSM}px)`,
md: `(min-width: ${token.screenMD}px)`,
lg: `(min-width: ${token.screenLG}px)`,
xl: `(min-width: ${token.screenXL}px)`,
xxl: `(min-width: ${token.screenXXL}px)`,
xxxl: `{min-width: ${token.screenXXXL}px}`,
});
export default function useResponsiveObserver() {
const [, token] = useToken();
const responsiveMap: BreakpointMap = getResponsiveMap(token.value);
// To avoid repeat create instance, we add `useMemo` here.
return useMemo(() => {
const subscribers = new Map<Number, SubscribeFunc>();
let subUid = -1;
let screens = {};
return {
matchHandlers: {} as {
[prop: string]: {
mql: MediaQueryList;
listener: ((this: MediaQueryList, ev: MediaQueryListEvent) => any) | null;
};
},
dispatch(pointMap: ScreenMap) {
screens = pointMap;
subscribers.forEach(func => func(screens));
return subscribers.size >= 1;
},
subscribe(func: SubscribeFunc): number {
if (!subscribers.size) this.register();
subUid += 1;
subscribers.set(subUid, func);
func(screens);
return subUid;
},
unsubscribe(paramToken: number) {
subscribers.delete(paramToken);
if (!subscribers.size) this.unregister();
},
unregister() {
Object.keys(responsiveMap).forEach((screen: string) => {
const matchMediaQuery = responsiveMap[screen];
const handler = this.matchHandlers[matchMediaQuery];
handler?.mql.removeListener(handler?.listener);
});
};
const mql = window.matchMedia(matchMediaQuery);
mql.addListener(listener);
this.matchHandlers[matchMediaQuery] = {
mql,
listener,
};
subscribers.clear();
},
register() {
Object.keys(responsiveMap).forEach((screen: string) => {
const matchMediaQuery = responsiveMap[screen];
const listener = ({ matches }: { matches: boolean }) => {
this.dispatch({
...screens,
[screen]: matches,
});
};
const mql = window.matchMedia(matchMediaQuery);
mql.addListener(listener);
this.matchHandlers[matchMediaQuery] = {
mql,
listener,
};
listener(mql);
});
},
};
export default responsiveObserve;
listener(mql);
});
},
responsiveMap,
};
}, [token]);
}

View File

@ -11,8 +11,8 @@ interface CellProps extends HTMLAttributes {
labelStyle?: CSSProperties;
contentStyle?: CSSProperties;
bordered?: boolean;
label?: VNodeTypes;
content?: VNodeTypes;
label?: number | VNodeTypes;
content?: number | VNodeTypes;
colon?: boolean;
}
@ -49,7 +49,7 @@ const Cell: FunctionalComponent<CellProps> = props => {
return (
<Component class={[`${itemPrefixCls}-item`]} colSpan={span}>
<div class={`${itemPrefixCls}-item-container`}>
{label && (
{(label || label === 0) && (
<span
class={[
`${itemPrefixCls}-item-label`,
@ -62,7 +62,7 @@ const Cell: FunctionalComponent<CellProps> = props => {
{label}
</span>
)}
{content && (
{(content || content === 0) && (
<span class={`${itemPrefixCls}-item-content`} style={contentStyle}>
{content}
</span>

View File

@ -2,7 +2,7 @@
category: Components
type: Data Display
title: Descriptions
cover: https://gw.alipayobjects.com/zos/alicdn/MjtG9_FOI/Descriptions.svg
cover: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*fHdlTpif6XQAAAAAAAAAAAAADrJ8AQ/original
---
Display multiple read-only fields in groups.

View File

@ -20,13 +20,15 @@ import {
} from 'vue';
import warning from '../_util/warning';
import type { Breakpoint, ScreenMap } from '../_util/responsiveObserve';
import ResponsiveObserve, { responsiveArray } from '../_util/responsiveObserve';
import useResponsiveObserve, { responsiveArray } from '../_util/responsiveObserve';
import Row from './Row';
import PropTypes from '../_util/vue-types';
import { cloneElement } from '../_util/vnode';
import { flattenChildren } from '../_util/props-util';
import useConfigInject from '../config-provider/hooks/useConfigInject';
import useStyle from './style';
export const DescriptionsItemProps = {
prefixCls: String,
label: PropTypes.any,
@ -82,7 +84,7 @@ function getColumn(column: DescriptionsProps['column'], screens: ScreenMap): num
return 3;
}
function getFilledItem(node: VNode, span: number | undefined, rowRestCol: number): VNode {
function getFilledItem(node: VNode, rowRestCol: number, span?: number): VNode {
let clone = node;
if (span === undefined || span > rowRestCol) {
@ -106,12 +108,12 @@ function getRows(children: VNode[], column: number) {
let tmpRow: VNode[] = [];
let rowRestCol = column;
childNodes.forEach((node, index) => {
const span: number | undefined = node.props?.span;
const span: number = node.props?.span;
const mergedSpan = span || 1;
// Additional handle last one
if (index === childNodes.length - 1) {
tmpRow.push(getFilledItem(node, span, rowRestCol));
tmpRow.push(getFilledItem(node, rowRestCol, span));
rows.push(tmpRow);
return;
}
@ -120,7 +122,7 @@ function getRows(children: VNode[], column: number) {
rowRestCol -= mergedSpan;
tmpRow.push(node);
} else {
tmpRow.push(getFilledItem(node, mergedSpan, rowRestCol));
tmpRow.push(getFilledItem(node, rowRestCol, mergedSpan));
rows.push(tmpRow);
rowRestCol = column;
tmpRow = [];
@ -167,8 +169,12 @@ const Descriptions = defineComponent({
const { prefixCls, direction } = useConfigInject('descriptions', props);
let token: number;
const screens = ref<ScreenMap>({});
const [wrapSSR, hashId] = useStyle(prefixCls);
const responsiveObserve = useResponsiveObserve();
onBeforeMount(() => {
token = ResponsiveObserve.subscribe(screen => {
token = responsiveObserve.value.subscribe(screen => {
if (typeof props.column !== 'object') {
return;
}
@ -177,7 +183,7 @@ const Descriptions = defineComponent({
});
onBeforeUnmount(() => {
ResponsiveObserve.unsubscribe(token);
responsiveObserve.value.unsubscribe(token);
});
provide(descriptionsContext, {
@ -200,7 +206,7 @@ const Descriptions = defineComponent({
const children = slots.default?.();
const rows = getRows(children, mergeColumn.value);
return (
return wrapSSR(
<div
class={[
prefixCls.value,
@ -209,6 +215,7 @@ const Descriptions = defineComponent({
[`${prefixCls.value}-bordered`]: !!bordered,
[`${prefixCls.value}-rtl`]: direction.value === 'rtl',
},
hashId.value,
]}
>
{(title || extra) && (
@ -234,7 +241,7 @@ const Descriptions = defineComponent({
</tbody>
</table>
</div>
</div>
</div>,
);
};
},

View File

@ -3,7 +3,7 @@ category: Components
type: 数据展示
title: Descriptions
subtitle: 描述列表
cover: https://gw.alipayobjects.com/zos/alicdn/MjtG9_FOI/Descriptions.svg
cover: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*fHdlTpif6XQAAAAAAAAAAAAADrJ8AQ/original
---
成组展示多个只读字段。

View File

@ -1,179 +0,0 @@
@import '../../style/themes/index';
@import '../../style/mixins/index';
@descriptions-prefix-cls: ~'@{ant-prefix}-descriptions';
.@{descriptions-prefix-cls} {
&-header {
display: flex;
align-items: center;
margin-bottom: @descriptions-title-margin-bottom;
}
&-title {
flex: auto;
overflow: hidden;
color: @heading-color;
font-weight: bold;
font-size: @font-size-lg;
line-height: @line-height-base;
white-space: nowrap;
text-overflow: ellipsis;
}
&-extra {
margin-left: auto;
color: @descriptions-extra-color;
font-size: @font-size-base;
}
&-view {
width: 100%;
border-radius: @border-radius-base;
table {
width: 100%;
table-layout: fixed;
}
}
&-row {
> th,
> td {
padding-bottom: @descriptions-item-padding-bottom;
}
&:last-child {
border-bottom: none;
}
}
&-item-label {
color: @heading-color;
font-weight: normal;
font-size: @font-size-base;
line-height: @line-height-base;
text-align: start;
&::after {
& when (@descriptions-item-trailing-colon=true) {
content: ':';
}
& when not (@descriptions-item-trailing-colon=true) {
content: ' ';
}
position: relative;
top: -0.5px;
margin: 0 @descriptions-item-label-colon-margin-right 0
@descriptions-item-label-colon-margin-left;
}
&.@{descriptions-prefix-cls}-item-no-colon::after {
content: ' ';
}
}
&-item-no-label {
&::after {
margin: 0;
content: '';
}
}
&-item-content {
display: table-cell;
flex: 1;
color: @text-color;
font-size: @font-size-base;
line-height: @line-height-base;
word-break: break-word;
overflow-wrap: break-word;
}
&-item {
padding-bottom: 0;
vertical-align: top;
&-container {
display: flex;
.@{descriptions-prefix-cls}-item-label,
.@{descriptions-prefix-cls}-item-content {
display: inline-flex;
align-items: baseline;
}
}
}
&-middle {
.@{descriptions-prefix-cls}-row {
> th,
> td {
padding-bottom: @padding-sm;
}
}
}
&-small {
.@{descriptions-prefix-cls}-row {
> th,
> td {
padding-bottom: @padding-xs;
}
}
}
&-bordered {
.@{descriptions-prefix-cls}-view {
border: 1px solid @border-color-split;
> table {
table-layout: auto;
border-collapse: collapse;
}
}
.@{descriptions-prefix-cls}-item-label,
.@{descriptions-prefix-cls}-item-content {
padding: @descriptions-default-padding;
border-right: 1px solid @border-color-split;
&:last-child {
border-right: none;
}
}
.@{descriptions-prefix-cls}-item-label {
background-color: @descriptions-bg;
&::after {
display: none;
}
}
.@{descriptions-prefix-cls}-row {
border-bottom: 1px solid @border-color-split;
&:last-child {
border-bottom: none;
}
}
&.@{descriptions-prefix-cls}-middle {
.@{descriptions-prefix-cls}-item-label,
.@{descriptions-prefix-cls}-item-content {
padding: @descriptions-middle-padding;
}
}
&.@{descriptions-prefix-cls}-small {
.@{descriptions-prefix-cls}-item-label,
.@{descriptions-prefix-cls}-item-content {
padding: @descriptions-small-padding;
}
}
}
}
@import './rtl';

View File

@ -0,0 +1,208 @@
import type { CSSObject } from '../../_util/cssinjs';
import type { FullToken, GenerateStyle } from '../../theme/internal';
import { genComponentStyleHook, mergeToken } from '../../theme/internal';
import { resetComponent, textEllipsis } from '../../_style';
interface DescriptionsToken extends FullToken<'Descriptions'> {
descriptionsTitleMarginBottom: number;
descriptionsExtraColor: string;
descriptionItemPaddingBottom: number;
descriptionsDefaultPadding: string;
descriptionsBg: string;
descriptionsMiddlePadding: string;
descriptionsSmallPadding: string;
descriptionsItemLabelColonMarginRight: number;
descriptionsItemLabelColonMarginLeft: number;
}
const genBorderedStyle = (token: DescriptionsToken): CSSObject => {
const {
componentCls,
descriptionsSmallPadding,
descriptionsDefaultPadding,
descriptionsMiddlePadding,
descriptionsBg,
} = token;
return {
[`&${componentCls}-bordered`]: {
[`${componentCls}-view`]: {
border: `${token.lineWidth}px ${token.lineType} ${token.colorSplit}`,
'> table': {
tableLayout: 'auto',
borderCollapse: 'collapse',
},
},
[`${componentCls}-item-label, ${componentCls}-item-content`]: {
padding: descriptionsDefaultPadding,
borderInlineEnd: `${token.lineWidth}px ${token.lineType} ${token.colorSplit}`,
'&:last-child': {
borderInlineEnd: 'none',
},
},
[`${componentCls}-item-label`]: {
backgroundColor: descriptionsBg,
'&::after': {
display: 'none',
},
},
[`${componentCls}-row`]: {
borderBottom: `${token.lineWidth}px ${token.lineType} ${token.colorSplit}`,
'&:last-child': {
borderBottom: 'none',
},
},
[`&${componentCls}-middle`]: {
[`${componentCls}-item-label, ${componentCls}-item-content`]: {
padding: descriptionsMiddlePadding,
},
},
[`&${componentCls}-small`]: {
[`${componentCls}-item-label, ${componentCls}-item-content`]: {
padding: descriptionsSmallPadding,
},
},
},
};
};
const genDescriptionStyles: GenerateStyle<DescriptionsToken> = (token: DescriptionsToken) => {
const {
componentCls,
descriptionsExtraColor,
descriptionItemPaddingBottom,
descriptionsItemLabelColonMarginRight,
descriptionsItemLabelColonMarginLeft,
descriptionsTitleMarginBottom,
} = token;
return {
[componentCls]: {
...resetComponent(token),
...genBorderedStyle(token),
[`&-rtl`]: {
direction: 'rtl',
},
[`${componentCls}-header`]: {
display: 'flex',
alignItems: 'center',
marginBottom: descriptionsTitleMarginBottom,
},
[`${componentCls}-title`]: {
...textEllipsis,
flex: 'auto',
color: token.colorText,
fontWeight: token.fontWeightStrong,
fontSize: token.fontSizeLG,
lineHeight: token.lineHeightLG,
},
[`${componentCls}-extra`]: {
marginInlineStart: 'auto',
color: descriptionsExtraColor,
fontSize: token.fontSize,
},
[`${componentCls}-view`]: {
width: '100%',
borderRadius: token.borderRadiusLG,
table: {
width: '100%',
tableLayout: 'fixed',
},
},
[`${componentCls}-row`]: {
'> th, > td': {
paddingBottom: descriptionItemPaddingBottom,
},
'&:last-child': {
borderBottom: 'none',
},
},
[`${componentCls}-item-label`]: {
color: token.colorText,
fontWeight: 'normal',
fontSize: token.fontSize,
lineHeight: token.lineHeight,
textAlign: `start`,
'&::after': {
content: '":"',
position: 'relative',
top: -0.5, // magic for position
marginInline: `${descriptionsItemLabelColonMarginLeft}px ${descriptionsItemLabelColonMarginRight}px`,
},
[`&${componentCls}-item-no-colon::after`]: {
content: '""',
},
},
[`${componentCls}-item-no-label`]: {
'&::after': {
margin: 0,
content: '""',
},
},
[`${componentCls}-item-content`]: {
display: 'table-cell',
flex: 1,
color: token.colorText,
fontSize: token.fontSize,
lineHeight: token.lineHeight,
wordBreak: 'break-word',
overflowWrap: 'break-word',
},
[`${componentCls}-item`]: {
paddingBottom: 0,
verticalAlign: 'top',
'&-container': {
display: 'flex',
[`${componentCls}-item-label`]: {
display: 'inline-flex',
alignItems: 'baseline',
},
[`${componentCls}-item-content`]: {
display: 'inline-flex',
alignItems: 'baseline',
},
},
},
'&-middle': {
[`${componentCls}-row`]: {
'> th, > td': {
paddingBottom: token.paddingSM,
},
},
},
'&-small': {
[`${componentCls}-row`]: {
'> th, > td': {
paddingBottom: token.paddingXS,
},
},
},
},
};
};
// ============================== Export ==============================
export default genComponentStyleHook('Descriptions', token => {
const descriptionsBg = token.colorFillAlter;
const descriptionsTitleMarginBottom = token.fontSizeSM * token.lineHeightSM;
const descriptionsExtraColor = token.colorText;
const descriptionsSmallPadding = `${token.paddingXS}px ${token.padding}px`;
const descriptionsDefaultPadding = `${token.padding}px ${token.paddingLG}px`;
const descriptionsMiddlePadding = `${token.paddingSM}px ${token.paddingLG}px`;
const descriptionItemPaddingBottom = token.padding;
const descriptionsItemLabelColonMarginRight = token.marginXS;
const descriptionsItemLabelColonMarginLeft = token.marginXXS / 2;
const descriptionToken = mergeToken<DescriptionsToken>(token, {
descriptionsBg,
descriptionsTitleMarginBottom,
descriptionsExtraColor,
descriptionItemPaddingBottom,
descriptionsSmallPadding,
descriptionsDefaultPadding,
descriptionsMiddlePadding,
descriptionsItemLabelColonMarginRight,
descriptionsItemLabelColonMarginLeft,
});
return [genDescriptionStyles(descriptionToken)];
});

View File

@ -1,2 +0,0 @@
import '../../style/index.less';
import './index.less';

View File

@ -1,33 +0,0 @@
@import '../../style/themes/index';
@import '../../style/mixins/index';
@descriptions-prefix-cls: ~'@{ant-prefix}-descriptions';
.@{descriptions-prefix-cls} {
&-rtl {
direction: rtl;
}
&-item-label {
&::after {
.@{descriptions-prefix-cls}-rtl & {
margin: 0 @descriptions-item-label-colon-margin-left 0
@descriptions-item-label-colon-margin-right;
}
}
}
&-bordered {
.@{descriptions-prefix-cls}-item-label,
.@{descriptions-prefix-cls}-item-content {
.@{descriptions-prefix-cls}-rtl& {
border-right: none;
border-left: 1px solid @border-color-split;
&:last-child {
border-left: none;
}
}
}
}
}

View File

@ -3,7 +3,7 @@ import { defineComponent, ref, onMounted, onBeforeUnmount, computed } from 'vue'
import classNames from '../_util/classNames';
import { tuple } from '../_util/type';
import type { Breakpoint, ScreenMap } from '../_util/responsiveObserve';
import ResponsiveObserve, { responsiveArray } from '../_util/responsiveObserve';
import useResponsiveObserve, { responsiveArray } from '../_util/responsiveObserve';
import useConfigInject from '../config-provider/hooks/useConfigInject';
import useFlexGapSupport from '../_util/hooks/useFlexGapSupport';
import useProvideRow from './context';
@ -19,8 +19,8 @@ export interface rowContextState {
}
export const rowProps = () => ({
align: String as PropType<typeof RowAligns[number]>,
justify: String as PropType<typeof RowJustify[number]>,
align: String as PropType<(typeof RowAligns)[number]>,
justify: String as PropType<(typeof RowJustify)[number]>,
prefixCls: String,
gutter: {
type: [Number, Array, Object] as PropType<Gutter | [Gutter, Gutter]>,
@ -40,6 +40,8 @@ const ARow = defineComponent({
let token: number;
const responsiveObserve = useResponsiveObserve();
const screens = ref<ScreenMap>({
xs: true,
sm: true,
@ -53,7 +55,7 @@ const ARow = defineComponent({
const supportFlexGap = useFlexGapSupport();
onMounted(() => {
token = ResponsiveObserve.subscribe(screen => {
token = responsiveObserve.value.subscribe(screen => {
const currentGutter = props.gutter || 0;
if (
(!Array.isArray(currentGutter) && typeof currentGutter === 'object') ||
@ -66,7 +68,7 @@ const ARow = defineComponent({
});
onBeforeUnmount(() => {
ResponsiveObserve.unsubscribe(token);
responsiveObserve.value.unsubscribe(token);
});
const gutter = computed(() => {

View File

@ -55,7 +55,7 @@ import './comment/style';
import './empty/style';
import './statistic/style';
import './result/style';
import './descriptions/style';
// import './descriptions/style';
import './page-header/style';
import './form/style';
import './space/style';

View File

@ -120,6 +120,9 @@ export interface AliasToken extends MapToken {
screenXLMax: number;
screenXXL: number;
screenXXLMin: number;
screenXXLMax: number;
screenXXXL: number;
screenXXXLMin: number;
/** Used for DefaultButton, Switch which has default outline */
controlTmpOutline: string;

View File

@ -30,6 +30,7 @@ export default function formatToken(derivativeToken: RawMergedToken): AliasToken
const screenLG = 992;
const screenXL = 1200;
const screenXXL = 1600;
const screenXXXL = 2000;
// Generate alias token
const aliasToken: AliasToken = {
@ -151,6 +152,9 @@ export default function formatToken(derivativeToken: RawMergedToken): AliasToken
screenXLMax: screenXXL - 1,
screenXXL,
screenXXLMin: screenXXL,
screenXXLMax: screenXXXL - 1,
screenXXXL,
screenXXXLMin: screenXXXL,
// FIXME: component box-shadow, should be removed
boxShadowPopoverArrow: '3px 3px 7px rgba(0, 0, 0, 0.1)',