From 55f7cd528eeac1c18b9f313193c8a07587ffa545 Mon Sep 17 00:00:00 2001 From: W_GUYONGCHANG Date: Tue, 15 Aug 2023 19:04:26 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E7=BB=84=E4=BB=B6=20?= =?UTF-8?q?Space?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/space/Item.tsx | 57 +++++++++++ components/space/configProvider.tsx | 21 ++++ components/space/index.md | 24 +++++ components/space/index.scss | 30 ++++++ components/space/index.tsx | 142 ++++++++++++++++++++++++++ components/space/styleChecker.ts | 20 ++++ components/space/toArray.tsx | 28 +++++ components/space/useFlexGapSupport.ts | 11 ++ 8 files changed, 333 insertions(+) create mode 100644 components/space/Item.tsx create mode 100644 components/space/configProvider.tsx create mode 100644 components/space/index.md create mode 100644 components/space/index.scss create mode 100644 components/space/index.tsx create mode 100644 components/space/styleChecker.ts create mode 100644 components/space/toArray.tsx create mode 100644 components/space/useFlexGapSupport.ts diff --git a/components/space/Item.tsx b/components/space/Item.tsx new file mode 100644 index 000000000..43018e72e --- /dev/null +++ b/components/space/Item.tsx @@ -0,0 +1,57 @@ +import * as React from "react"; +import { SpaceContext } from "./index"; + +export interface ItemProps { + className: string; + children: React.ReactNode; + index: number; + direction?: "horizontal" | "vertical"; + marginDirection: "marginLeft" | "marginRight"; + split?: string | React.ReactNode; + wrap?: boolean; +} + +export default function Item({ + className, + direction, + index, + marginDirection, + children, + split, + wrap, +}: ItemProps) { + const { horizontalSize, verticalSize, latestIndex, supportFlexGap } = + React.useContext(SpaceContext); + + let style: React.CSSProperties = {}; + + if (!supportFlexGap) { + if (direction === "vertical") { + if (index < latestIndex) { + style = { marginBottom: horizontalSize / (split ? 2 : 1) }; + } + } else { + style = { + ...(index < latestIndex && { [marginDirection]: horizontalSize / (split ? 2 : 1) }), + ...(wrap && { paddingBottom: verticalSize }), + }; + } + } + + if (children === null || children === undefined) { + return null; + } + + return ( + <> +
+ {children} +
+ {index < latestIndex && split && ( + + {split} + + )} + + ); +} diff --git a/components/space/configProvider.tsx b/components/space/configProvider.tsx new file mode 100644 index 000000000..f9f3e7622 --- /dev/null +++ b/components/space/configProvider.tsx @@ -0,0 +1,21 @@ +import React from "react"; + +export type DirectionType = "ltr" | "rtl" | undefined; + +export type SizeType = "small" | "middle" | "large" | undefined; + +export interface ConfigConsumerProps { + getPrefixCls: (suffixCls?: string) => string; + direction?: DirectionType; + space?: { + size?: SizeType | number; + }; +} + +export const defaultGetPrefixCls = (suffixCls?: string) => { + return suffixCls ? `auge-${suffixCls}` : "auge"; +}; + +export const ConfigContext = React.createContext({ + getPrefixCls: defaultGetPrefixCls, +}); diff --git a/components/space/index.md b/components/space/index.md new file mode 100644 index 000000000..6ff72d2ce --- /dev/null +++ b/components/space/index.md @@ -0,0 +1,24 @@ +--- +title: 间距 +category: 组件 +order: 99 +sidebar: doc +--- + +# 属性 + +| 属性 | 说明 | 类型 | 默认值 | +| --------- | ------------------------------- | ---------- | ----------- | +| className | 指定元素固定距离顶部的位置 | `number` | `undefined` | +| style | 指定元素固定距离底部的位置 | `number` | `undefined` | +| size | 间距大小 | `string` | `samll ` | +| direction | 排列方向 | `Function` | `undefined` | +| align | 对齐方式 | `Function` | `undefined` | +| split | 分隔符 | `Function` | `undefined` | +| wrap | 是否自动换行(horizontal 时生效) | `Function` | `undefined` | + +# 事件 + +| 事件名 | 说明 | 参数 | +| ------ | ------------------ | --------- | +| change | 固定状态改变时触发 | `isFixed` | diff --git a/components/space/index.scss b/components/space/index.scss new file mode 100644 index 000000000..bc3ba6e46 --- /dev/null +++ b/components/space/index.scss @@ -0,0 +1,30 @@ +$auge-prefix: 'auge'; + +$space-prefix-cls: #{$auge-prefix}-space; +$space-item-prefix-cls: #{$auge-prefix}-space-item; + +.#{$space-prefix-cls} { + display: inline-flex; + &-vertical { + flex-direction: column; + } + &-align { + &-center { + align-items: center; + } + &-start { + align-items: flex-start; + } + &-end { + align-items: flex-end; + } + &-baseline { + align-items: baseline; + } + } +} +.#{$space-prefix-cls} { + &-rtl { + direction: rtl; + } +} diff --git a/components/space/index.tsx b/components/space/index.tsx new file mode 100644 index 000000000..e74dde92b --- /dev/null +++ b/components/space/index.tsx @@ -0,0 +1,142 @@ +import React, { useContext } from "react"; +import classNames from "classnames"; +import { ConfigContext, SizeType } from "./configProvider"; +import Item from "./Item"; +import toArray from "./toArray"; +import useFlexGapSupport from "./useFlexGapSupport"; +import "./index.scss"; + +export interface Option { + keepEmpty?: boolean; +} + +export const SpaceContext = React.createContext({ + latestIndex: 0, + horizontalSize: 0, + verticalSize: 0, + supportFlexGap: false, +}); + +export type SpaceSize = SizeType | number; + +export interface SpaceProps extends React.HTMLAttributes { + className?: string; + style?: React.CSSProperties; + size?: SpaceSize | [SpaceSize, SpaceSize]; // 间距大小 + direction?: "horizontal" | "vertical"; // 排列方向 + align?: "start" | "end" | "center" | "baseline"; // 对齐方式 + split?: React.ReactNode; // 分隔符 + wrap?: boolean; // 是否自动换行 +} + +const spaceSize = { + small: 8, + middle: 16, + large: 24, +}; + +function getNumberSize(size: SpaceSize) { + return typeof size === "string" ? spaceSize[size] : size || 0; +} +const Space: React.FC = (props) => { + const { + getPrefixCls, + space, + direction: directionConfig, + } = useContext(ConfigContext); + const { + size = space?.size || "small", + align, + className, + children, + direction = "horizontal", + split, + style, + wrap = false, + ...otherProps + } = props; + const supportFlexGap = useFlexGapSupport(); + const [horizontalSize, verticalSize] = React.useMemo( + () => + ( + (Array.isArray(size) ? size : [size, size]) as [SpaceSize, SpaceSize] + ).map((item) => getNumberSize(item)), + [size] + ); + const childNodes = toArray(children, { keepEmpty: true }); + const mergedAlign = + align === undefined && direction === "horizontal" ? "center" : align; + const prefixCls = getPrefixCls("space"); + const cn = classNames( + prefixCls, + `${prefixCls}-${direction}`, + { + [`${prefixCls}-rtl`]: directionConfig === "rtl", + [`${prefixCls}-align-${mergedAlign}`]: mergedAlign, + }, + className + ); + const itemClassName = `${prefixCls}-item`; + const marginDirection = + directionConfig === "rtl" ? "marginLeft" : "marginRight"; + // Calculate latest one + let latestIndex = 0; + const nodes = childNodes.map((child: any, i) => { + if (child !== null && child !== undefined) { + latestIndex = i; + } + const key = (child && child.key) || `${itemClassName}-${i}`; + return ( + + {child} + + ); + }); + const spaceContext = React.useMemo( + () => ({ horizontalSize, verticalSize, latestIndex, supportFlexGap }), + [horizontalSize, verticalSize, latestIndex, supportFlexGap] + ); + + if (childNodes.length === 0) { + return null; + } + const gapStyle: React.CSSProperties = {}; + + if (wrap) { + gapStyle.flexWrap = "wrap"; + + if (!supportFlexGap) { + gapStyle.marginBottom = -verticalSize; + } + } + + if (supportFlexGap) { + gapStyle.columnGap = horizontalSize; + gapStyle.rowGap = verticalSize; + } + + return ( +
+ + {nodes} + +
+ ); +}; + +export default Space; diff --git a/components/space/styleChecker.ts b/components/space/styleChecker.ts new file mode 100644 index 000000000..10d7ee885 --- /dev/null +++ b/components/space/styleChecker.ts @@ -0,0 +1,20 @@ +let flexGapSupported: boolean | undefined; +export const detectFlexGapSupported = () => { + if (flexGapSupported !== undefined) { + return flexGapSupported; + } + + const flex = document.createElement("div"); + flex.style.display = "flex"; + flex.style.flexDirection = "column"; + flex.style.rowGap = "1px"; + + flex.appendChild(document.createElement("div")); + flex.appendChild(document.createElement("div")); + + document.body.appendChild(flex); + flexGapSupported = flex.scrollHeight === 1; + document.body.removeChild(flex); + + return flexGapSupported; +}; diff --git a/components/space/toArray.tsx b/components/space/toArray.tsx new file mode 100644 index 000000000..da7906617 --- /dev/null +++ b/components/space/toArray.tsx @@ -0,0 +1,28 @@ +import React from "react"; +import { isFragment } from "react-is"; + +export interface Option { + keepEmpty?: boolean; +} + +export default function toArray( + children: React.ReactNode, + option: Option = {}, +): React.ReactElement[] { + let ret: React.ReactElement[] = []; + + React.Children.forEach(children, (child: any | any[]) => { + if ((child === undefined || child === null) && !option.keepEmpty) { + return; + } + if (Array.isArray(child)) { + ret = ret.concat(toArray(child)); + } else if (isFragment(child) && child.props) { + ret = ret.concat(toArray(child.props.children, option)); + } else { + ret.push(child); + } + }); + + return ret; +} diff --git a/components/space/useFlexGapSupport.ts b/components/space/useFlexGapSupport.ts new file mode 100644 index 000000000..fa2b43597 --- /dev/null +++ b/components/space/useFlexGapSupport.ts @@ -0,0 +1,11 @@ +import * as React from "react"; +import { detectFlexGapSupported } from "./styleChecker"; + +export default () => { + const [flexible, setFlexible] = React.useState(false); + React.useEffect(() => { + setFlexible(detectFlexGapSupported()); + }, []); + + return flexible; +};