feat: add space

This commit is contained in:
aibayanyu 2023-04-17 08:08:22 +08:00
parent e5896c093a
commit 27f2980768
11 changed files with 431 additions and 52 deletions

View File

@ -0,0 +1,12 @@
import { useState } from '@v-c/utils'
import { onMounted } from 'vue'
import { detectFlexGapSupported } from '../style-checker'
export default () => {
const [flexible, setFlexible] = useState(false)
onMounted(() => {
setFlexible(detectFlexGapSupported())
})
return flexible
}

View File

@ -0,0 +1,32 @@
import { canUseDom } from '@v-c/utils'
export const canUseDocElement = () =>
canUseDom() && window.document.documentElement
let flexGapSupported: boolean
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
}

View File

@ -53,9 +53,13 @@ const Button = defineComponent({
direction direction
) )
const sizeCls = computed(() => { const sizeCls = computed(() => {
const sizeClassNameMap = { large: 'lg', small: 'sm', middle: undefined } const sizeClassNameMap: Record<string, any> = {
large: 'lg',
small: 'sm',
middle: undefined
}
const sizeFullname = compactSize?.value || size.value const sizeFullname = compactSize?.value || size.value
return sizeClassNameMap[sizeFullname!] return sizeClassNameMap[sizeFullname]
}) })
const disabled = useDisabled(props) const disabled = useDisabled(props)
const buttonRef = shallowRef<HTMLButtonElement | null>(null) const buttonRef = shallowRef<HTMLButtonElement | null>(null)

View File

@ -14,12 +14,12 @@ title: Type
There are `primary` button, `default` button, `dashed` button, `text` button and `link` button in antd. There are `primary` button, `default` button, `dashed` button, `text` button and `link` button in antd.
</docs> </docs>
<script setup lang="ts"></script>
<template> <template>
<a-button type="primary">Primary Button</a-button> <a-space wrap>
<a-button>Default Button</a-button> <a-button type="primary">Primary Button</a-button>
<a-button type="dashed">Dashed Button</a-button> <a-button>Default Button</a-button>
<a-button type="text">Text Button</a-button> <a-button type="dashed">Dashed Button</a-button>
<a-button type="link">Link Button</a-button> <a-button type="text">Text Button</a-button>
<a-button type="link">Link Button</a-button>
</a-space>
</template> </template>

View File

@ -49,7 +49,6 @@ It accepts all props which native buttons support.
## Design Token ## Design Token
<ComponentTokenTable component="Button"></ComponentTokenTable>
## FAQ ## FAQ

View File

@ -54,7 +54,6 @@
## Design Token ## Design Token
<ComponentTokenTable component="Button"></ComponentTokenTable>
## FAQ ## FAQ

View File

@ -1,2 +1,3 @@
export { default as Button } from './button' export { default as Button } from './button'
export { default as ConfigProvider } from './config-provider' export { default as ConfigProvider } from './config-provider'
export { default as Space } from './space'

View File

@ -1,41 +0,0 @@
import { classNames, createInjectionState } from '@v-c/utils'
import type { Ref } from 'vue'
import { computed } from 'vue'
import type { DirectionType } from '../config-provider'
const spaceCompactItem = () => {
return {
compactDirection: computed(() => null),
isFirstItem: computed(() => null),
isLastItem: computed(() => null),
compactSize: computed(() => null)
}
}
export const [useSpaceCompactProvider, useSpaceCompactInject] = createInjectionState(spaceCompactItem)
export const useSpaceCompactItemState = (): ReturnType<typeof spaceCompactItem> | undefined => useSpaceCompactInject()
export const useCompactItemContext = (prefixCls: Ref<string>, direction: Ref<DirectionType>) => {
const compactItemContext = useSpaceCompactItemState()
const compactItemClassnames = computed(() => {
if (!compactItemContext) return ''
const { compactDirection, isFirstItem, isLastItem } = compactItemContext
const separator = compactDirection.value === 'vertical' ? '-vertical-' : '-'
return classNames({
[`${prefixCls.value}-compact${separator}item`]: true,
[`${prefixCls.value}-compact${separator}first-item`]: isFirstItem.value,
[`${prefixCls.value}-compact${separator}last-item`]: isLastItem.value,
[`${prefixCls.value}-compact${separator}item-rtl`]: direction.value === 'rtl'
})
})
return {
compactSize: compactItemContext?.compactSize,
compactDirection: compactItemContext?.compactDirection,
compactItemClassnames
}
}

View File

