diff --git a/components/_util/canUseDom.ts b/components/_util/canUseDom.ts new file mode 100644 index 000000000..39705dc74 --- /dev/null +++ b/components/_util/canUseDom.ts @@ -0,0 +1,5 @@ +function canUseDom() { + return !!(typeof window !== 'undefined' && window.document && window.document.createElement); +} + +export default canUseDom; diff --git a/components/_util/hooks/useFlexGapSupport.ts b/components/_util/hooks/useFlexGapSupport.ts new file mode 100644 index 000000000..eb3c100ec --- /dev/null +++ b/components/_util/hooks/useFlexGapSupport.ts @@ -0,0 +1,11 @@ +import { onMounted, ref } from 'vue'; +import { detectFlexGapSupported } from '../styleChecker'; + +export default () => { + const flexible = ref(false); + onMounted(() => { + flexible.value = detectFlexGapSupported(); + }); + + return flexible; +}; diff --git a/components/_util/styleChecker.ts b/components/_util/styleChecker.ts index 6554faea8..ae845c67e 100644 --- a/components/_util/styleChecker.ts +++ b/components/_util/styleChecker.ts @@ -1,5 +1,9 @@ -const isStyleSupport = (styleName: string | Array<string>): boolean => { - if (typeof window !== 'undefined' && window.document && window.document.documentElement) { +import canUseDom from './canUseDom'; + +export const canUseDocElement = () => canUseDom() && window.document.documentElement; + +export const isStyleSupport = (styleName: string | Array<string>): boolean => { + if (canUseDocElement()) { const styleNameList = Array.isArray(styleName) ? styleName : [styleName]; const { documentElement } = window.document; @@ -8,6 +12,32 @@ const isStyleSupport = (styleName: string | Array<string>): boolean => { return false; }; -export const isFlexSupported = isStyleSupport(['flex', 'webkitFlex', 'Flex', 'msFlex']); +let flexGapSupported: boolean | undefined; +export const detectFlexGapSupported = () => { + if (!canUseDocElement()) { + return false; + } + + if (flexGapSupported !== undefined) { + return flexGapSupported; + } + + // create flex container with row-gap set + const flex = document.createElement('div'); + flex.style.display = 'flex'; + flex.style.flexDirection = 'column'; + flex.style.rowGap = '1px'; + + // create two, elements inside it + flex.appendChild(document.createElement('div')); + flex.appendChild(document.createElement('div')); + + // append to the DOM (needed to obtain scrollHeight) + document.body.appendChild(flex); + flexGapSupported = flex.scrollHeight === 1; // flex container should be 1px high from the row-gap + document.body.removeChild(flex); + + return flexGapSupported; +}; export default isStyleSupport; diff --git a/components/grid/Col.tsx b/components/grid/Col.tsx index d1b0ee783..35ee71d82 100644 --- a/components/grid/Col.tsx +++ b/components/grid/Col.tsx @@ -1,8 +1,9 @@ -import { inject, defineComponent, HTMLAttributes, CSSProperties } from 'vue'; +import { inject, defineComponent, CSSProperties, ExtractPropTypes, computed } from 'vue'; import classNames from '../_util/classNames'; import PropTypes from '../_util/vue-types'; -import { defaultConfigProvider } from '../config-provider'; import { rowContextState } from './Row'; +import useConfigInject from '../_util/hooks/useConfigInject'; +import { useInjectRow } from './context'; type ColSpanType = number | string; @@ -16,22 +17,6 @@ export interface ColSize { pull?: ColSpanType; } -export interface ColProps extends HTMLAttributes { - span?: ColSpanType; - order?: ColSpanType; - offset?: ColSpanType; - push?: ColSpanType; - pull?: ColSpanType; - xs?: ColSpanType | ColSize; - sm?: ColSpanType | ColSize; - md?: ColSpanType | ColSize; - lg?: ColSpanType | ColSize; - xl?: ColSpanType | ColSize; - xxl?: ColSpanType | ColSize; - prefixCls?: string; - flex?: FlexType; -} - function parseFlex(flex: FlexType): string { if (typeof flex === 'number') { return `${flex} ${flex} auto`; @@ -44,92 +29,17 @@ function parseFlex(flex: FlexType): string { return flex; } -const ACol = defineComponent<ColProps>({ - name: 'ACol', - setup(props, { slots }) { - const configProvider = inject('configProvider', defaultConfigProvider); - const rowContext = inject<rowContextState>('rowContext', {}); - - return () => { - const { gutter } = rowContext; - const { prefixCls: customizePrefixCls, span, order, offset, push, pull, flex } = props; - const prefixCls = configProvider.getPrefixCls('col', customizePrefixCls); - let sizeClassObj = {}; - ['xs', 'sm', 'md', 'lg', 'xl', 'xxl'].forEach(size => { - let sizeProps: ColSize = {}; - const propSize = props[size]; - if (typeof propSize === 'number') { - sizeProps.span = propSize; - } else if (typeof propSize === 'object') { - sizeProps = propSize || {}; - } - - sizeClassObj = { - ...sizeClassObj, - [`${prefixCls}-${size}-${sizeProps.span}`]: sizeProps.span !== undefined, - [`${prefixCls}-${size}-order-${sizeProps.order}`]: - sizeProps.order || sizeProps.order === 0, - [`${prefixCls}-${size}-offset-${sizeProps.offset}`]: - sizeProps.offset || sizeProps.offset === 0, - [`${prefixCls}-${size}-push-${sizeProps.push}`]: sizeProps.push || sizeProps.push === 0, - [`${prefixCls}-${size}-pull-${sizeProps.pull}`]: sizeProps.pull || sizeProps.pull === 0, - }; - }); - const classes = classNames( - prefixCls, - { - [`${prefixCls}-${span}`]: span !== undefined, - [`${prefixCls}-order-${order}`]: order, - [`${prefixCls}-offset-${offset}`]: offset, - [`${prefixCls}-push-${push}`]: push, - [`${prefixCls}-pull-${pull}`]: pull, - }, - sizeClassObj, - ); - let mergedStyle: CSSProperties = {}; - if (gutter) { - mergedStyle = { - ...(gutter[0] > 0 - ? { - paddingLeft: `${gutter[0] / 2}px`, - paddingRight: `${gutter[0] / 2}px`, - } - : {}), - ...(gutter[1] > 0 - ? { - paddingTop: `${gutter[1] / 2}px`, - paddingBottom: `${gutter[1] / 2}px`, - } - : {}), - ...mergedStyle, - }; - } - if (flex) { - mergedStyle.flex = parseFlex(flex); - } - - return ( - <div class={classes} style={mergedStyle}> - {slots.default?.()} - </div> - ); - }; - }, -}); - const stringOrNumber = PropTypes.oneOfType([PropTypes.string, PropTypes.number]); - -export const ColSize = PropTypes.shape({ +export const colSize = PropTypes.shape({ span: stringOrNumber, order: stringOrNumber, offset: stringOrNumber, push: stringOrNumber, pull: stringOrNumber, }).loose; +const objectOrNumber = PropTypes.oneOfType([PropTypes.string, PropTypes.number, colSize]); -const objectOrNumber = PropTypes.oneOfType([PropTypes.string, PropTypes.number, ColSize]); - -ACol.props = { +const colProps = { span: stringOrNumber, order: stringOrNumber, offset: stringOrNumber, @@ -145,4 +55,85 @@ ACol.props = { flex: stringOrNumber, }; -export default ACol; +export type ColProps = Partial<ExtractPropTypes<typeof colProps>>; + +export default defineComponent({ + name: 'ACol', + props: colProps, + setup(props, { slots }) { + const { gutter, supportFlexGap, wrap } = useInjectRow(); + const { prefixCls, direction } = useConfigInject('col', props); + const classes = computed(() => { + const { span, order, offset, push, pull } = props; + const pre = prefixCls.value; + let sizeClassObj = {}; + ['xs', 'sm', 'md', 'lg', 'xl', 'xxl'].forEach(size => { + let sizeProps: ColSize = {}; + const propSize = props[size]; + if (typeof propSize === 'number') { + sizeProps.span = propSize; + } else if (typeof propSize === 'object') { + sizeProps = propSize || {}; + } + + sizeClassObj = { + ...sizeClassObj, + [`${pre}-${size}-${sizeProps.span}`]: sizeProps.span !== undefined, + [`${pre}-${size}-order-${sizeProps.order}`]: sizeProps.order || sizeProps.order === 0, + [`${pre}-${size}-offset-${sizeProps.offset}`]: sizeProps.offset || sizeProps.offset === 0, + [`${pre}-${size}-push-${sizeProps.push}`]: sizeProps.push || sizeProps.push === 0, + [`${pre}-${size}-pull-${sizeProps.pull}`]: sizeProps.pull || sizeProps.pull === 0, + [`${pre}-rtl`]: direction.value === 'rtl', + }; + }); + return classNames( + pre, + { + [`${pre}-${span}`]: span !== undefined, + [`${pre}-order-${order}`]: order, + [`${pre}-offset-${offset}`]: offset, + [`${pre}-push-${push}`]: push, + [`${pre}-pull-${pull}`]: pull, + }, + sizeClassObj, + ); + }); + + const mergedStyle = computed(() => { + const { flex } = props; + const gutterVal = gutter.value; + let style: CSSProperties = {}; + // Horizontal gutter use padding + if (gutterVal && gutterVal[0] > 0) { + const horizontalGutter = `${gutterVal[0] / 2}px`; + style.paddingLeft = horizontalGutter; + style.paddingRight = horizontalGutter; + } + + // Vertical gutter use padding when gap not support + if (gutterVal && gutterVal[1] > 0 && !supportFlexGap.value) { + const verticalGutter = `${gutterVal[1] / 2}px`; + style.paddingTop = verticalGutter; + style.paddingBottom = verticalGutter; + } + + if (flex) { + style.flex = parseFlex(flex); + + // Hack for Firefox to avoid size issue + // https://github.com/ant-design/ant-design/pull/20023#issuecomment-564389553 + if (flex === 'auto' && wrap.value === false && !style.minWidth) { + style.minWidth = 0; + } + } + return style; + }); + return () => { + return ( + <div class={classes.value} style={mergedStyle.value}> + {slots.default?.()} + </div> + ); + }; + }, +}); diff --git a/components/grid/Row.tsx b/components/grid/Row.tsx index e038db608..3b38b66c1 100644 --- a/components/grid/Row.tsx +++ b/components/grid/Row.tsx @@ -1,22 +1,23 @@ import { - inject, - provide, - reactive, defineComponent, - HTMLAttributes, ref, onMounted, onBeforeUnmount, + ExtractPropTypes, + computed, + CSSProperties, } from 'vue'; import classNames from '../_util/classNames'; import { tuple } from '../_util/type'; import PropTypes from '../_util/vue-types'; -import { defaultConfigProvider } from '../config-provider'; import ResponsiveObserve, { Breakpoint, ScreenMap, responsiveArray, } from '../_util/responsiveObserve'; +import useConfigInject from '../_util/hooks/useConfigInject'; +import useFlexGapSupport from '../_util/hooks/useFlexGapSupport'; +import useProvideRow from './context'; const RowAligns = tuple('top', 'middle', 'bottom', 'stretch'); const RowJustify = tuple('start', 'end', 'center', 'space-around', 'space-between'); @@ -27,24 +28,36 @@ export interface rowContextState { gutter?: [number, number]; } -export interface RowProps extends HTMLAttributes { - type?: 'flex'; - gutter?: Gutter | [Gutter, Gutter]; - align?: typeof RowAligns[number]; - justify?: typeof RowJustify[number]; - prefixCls?: string; -} +const rowProps = { + type: PropTypes.oneOf(['flex']), + align: PropTypes.oneOf(RowAligns), + justify: PropTypes.oneOf(RowJustify), + prefixCls: PropTypes.string, + gutter: PropTypes.oneOfType([PropTypes.object, PropTypes.number, PropTypes.array]).def(0), + wrap: PropTypes.looseBool, +}; -const ARow = defineComponent<RowProps>({ +export type RowProps = Partial<ExtractPropTypes<typeof rowProps>>; + +const ARow = defineComponent({ name: 'ARow', + props: rowProps, setup(props, { slots }) { - const rowContext = reactive<rowContextState>({ - gutter: undefined, - }); - provide('rowContext', rowContext); + const { prefixCls, direction } = useConfigInject('row', props); let token: number; + const screens = ref<ScreenMap>({ + xs: true, + sm: true, + md: true, + lg: true, + xl: true, + xxl: true, + }); + + const supportFlexGap = useFlexGapSupport(); + onMounted(() => { token = ResponsiveObserve.subscribe(screen => { const currentGutter = props.gutter || 0; @@ -62,18 +75,7 @@ const ARow = defineComponent<RowProps>({ ResponsiveObserve.unsubscribe(token); }); - const screens = ref<ScreenMap>({ - xs: true, - sm: true, - md: true, - lg: true, - xl: true, - xxl: true, - }); - - const { getPrefixCls } = inject('configProvider', defaultConfigProvider); - - const getGutter = (): [number, number] => { + const gutter = computed(() => { const results: [number, number] = [0, 0]; const { gutter = 0 } = props; const normalizedGutter = Array.isArray(gutter) ? gutter : [gutter, 0]; @@ -91,34 +93,48 @@ const ARow = defineComponent<RowProps>({ } }); return results; - }; + }); + + useProvideRow({ + gutter, + supportFlexGap, + wrap: computed(() => props.wrap), + }); + + const classes = computed(() => + classNames(prefixCls.value, { + [`${prefixCls.value}-no-wrap`]: props.wrap === false, + [`${prefixCls.value}-${props.justify}`]: props.justify, + [`${prefixCls.value}-${props.align}`]: props.align, + [`${prefixCls.value}-rtl`]: direction.value === 'rtl', + }), + ); + + const rowStyle = computed(() => { + const gt = gutter.value; + // Add gutter related style + const style: CSSProperties = {}; + const horizontalGutter = gt[0] > 0 ? `${gt[0] / -2}px` : undefined; + const verticalGutter = gt[1] > 0 ? `${gt[1] / -2}px` : undefined; + + if (horizontalGutter) { + style.marginLeft = horizontalGutter; + style.marginRight = horizontalGutter; + } + + if (supportFlexGap.value) { + // Set gap direct if flex gap support + style.rowGap = `${gt[1]}px`; + } else if (verticalGutter) { + style.marginTop = verticalGutter; + style.marginBottom = verticalGutter; + } + return style; + }); return () => { - const { prefixCls: customizePrefixCls, justify, align } = props; - const prefixCls = getPrefixCls('row', customizePrefixCls); - const gutter = getGutter(); - const classes = classNames(prefixCls, { - [`${prefixCls}-${justify}`]: justify, - [`${prefixCls}-${align}`]: align, - }); - const rowStyle = { - ...(gutter[0] > 0 - ? { - marginLeft: `${gutter[0] / -2}px`, - marginRight: `${gutter[0] / -2}px`, - } - : {}), - ...(gutter[1] > 0 - ? { - marginTop: `${gutter[1] / -2}px`, - marginBottom: `${gutter[1] / -2}px`, - } - : {}), - }; - - rowContext.gutter = gutter; return ( - <div class={classes} style={rowStyle}> + <div class={classes.value} style={rowStyle.value}> {slots.default?.()} </div> ); @@ -126,12 +142,4 @@ const ARow = defineComponent<RowProps>({ }, }); -ARow.props = { - type: PropTypes.oneOf(['flex']), - align: PropTypes.oneOf(RowAligns), - justify: PropTypes.oneOf(RowJustify), - prefixCls: PropTypes.string, - gutter: PropTypes.oneOfType([PropTypes.object, PropTypes.number, PropTypes.array]).def(0), -}; - export default ARow; diff --git a/components/grid/context.ts b/components/grid/context.ts new file mode 100644 index 000000000..38fb8a174 --- /dev/null +++ b/components/grid/context.ts @@ -0,0 +1,20 @@ +import { Ref, inject, InjectionKey, provide, ComputedRef } from 'vue'; + +export interface RowContext { + gutter: ComputedRef<[number, number]>; + wrap: ComputedRef<boolean>; + supportFlexGap: Ref<boolean>; +} + +export const RowContextKey: InjectionKey<RowContext> = Symbol('rowContextKey'); + +const useProvideRow = (state: RowContext) => { + provide(RowContextKey, state); +}; + +const useInjectRow = () => { + return inject(RowContextKey); +}; + +export { useInjectRow, useProvideRow }; +export default useProvideRow; diff --git a/components/grid/index.ts b/components/grid/index.ts index 8b2900f88..d8eddbf16 100644 --- a/components/grid/index.ts +++ b/components/grid/index.ts @@ -1,4 +1,11 @@ import Row from './Row'; import Col from './Col'; +import useBreakpoint from '../_util/hooks/useBreakpoint'; + +export { RowProps } from './Row'; + +export { ColProps, ColSize } from './Col'; export { Row, Col }; + +export default { useBreakpoint }; diff --git a/components/grid/style/index.less b/components/grid/style/index.less index 490d959b5..67b091844 100644 --- a/components/grid/style/index.less +++ b/components/grid/style/index.less @@ -11,6 +11,11 @@ &::after { display: flex; } + + // No wrap of flex + &-no-wrap { + flex-wrap: nowrap; + } } // x轴原点 diff --git a/v2-doc b/v2-doc index a7013ae87..0f6d531d0 160000 --- a/v2-doc +++ b/v2-doc @@ -1 +1 @@ -Subproject commit a7013ae87f69dcbcf547f4b023255b8a7a775557 +Subproject commit 0f6d531d088d5283250c8cec1c7e8be0e0d36a36 diff --git a/v3-changelog.md b/v3-changelog.md index e69de29bb..cbdd025c7 100644 --- a/v3-changelog.md +++ b/v3-changelog.md @@ -0,0 +1,3 @@ +## grid + +破坏性更新:row gutter 支持 row-wrap, 无需使用多个 row 划分 col