From c894adc8764b49bd6f26989c08c9d37aa3af4029 Mon Sep 17 00:00:00 2001 From: Lyca Date: Thu, 21 Oct 2021 20:17:13 +0800 Subject: [PATCH] feat(next/antd): support breakpoints for FormLayout (#2336) --- packages/antd/docs/components/FormItem.md | 2 +- .../antd/docs/components/FormItem.zh-CN.md | 2 +- packages/antd/docs/components/FormLayout.md | 53 ++++----- .../antd/docs/components/FormLayout.zh-CN.md | 61 +++++----- packages/antd/src/form-layout/index.tsx | 21 ++-- .../form-layout/useResponsiveFormLayout.ts | 108 ++++++++++++++++++ .../docs/demos/guide/form-item/common.vue | 2 +- packages/next/docs/components/FormItem.md | 2 +- .../next/docs/components/FormItem.zh-CN.md | 2 +- packages/next/docs/components/FormLayout.md | 53 ++++----- .../next/docs/components/FormLayout.zh-CN.md | 61 +++++----- packages/next/src/form-layout/index.tsx | 21 ++-- .../form-layout/useResponsiveFormLayout.ts | 108 ++++++++++++++++++ 13 files changed, 371 insertions(+), 125 deletions(-) create mode 100644 packages/antd/src/form-layout/useResponsiveFormLayout.ts create mode 100644 packages/next/src/form-layout/useResponsiveFormLayout.ts diff --git a/packages/antd/docs/components/FormItem.md b/packages/antd/docs/components/FormItem.md index 3a277bb0561..ad608422a8b 100644 --- a/packages/antd/docs/components/FormItem.md +++ b/packages/antd/docs/components/FormItem.md @@ -228,7 +228,7 @@ export default () => { }} /> { }} /> ( ## API -| Property name | Type | Description | Default value | -| -------------- | ---------------------------------------- | ------------------------------------- | ------------- | -| style | CSSProperties | Style | - | -| className | string | class name | - | -| colon | boolean | Is there a colon | true | -| labelAlign | `'right' \|'left'` | Label content alignment | - | -| wrapperAlign | `'right' \|'left'` | Component container content alignment | - | -| labelWrap | boolean | Wrap label content | false | -| labelWidth | number | Label width (px) | - | -| wrapperWidth | number | Component container width (px) | - | -| wrapperWrap | boolean | Component container wrap | false | -| labelCol | number | Label width (24 column) | - | -| wrapperCol | number | Component container width (24 column) | - | -| fullness | boolean | Component container width 100% | false | -| size | `'small' \|'default' \|'large'` | component size | default | -| layout | `'vertical' \|'horizontal' \|'inline'` | layout mode | horizontal | -| direction | `'rtl' \|'ltr'` | direction (not supported yet) | ltr | -| inset | boolean | Inline layout | false | -| shallow | boolean | shallow context transfer | true | -| feedbackLayout | `'loose' \|'terse' \|'popover' \|'none'` | feedback layout | true | -| tooltipLayout | `"icon" \| "text"` | Ask the prompt layout | `"icon"` | -| tooltipIcon | ReactNode | Ask the prompt icon | - | -| bordered | boolean | Is there a border | true | -| gridColumnGap | number | Grid Column Gap | 8 | -| gridRowGap | number | Grid Row Gap | 4 | -| spaceGap | number | Space Gap | 8 | +| Property name | Type | Description | Default value | +| -------------- | -------------------------------------------------------------------------------------- | ------------------------------------- | ------------- | +| style | CSSProperties | Style | - | +| className | string | class name | - | +| colon | boolean | Is there a colon | true | +| labelAlign | `'right' \| 'left' \| ('right' \| 'left')[]` | Label content alignment | - | +| wrapperAlign | `'right' \| 'left' \| ('right' \| 'left')[]` | Component container content alignment | - | +| labelWrap | boolean | Wrap label content | false | +| labelWidth | number | Label width (px) | - | +| wrapperWidth | number | Component container width (px) | - | +| wrapperWrap | boolean | Component container wrap | false | +| labelCol | `number \| number[]` | Label width (24 column) | - | +| wrapperCol | `number \| number[]` | Component container width (24 column) | - | +| fullness | boolean | Component container width 100% | false | +| size | `'small' \|'default' \|'large'` | component size | default | +| layout | `'vertical' \| 'horizontal' \| 'inline' \| ('vertical' \| 'horizontal' \| 'inline')[]` | layout mode | horizontal | +| direction | `'rtl' \|'ltr'` | direction (not supported yet) | ltr | +| inset | boolean | Inline layout | false | +| shallow | boolean | shallow context transfer | true | +| feedbackLayout | `'loose' \|'terse' \|'popover' \|'none'` | feedback layout | true | +| tooltipLayout | `"icon" \| "text"` | Ask the prompt layout | `"icon"` | +| tooltipIcon | ReactNode | Ask the prompt icon | - | +| bordered | boolean | Is there a border | true | +| breakpoints | number[] | Container size breakpoints | - | +| gridColumnGap | number | Grid Column Gap | 8 | +| gridRowGap | number | Grid Row Gap | 4 | +| spaceGap | number | Space Gap | 8 | diff --git a/packages/antd/docs/components/FormLayout.zh-CN.md b/packages/antd/docs/components/FormLayout.zh-CN.md index 66a34373297..627093ebd33 100644 --- a/packages/antd/docs/components/FormLayout.zh-CN.md +++ b/packages/antd/docs/components/FormLayout.zh-CN.md @@ -133,7 +133,13 @@ const form = createForm() export default () => ( - + ( ## API -| 属性名 | 类型 | 描述 | 默认值 | -| -------------- | ------------------------------------------- | ----------------------- | ---------- | -| style | CSSProperties | 样式 | - | -| className | string | 类名 | - | -| colon | boolean | 是否有冒号 | true | -| labelAlign | `'right' \| 'left'` | 标签内容对齐 | - | -| wrapperAlign | `'right' \| 'left'` | 组件容器内容对齐 | - | -| labelWrap | boolean | 标签内容换行 | false | -| labelWidth | number | 标签宽度(px) | - | -| wrapperWidth | number | 组件容器宽度(px) | - | -| wrapperWrap | boolean | 组件容器换行 | false | -| labelCol | number | 标签宽度(24 column) | - | -| wrapperCol | number | 组件容器宽度(24 column) | - | -| fullness | boolean | 组件容器宽度 100% | false | -| size | `'small' \| 'default' \| 'large'` | 组件尺寸 | default | -| layout | `'vertical' \| 'horizontal' \| 'inline'` | 布局模式 | horizontal | -| direction | `'rtl' \| 'ltr'` | 方向(暂不支持) | ltr | -| inset | boolean | 内联布局 | false | -| shallow | boolean | 上下文浅层传递 | true | -| feedbackLayout | `'loose' \| 'terse' \| 'popover' \| 'none'` | 反馈布局 | true | -| tooltipLayout | `"icon" \| "text"` | 问号提示布局 | `"icon"` | -| tooltipIcon | ReactNode | 问号提示图标 | - | -| bordered | boolean | 是否有边框 | true | -| gridColumnGap | number | 网格布局列间距 | 8 | -| gridRowGap | number | 网格布局行间距 | 4 | -| spaceGap | number | 弹性间距 | 8 | +| 属性名 | 类型 | 描述 | 默认值 | +| -------------- | -------------------------------------------------------------------------------------- | ----------------------- | ---------- | +| style | CSSProperties | 样式 | - | +| className | string | 类名 | - | +| colon | boolean | 是否有冒号 | true | +| labelAlign | `'right' \| 'left' \| ('right' \| 'left')[]` | 标签内容对齐 | - | +| wrapperAlign | `'right' \| 'left' \| ('right' \| 'left')[]` | 组件容器内容对齐 | - | +| labelWrap | boolean | 标签内容换行 | false | +| labelWidth | number | 标签宽度(px) | - | +| wrapperWidth | number | 组件容器宽度(px) | - | +| wrapperWrap | boolean | 组件容器换行 | false | +| labelCol | `number \| number[]` | 标签宽度(24 column) | - | +| wrapperCol | `number \| number[]` | 组件容器宽度(24 column) | - | +| fullness | boolean | 组件容器宽度 100% | false | +| size | `'small' \| 'default' \| 'large'` | 组件尺寸 | default | +| layout | `'vertical' \| 'horizontal' \| 'inline' \| ('vertical' \| 'horizontal' \| 'inline')[]` | 布局模式 | horizontal | +| direction | `'rtl' \| 'ltr'` | 方向(暂不支持) | ltr | +| inset | boolean | 内联布局 | false | +| shallow | boolean | 上下文浅层传递 | true | +| feedbackLayout | `'loose' \| 'terse' \| 'popover' \| 'none'` | 反馈布局 | true | +| tooltipLayout | `"icon" \| "text"` | 问号提示布局 | `"icon"` | +| tooltipIcon | ReactNode | 问号提示图标 | - | +| bordered | boolean | 是否有边框 | true | +| breakpoints | number[] | 容器尺寸断点 | - | +| gridColumnGap | number | 网格布局列间距 | 8 | +| gridRowGap | number | 网格布局行间距 | 4 | +| spaceGap | number | 弹性间距 | 8 | diff --git a/packages/antd/src/form-layout/index.tsx b/packages/antd/src/form-layout/index.tsx index cc13d91d160..b529a2e8b4f 100644 --- a/packages/antd/src/form-layout/index.tsx +++ b/packages/antd/src/form-layout/index.tsx @@ -1,4 +1,5 @@ import React, { createContext, useContext } from 'react' +import useResponsiveFormLayout from './useResponsiveFormLayout' import { usePrefixCls } from '../__builtins__' import cls from 'classnames' @@ -7,17 +8,21 @@ export interface IFormLayoutProps { className?: string style?: React.CSSProperties colon?: boolean - labelAlign?: 'right' | 'left' - wrapperAlign?: 'right' | 'left' + labelAlign?: 'right' | 'left' | ('right' | 'left')[] + wrapperAlign?: 'right' | 'left' | ('right' | 'left')[] labelWrap?: boolean labelWidth?: number wrapperWidth?: number wrapperWrap?: boolean - labelCol?: number - wrapperCol?: number + labelCol?: number | number[] + wrapperCol?: number | number[] fullness?: boolean size?: 'small' | 'default' | 'large' - layout?: 'vertical' | 'horizontal' | 'inline' + layout?: + | 'vertical' + | 'horizontal' + | 'inline' + | ('vertical' | 'horizontal' | 'inline')[] direction?: 'rtl' | 'ltr' inset?: boolean shallow?: boolean @@ -25,6 +30,7 @@ export interface IFormLayoutProps { tooltipIcon?: React.ReactNode feedbackLayout?: 'loose' | 'terse' | 'popover' | 'none' bordered?: boolean + breakpoints?: number[] spaceGap?: number gridColumnGap?: number gridRowGap?: number @@ -47,7 +53,8 @@ export const FormLayout: React.FC & { useFormLayout: () => IFormLayoutProps useFormDeepLayout: () => IFormLayoutProps useFormShallowLayout: () => IFormLayoutProps -} = ({ shallow, children, prefixCls, className, style, ...props }) => { +} = ({ shallow, children, prefixCls, className, style, ...otherProps }) => { + const { ref, props } = useResponsiveFormLayout(otherProps) const deepLayout = useFormDeepLayout() const formPrefixCls = usePrefixCls('form', { prefixCls }) const layoutPrefixCls = usePrefixCls('formily-layout', { prefixCls }) @@ -83,7 +90,7 @@ export const FormLayout: React.FC & { ) } return ( -
+
{renderChildren()}
) diff --git a/packages/antd/src/form-layout/useResponsiveFormLayout.ts b/packages/antd/src/form-layout/useResponsiveFormLayout.ts new file mode 100644 index 00000000000..26fc838c760 --- /dev/null +++ b/packages/antd/src/form-layout/useResponsiveFormLayout.ts @@ -0,0 +1,108 @@ +import { useRef, useState, useEffect } from 'react' +import { isArr, isValid } from '@formily/shared' + +interface IProps { + breakpoints?: number[] + layout?: + | 'vertical' + | 'horizontal' + | 'inline' + | ('vertical' | 'horizontal' | 'inline')[] + labelCol?: number | number[] + wrapperCol?: number | number[] + labelAlign?: 'right' | 'left' | ('right' | 'left')[] + wrapperAlign?: 'right' | 'left' | ('right' | 'left')[] + [props: string]: any +} + +interface ICalcBreakpointIndex { + (originalBreakpoints: number[], width: number): number +} + +interface ICalculateProps { + (target: HTMLElement, props: IProps): IProps +} + +interface IUseResponsiveFormLayout { + (props: IProps): { + ref: React.MutableRefObject + props: IProps + } +} + +const calcBreakpointIndex: ICalcBreakpointIndex = (breakpoints, width) => { + for (let i = 0; i < breakpoints.length; i++) { + if (width <= breakpoints[i]) { + return i + } + } +} + +const calcFactor = (value: T | T[], breakpointIndex: number): T => { + if (Array.isArray(value)) { + if (breakpointIndex === -1) return value[0] + return value[breakpointIndex] ?? value[value.length - 1] + } else { + return value + } +} + +const factor = (value: T | T[], breakpointIndex: number): T => + isValid(value) ? calcFactor(value as any, breakpointIndex) : value + +const calculateProps: ICalculateProps = (target, props) => { + const { clientWidth } = target + const { + breakpoints, + layout, + labelAlign, + wrapperAlign, + labelCol, + wrapperCol, + ...otherProps + } = props + const breakpointIndex = calcBreakpointIndex(breakpoints, clientWidth) + + return { + layout: factor(layout, breakpointIndex), + labelAlign: factor(labelAlign, breakpointIndex), + wrapperAlign: factor(wrapperAlign, breakpointIndex), + labelCol: factor(labelCol, breakpointIndex), + wrapperCol: factor(wrapperCol, breakpointIndex), + ...otherProps, + } +} + +const useResponsiveFormLayout: IUseResponsiveFormLayout = (props) => { + const ref = useRef(null) + const { breakpoints } = props + if (!isArr(breakpoints)) { + return { ref, props } + } + const [layoutProps, setLayout] = useState({}) + + const updateUI = () => { + setLayout(calculateProps(ref.current, props)) + } + + useEffect(() => { + const observer = () => { + updateUI() + } + const resizeObserver = new ResizeObserver(observer) + if (ref.current) { + resizeObserver.observe(ref.current) + } + updateUI() + return () => { + resizeObserver.disconnect() + } + }, []) + + return { + ref, + props: layoutProps, + } +} + +export default useResponsiveFormLayout diff --git a/packages/element/docs/demos/guide/form-item/common.vue b/packages/element/docs/demos/guide/form-item/common.vue index 3ffad062244..b857e7a4f01 100644 --- a/packages/element/docs/demos/guide/form-item/common.vue +++ b/packages/element/docs/demos/guide/form-item/common.vue @@ -62,7 +62,7 @@ }" /> { }} /> ( ## API -| Property name | Type | Description | Default value | -| -------------- | ---------------------------------------- | ------------------------------------- | ------------- | -| style | CSSProperties | Style | - | -| className | string | class name | - | -| colon | boolean | Is there a colon | true | -| labelAlign | `'right' \|'left'` | Label content alignment | - | -| wrapperAlign | `'right' \|'left'` | Component container content alignment | - | -| labelWrap | boolean | Wrap label content | false | -| labelWidth | number | Label width (px) | - | -| wrapperWidth | number | Component container width (px) | - | -| wrapperWrap | boolean | Component container wrap | false | -| labelCol | number | Label width (24 column) | - | -| wrapperCol | number | Component container width (24 column) | - | -| fullness | boolean | Component container width 100% | false | -| size | `'small' \|'default' \|'large'` | component size | default | -| layout | `'vertical' \|'horizontal' \|'inline'` | layout mode | horizontal | -| direction | `'rtl' \|'ltr'` | direction (not supported yet) | ltr | -| inset | boolean | Inline layout | false | -| shallow | boolean | shallow context transfer | true | -| feedbackLayout | `'loose' \|'terse' \|'popover' \|'none'` | feedback layout | true | -| tooltipLayout | `"icon" \| "text"` | Ask the prompt layout | `"icon"` | -| tooltipIcon | ReactNode | Ask the prompt icon | - | -| bordered | boolean | Is there a border | true | -| gridColumnGap | number | Grid Column Gap | 8 | -| gridRowGap | number | Grid Row Gap | 4 | -| spaceGap | number | Space Gap | 8 | +| Property name | Type | Description | Default value | +| -------------- | -------------------------------------------------------------------------------------- | ------------------------------------- | ------------- | +| style | CSSProperties | Style | - | +| className | string | class name | - | +| colon | boolean | Is there a colon | true | +| labelAlign | `'right' \| 'left' \| ('right' \| 'left')[]` | Label content alignment | - | +| wrapperAlign | `'right' \| 'left' \| ('right' \| 'left')[]` | Component container content alignment | - | +| labelWrap | boolean | Wrap label content | false | +| labelWidth | number | Label width (px) | - | +| wrapperWidth | number | Component container width (px) | - | +| wrapperWrap | boolean | Component container wrap | false | +| labelCol | `number \| number[]` | Label width (24 column) | - | +| wrapperCol | `number \| number[]` | Component container width (24 column) | - | +| fullness | boolean | Component container width 100% | false | +| size | `'small' \|'default' \|'large'` | component size | default | +| layout | `'vertical' \| 'horizontal' \| 'inline' \| ('vertical' \| 'horizontal' \| 'inline')[]` | layout mode | horizontal | +| direction | `'rtl' \|'ltr'` | direction (not supported yet) | ltr | +| inset | boolean | Inline layout | false | +| shallow | boolean | shallow context transfer | true | +| feedbackLayout | `'loose' \|'terse' \|'popover' \|'none'` | feedback layout | true | +| tooltipLayout | `"icon" \| "text"` | Ask the prompt layout | `"icon"` | +| tooltipIcon | ReactNode | Ask the prompt icon | - | +| bordered | boolean | Is there a border | true | +| breakpoints | number[] | Container size breakpoints | - | +| gridColumnGap | number | Grid Column Gap | 8 | +| gridRowGap | number | Grid Row Gap | 4 | +| spaceGap | number | Space Gap | 8 | diff --git a/packages/next/docs/components/FormLayout.zh-CN.md b/packages/next/docs/components/FormLayout.zh-CN.md index 159db7c210a..ad7c5fa336b 100644 --- a/packages/next/docs/components/FormLayout.zh-CN.md +++ b/packages/next/docs/components/FormLayout.zh-CN.md @@ -133,7 +133,13 @@ const form = createForm() export default () => ( - + ( ## API -| 属性名 | 类型 | 描述 | 默认值 | -| -------------- | ------------------------------------------- | ----------------------- | ---------- | -| style | CSSProperties | 样式 | - | -| className | string | 类名 | - | -| colon | boolean | 是否有冒号 | true | -| labelAlign | `'right' \| 'left'` | 标签内容对齐 | - | -| wrapperAlign | `'right' \| 'left'` | 组件容器内容对齐 | - | -| labelWrap | boolean | 标签内容换行 | false | -| labelWidth | number | 标签宽度(px) | - | -| wrapperWidth | number | 组件容器宽度(px) | - | -| wrapperWrap | boolean | 组件容器换行 | false | -| labelCol | number | 标签宽度(24 column) | - | -| wrapperCol | number | 组件容器宽度(24 column) | - | -| fullness | boolean | 组件容器宽度 100% | false | -| size | `'small' \| 'default' \| 'large'` | 组件尺寸 | default | -| layout | `'vertical' \| 'horizontal' \| 'inline'` | 布局模式 | horizontal | -| direction | `'rtl' \| 'ltr'` | 方向(暂不支持) | ltr | -| inset | boolean | 内联布局 | false | -| shallow | boolean | 上下文浅层传递 | true | -| feedbackLayout | `'loose' \| 'terse' \| 'popover' \| 'none'` | 反馈布局 | true | -| tooltipLayout | `"icon" \| "text"` | 问号提示布局 | `"icon"` | -| tooltipIcon | ReactNode | 问号提示图标 | - | -| bordered | boolean | 是否有边框 | true | -| gridColumnGap | number | 网格布局列间距 | 8 | -| gridRowGap | number | 网格布局行间距 | 4 | -| spaceGap | number | 弹性间距 | 8 | +| 属性名 | 类型 | 描述 | 默认值 | +| -------------- | ------------------------------------------------------------------------------------- | ----------------------- | ---------- | +| style | CSSProperties | 样式 | - | +| className | string | 类名 | - | +| colon | boolean | 是否有冒号 | true | +| labelAlign | `'right' \| 'left' \| ('right' \| 'left')[]` | 标签内容对齐 | - | +| wrapperAlign | `'right' \| 'left' \| ('right' \| 'left')[]` | 组件容器内容对齐 | - | +| labelWrap | boolean | 标签内容换行 | false | +| labelWidth | number | 标签宽度(px) | - | +| wrapperWidth | number | 组件容器宽度(px) | - | +| wrapperWrap | boolean | 组件容器换行 | false | +| labelCol | `number \| number[]` | 标签宽度(24 column) | - | +| wrapperCol | `number \| number[]` | 组件容器宽度(24 column) | - | +| fullness | boolean | 组件容器宽度 100% | false | +| size | `'small' \| 'default' \| 'large'` | 组件尺寸 | default | +| layout | `'vertical' \| 'horizontal' \| 'inline' \|('vertical' \| 'horizontal' \| 'inline')[]` | 布局模式 | horizontal | +| direction | `'rtl' \| 'ltr'` | 方向(暂不支持) | ltr | +| inset | boolean | 内联布局 | false | +| shallow | boolean | 上下文浅层传递 | true | +| feedbackLayout | `'loose' \| 'terse' \| 'popover' \| 'none'` | 反馈布局 | true | +| tooltipLayout | `"icon" \| "text"` | 问号提示布局 | `"icon"` | +| tooltipIcon | ReactNode | 问号提示图标 | - | +| bordered | boolean | 是否有边框 | true | +| breakpoints | number[] | 容器尺寸断点 | - | +| gridColumnGap | number | 网格布局列间距 | 8 | +| gridRowGap | number | 网格布局行间距 | 4 | +| spaceGap | number | 弹性间距 | 8 | diff --git a/packages/next/src/form-layout/index.tsx b/packages/next/src/form-layout/index.tsx index 8f72173af38..8d1a33cce78 100644 --- a/packages/next/src/form-layout/index.tsx +++ b/packages/next/src/form-layout/index.tsx @@ -1,4 +1,5 @@ import React, { createContext, useContext } from 'react' +import useResponsiveFormLayout from './useResponsiveFormLayout' import { usePrefixCls } from '../__builtins__' import cls from 'classnames' @@ -7,17 +8,21 @@ export interface IFormLayoutProps { className?: string style?: React.CSSProperties colon?: boolean - labelAlign?: 'right' | 'left' - wrapperAlign?: 'right' | 'left' + labelAlign?: 'right' | 'left' | ('right' | 'left')[] + wrapperAlign?: 'right' | 'left' | ('right' | 'left')[] labelWrap?: boolean labelWidth?: number wrapperWidth?: number wrapperWrap?: boolean - labelCol?: number - wrapperCol?: number + labelCol?: number | number[] + wrapperCol?: number | number[] fullness?: boolean size?: 'small' | 'default' | 'large' - layout?: 'vertical' | 'horizontal' | 'inline' + layout?: + | 'vertical' + | 'horizontal' + | 'inline' + | ('vertical' | 'horizontal' | 'inline')[] direction?: 'rtl' | 'ltr' inset?: boolean shallow?: boolean @@ -25,6 +30,7 @@ export interface IFormLayoutProps { tooltipIcon?: React.ReactNode feedbackLayout?: 'loose' | 'terse' | 'popover' | 'none' bordered?: boolean + breakpoints?: number[] gridColumnGap?: number gridRowGap?: number spaceGap?: number @@ -47,7 +53,8 @@ export const FormLayout: React.FC & { useFormLayout: () => IFormLayoutProps useFormDeepLayout: () => IFormLayoutProps useFormShallowLayout: () => IFormLayoutProps -} = ({ shallow, children, prefix, className, style, ...props }) => { +} = ({ shallow, children, prefix, className, style, ...otherProps }) => { + const { ref, props } = useResponsiveFormLayout(otherProps) const deepLayout = useFormDeepLayout() const formPrefixCls = usePrefixCls('form', { prefix }) const layoutPrefixCls = usePrefixCls('formily-layout', { prefix }) @@ -83,7 +90,7 @@ export const FormLayout: React.FC & { ) } return ( -
+
{renderChildren()}
) diff --git a/packages/next/src/form-layout/useResponsiveFormLayout.ts b/packages/next/src/form-layout/useResponsiveFormLayout.ts new file mode 100644 index 00000000000..26fc838c760 --- /dev/null +++ b/packages/next/src/form-layout/useResponsiveFormLayout.ts @@ -0,0 +1,108 @@ +import { useRef, useState, useEffect } from 'react' +import { isArr, isValid } from '@formily/shared' + +interface IProps { + breakpoints?: number[] + layout?: + | 'vertical' + | 'horizontal' + | 'inline' + | ('vertical' | 'horizontal' | 'inline')[] + labelCol?: number | number[] + wrapperCol?: number | number[] + labelAlign?: 'right' | 'left' | ('right' | 'left')[] + wrapperAlign?: 'right' | 'left' | ('right' | 'left')[] + [props: string]: any +} + +interface ICalcBreakpointIndex { + (originalBreakpoints: number[], width: number): number +} + +interface ICalculateProps { + (target: HTMLElement, props: IProps): IProps +} + +interface IUseResponsiveFormLayout { + (props: IProps): { + ref: React.MutableRefObject + props: IProps + } +} + +const calcBreakpointIndex: ICalcBreakpointIndex = (breakpoints, width) => { + for (let i = 0; i < breakpoints.length; i++) { + if (width <= breakpoints[i]) { + return i + } + } +} + +const calcFactor = (value: T | T[], breakpointIndex: number): T => { + if (Array.isArray(value)) { + if (breakpointIndex === -1) return value[0] + return value[breakpointIndex] ?? value[value.length - 1] + } else { + return value + } +} + +const factor = (value: T | T[], breakpointIndex: number): T => + isValid(value) ? calcFactor(value as any, breakpointIndex) : value + +const calculateProps: ICalculateProps = (target, props) => { + const { clientWidth } = target + const { + breakpoints, + layout, + labelAlign, + wrapperAlign, + labelCol, + wrapperCol, + ...otherProps + } = props + const breakpointIndex = calcBreakpointIndex(breakpoints, clientWidth) + + return { + layout: factor(layout, breakpointIndex), + labelAlign: factor(labelAlign, breakpointIndex), + wrapperAlign: factor(wrapperAlign, breakpointIndex), + labelCol: factor(labelCol, breakpointIndex), + wrapperCol: factor(wrapperCol, breakpointIndex), + ...otherProps, + } +} + +const useResponsiveFormLayout: IUseResponsiveFormLayout = (props) => { + const ref = useRef(null) + const { breakpoints } = props + if (!isArr(breakpoints)) { + return { ref, props } + } + const [layoutProps, setLayout] = useState({}) + + const updateUI = () => { + setLayout(calculateProps(ref.current, props)) + } + + useEffect(() => { + const observer = () => { + updateUI() + } + const resizeObserver = new ResizeObserver(observer) + if (ref.current) { + resizeObserver.observe(ref.current) + } + updateUI() + return () => { + resizeObserver.disconnect() + } + }, []) + + return { + ref, + props: layoutProps, + } +} + +export default useResponsiveFormLayout