@ -0,0 +1,142 @@
import {
anyType,
booleanType,
classNames,
createInjectionState,
filterEmpty,
isObject,
stringType
} from '@v-c/utils'
import type { Ref } from 'vue'
import { computed, defineComponent } from 'vue'
import type { DirectionType } from '../config-provider'
import type { SizeType } from '../config-provider/context'
import { useProviderConfigState } from '../config-provider/context'
import useStyle from './style'
const spaceCompactItem = (props: any) => {
return {
compactDirection: computed(() => props.compactDirection),
isFirstItem: computed(() => props.isFirstItem),
isLastItem: computed(() => props.isLastItem),
compactSize: computed(() => props.compactSize)
}
}
export const [useSpaceCompactProvider, useSpaceCompactInject] =
createInjectionState(spaceCompactItem)
export const useSpaceCompactItemState = ():
| ReturnType<typeof spaceCompactItem>
| undefined => useSpaceCompactInject()
export const useCompactItemContext = (
prefixCls: Ref<string>,
direction: Ref<DirectionType>
) => {
const compactItemContext = useSpaceCompactItemState()
const compactItemClassnames = computed(() => {
if (!compactItemContext) return ''
const { compactDirection, isFirstItem, isLastItem } = compactItemContext
const separator = compactDirection.value === 'vertical' ? '-vertical-' : '-'
return classNames({
[`${prefixCls.value}-compact${separator}item`]: true,
[`${prefixCls.value}-compact${separator}first-item`]: isFirstItem.value,
[`${prefixCls.value}-compact${separator}last-item`]: isLastItem.value,
[`${prefixCls.value}-compact${separator}item-rtl`]:
direction.value === 'rtl'
})
})
return {
compactSize: compactItemContext?.compactSize,
compactDirection: compactItemContext?.compactDirection,
compactItemClassnames
}
}
const CompactItem = defineComponent({
name: 'CompactItem',
inheritAttrs: false,
props: {
compactSize: anyType<SizeType>(),
compactDirection: stringType<'horizontal' | 'vertical'>(),
isFirstItem: booleanType(),
isLastItem: booleanType()
},
setup(props, { slots }) {
useSpaceCompactProvider(props)
return () => slots.default?.()
}
})
export const spaceCompactProps = {
prefixCls: stringType(),
size: anyType<SizeType>('middle'),
direction: stringType<'horizontal' | 'vertical'>(),
block: anyType<boolean>(),
rootClassName: stringType()
}
const Compact = defineComponent({
name: 'Compact',
inheritAttrs: false,
props: spaceCompactProps,
setup(props, { slots, attrs }) {
const { getPrefixCls, direction: directionConfig } =
useProviderConfigState()
const prefixCls = computed(() =>
getPrefixCls('space-compact', props.prefixCls)
)
const [wrapSSR, hashId] = useStyle(prefixCls)
const compactItemContext = useSpaceCompactItemState()
return () => {
const childNodes = filterEmpty(slots.default?.())
if (childNodes.length === 0) return null
const { rootClassName, size, direction } = props
const cls = classNames(
prefixCls.value,
hashId.value,
{
[`${prefixCls.value}-rtl`]: directionConfig.value === 'rtl',
[`${prefixCls.value}-block`]: props.block,
[`${prefixCls.value}-vertical`]: props.direction === 'vertical'
},
attrs.class,
rootClassName
)
const nodes = childNodes.map((child, index) => {
const key =
(isObject(child) && (child as any).key) ||
`${prefixCls.value}-item-${index}`
return (
<CompactItem
key={key}
compactSize={size}
compactDirection={direction}
isFirstItem={
(index === 0 && !compactItemContext) ||
compactItemContext?.isFirstItem.value
}
isLastItem={
index === childNodes.length - 1 &&
(!compactItemContext || compactItemContext?.isLastItem.value)
}
>
{child}
</CompactItem>
)
})
return wrapSSR(
<div {...attrs} class={cls}>
{nodes}
</div>
)
}
}
})
export default Compact

157
components/space/index.tsx Normal file
View File

@ -0,0 +1,157 @@
import {
anyType,
booleanType,
classNames,
createInjectionState,
filterEmpty,
isObject,
stringType,
vNodeType
} from '@v-c/utils'
import type { App, CSSProperties } from 'vue'
import { computed, defineComponent, shallowRef } from 'vue'
import type { SizeType } from '../config-provider/context'
import { useProviderConfigState } from '../config-provider/context'
import useFlexGapSupport from '../_util/hooks/flex-gap-support'
import useStyle from './style'
import Item from './item'
import Compact from './compact'
export type SpaceSize = SizeType | number
const spaceState = function ({ sizes, supportFlexGap, latestIndex }: any) {
return {
latestIndex: computed(() => latestIndex.value),
horizontalSize: computed(() => sizes[0]),
verticalSize: computed(() => sizes[1]),
supportFlexGap: computed(() => supportFlexGap.value)
}
}
export const [useSpaceProvider, useSpaceInject] =
createInjectionState(spaceState)
export const useSpaceContextState = () =>
useSpaceInject() ?? {
latestIndex: computed(() => 0),
horizontalSize: computed(() => 0),
verticalSize: computed(() => 0),
supportFlexGap: computed(() => false)
}
export const spaceProps = {
prefixCls: stringType(),
rootClassName: stringType(),
size: anyType<SizeType | [SpaceSize, SpaceSize]>('small'),
direction: anyType<'horizontal' | 'vertical'>('horizontal'),
align: stringType<'start' | 'end' | 'center' | 'baseline'>(),
split: vNodeType(),
wrap: booleanType(false)
}
const spaceSize = {
small: 8,
middle: 16,
large: 24
}
function getNumberSize(size: SpaceSize) {
return typeof size === 'string' ? spaceSize[size] : size || 0
}
const Space = defineComponent({
name: 'ASpace',
inheritAttrs: false,
props: spaceProps,
setup(props, { attrs, slots }) {
const { getPrefixCls, direction: directionConfig } =
useProviderConfigState()
const supportFlexGap = useFlexGapSupport()
const prefixCls = computed(() => getPrefixCls('space', props.prefixCls))
const [wrapSSR, hashId] = useStyle(prefixCls)
const sizes = computed<[SpaceSize, SpaceSize]>(() => {
const { size } = props
if (Array.isArray(size)) {
return size.map(getNumberSize) as [SpaceSize, SpaceSize]
}
return [getNumberSize(size), getNumberSize(size)] as [
SpaceSize,
SpaceSize
]
})
const latestIndex = shallowRef(0)
useSpaceProvider({ sizes, supportFlexGap, latestIndex })
return () => {
const { align, direction, rootClassName, split, wrap } = props
const childNodes = filterEmpty(slots.default?.())
const mergedAlign =
align === undefined && direction === 'horizontal' ? 'center' : align
const cn = classNames(
prefixCls.value,
hashId.value,
`${prefixCls.value}-${direction}`,
{
[`${prefixCls.value}-rtl`]: directionConfig.value === 'rtl',
[`${prefixCls.value}-align-${mergedAlign}`]: mergedAlign
},
attrs.class,
rootClassName
)
const itemClassName = `${prefixCls.value}-item`
const marginDirection =
directionConfig.value === 'rtl' ? 'marginLeft' : 'marginRight'
const nodes = childNodes.map((child, i) => {
if (child !== null && child !== undefined) {
latestIndex.value = i
}
const key =
(isObject(child) && (child as any).key) || `${itemClassName}-${i}`
return (
<Item
class={itemClassName}
key={key}
direction={direction}
index={i}
marginDirection={marginDirection}
split={split}
wrap={wrap}
>
{child}
</Item>
)
})
// =========================== Render ===========================
if (childNodes.length === 0) {
return null
}
const gapStyle: CSSProperties = {}
if (wrap) {
gapStyle.flexWrap = 'wrap'
if (!supportFlexGap.value) {
gapStyle.marginBottom = `-${sizes.value[1]}px`
}
}
if (supportFlexGap.value) {
gapStyle.columnGap = `${sizes.value[0]}px`
gapStyle.rowGap = `${sizes.value[1]}px`
}
return wrapSSR(
<div {...attrs} class={cn} style={[gapStyle, (attrs as any).style]}>
{nodes}
</div>
)
}
}
})
Space.install = function (app: App) {
app.component('ASpace', Space)
}
Space.Compact = Compact
export default Space as typeof Space &
Plugin & {
readonly Compact: typeof Compact
}

74
components/space/item.tsx Normal file
View File

@ -0,0 +1,74 @@
import {
booleanType,
filterEmpty,
numberType,
someType,
stringType,
vNodeType
} from '@v-c/utils'
import type { CSSProperties, ExtractPropTypes, VNodeChild } from 'vue'
import { defineComponent } from 'vue'
import { useSpaceContextState } from './index'
export const itemProps = {
className: stringType(),
children: vNodeType(),
index: numberType(),
direction: stringType<'horizontal' | 'vertical'>(),
marginDirection: stringType<'marginLeft' | 'marginRight'>(),
split: someType<string | (() => VNodeChild) | VNodeChild>([
String,
Function,
Object
]),
wrap: booleanType()
}
export type ItemProps = ExtractPropTypes<typeof itemProps>
const Item = defineComponent({
name: 'VcSpaceItem',
props: itemProps,
setup(props, { attrs, slots }) {
const { supportFlexGap, latestIndex, verticalSize, horizontalSize } =
useSpaceContextState()
return () => {
const { direction, index, marginDirection, split, wrap } = props
const children = slots.default?.()
if (!children || filterEmpty(children).length === 0) {
return null
}
let style: CSSProperties = {}
if (!supportFlexGap.value) {
if (direction === 'vertical') {
if (index < latestIndex.value) {
style.marginBottom = `${horizontalSize.value / (split ? 2 : 1)}px`
}
} else {
style = {
...(index < latestIndex.value &&
({
[marginDirection]: `${horizontalSize.value / (split ? 2 : 1)}px`
} as CSSProperties)),
...(wrap && { paddingBottom: `${verticalSize.value}px` })
}
}
}
return (
<>
<div class={attrs.class} style={style}>
{children}
</div>
{index < latestIndex.value && split && (
<span class={`${attrs.class}-split`} style={style}>
{split}
</span>
)}
</>
)
}
}
})
export default Item