Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions packages/ui/src/components/space/CompactItem.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<script setup lang="ts">
import { provide, inject, computed } from 'vue'
import { spaceCompactItemContextKey, spaceCompactContextKey } from './types'
import type { SizeType, SpaceCompactItemContext } from './types'

const props = defineProps<{
compactSize?: SizeType
compactDirection?: 'horizontal' | 'vertical'
isFirstItem: boolean
isLastItem: boolean
}>()

// Detect if we're inside a nested SpaceCompact
const hasParentCompact = inject(spaceCompactContextKey, false)

// If nested, inherit isFirstItem/isLastItem from parent context
const parentContext = inject<SpaceCompactItemContext | undefined>(spaceCompactItemContextKey, undefined)

// Use parent's isFirstItem/isLastItem if nested, otherwise use own props
const resolvedIsFirstItem = computed(() =>
hasParentCompact && parentContext ? parentContext.isFirstItem : props.isFirstItem,
)
const resolvedIsLastItem = computed(() =>
hasParentCompact && parentContext ? parentContext.isLastItem : props.isLastItem,
)

// Provide context for child components (Button, Input, Select etc.)
provide(spaceCompactItemContextKey, {
get compactSize() {
return props.compactSize
},
get compactDirection() {
return props.compactDirection
},
get isFirstItem() {
return resolvedIsFirstItem.value
},
get isLastItem() {
return resolvedIsLastItem.value
},
})
</script>

<template>
<div class="ant-space-compact-item">
<slot />
</div>
</template>
65 changes: 52 additions & 13 deletions packages/ui/src/components/space/Space.vue
Original file line number Diff line number Diff line change
@@ -1,37 +1,60 @@
<script setup lang="ts">
import { computed, useSlots } from 'vue'
import { computed, useSlots, useAttrs, type CSSProperties } from 'vue'
import type { SpaceProps, SpaceSlots, SpaceSize, SpaceSizePreset } from './types'
import { spaceDefaultProps, SPACE_SIZE_MAP } from './types'
import { spaceDefaultProps, SPACE_SIZE_MAP, GLOBAL_SIZE_MAP, filterEmpty } from './types'
import { useConfigInject } from '@/hooks'

defineOptions({ name: 'ASpace' })
defineOptions({ name: 'ASpace', inheritAttrs: false })
const props = withDefaults(defineProps<SpaceProps>(), spaceDefaultProps)
defineSlots<SpaceSlots>()

const slots = useSlots()
const attrs = useAttrs()

// Support global size from ConfigProvider and RTL direction
const { size: globalSize, direction: rtlDirection } = useConfigInject()

function resolveSize(size: SpaceSize): number {
function resolveSize(size: SpaceSize | undefined): number {
if (size === undefined) return 0
return typeof size === 'string' ? SPACE_SIZE_MAP[size as SpaceSizePreset] ?? 0 : size
}

// Merge prop size with global size (prop takes precedence)
const mergedSize = computed(() => {
if (props.size !== undefined) return props.size
const global = globalSize.value
// Only inherit non-default ConfigProvider size (default 'md' should not override Space's 'small' default)
if (typeof global === 'string' && global !== 'md' && global in GLOBAL_SIZE_MAP) {
return GLOBAL_SIZE_MAP[global]!
}
return 'small'
})

const gap = computed(() => {
if (Array.isArray(props.size)) {
return [resolveSize(props.size[0]), resolveSize(props.size[1])] as [number, number]
const size = mergedSize.value
if (Array.isArray(size)) {
return [resolveSize(size[0]), resolveSize(size[1])] as [number, number]
}
const s = resolveSize(props.size!)
const s = resolveSize(size as SpaceSize)
return [s, s] as [number, number]
})

const hasSplit = computed(() => !!slots.split)

// Add RTL support
const isRtl = computed(() => rtlDirection.value === 'rtl')

const classes = computed(() => ({
'ant-space': true,
[`ant-space-${props.direction}`]: true,
'ant-space-rtl': isRtl.value,
'ant-space-align-center': !props.align && props.direction === 'horizontal',
[`ant-space-align-${props.align}`]: !!props.align,
}))

const containerStyle = computed(() => {
const [h, v] = gap.value
const style: Record<string, string> = {}
const style: CSSProperties = {}

if (!hasSplit.value) {
style.columnGap = `${h}px`
Expand All @@ -47,23 +70,39 @@ const containerStyle = computed(() => {

const splitItemGap = computed(() => {
const [h, v] = gap.value
return {
const style: CSSProperties = {
columnGap: `${h / 2}px`,
rowGap: `${v}px`,
}
if (props.wrap) {
style.flexWrap = 'wrap'
}
return style
})

const validChildren = computed(() => filterEmpty(slots.default?.()))

// Return null if no children
const shouldRender = computed(() => validChildren.value.length > 0)
</script>

<template>
<div :class="classes" :style="hasSplit ? splitItemGap : containerStyle">
<div
v-if="shouldRender"
:class="[classes, attrs.class]"
:style="[hasSplit ? splitItemGap : containerStyle, attrs.style as CSSProperties]"
>
<template v-if="hasSplit">
<template v-for="(child, index) in $slots.default?.()" :key="index">
<template v-for="(child, index) in validChildren" :key="child.key ?? index">
<div class="ant-space-item">
<component :is="child" />
</div>
<div v-if="index < ($slots.default?.()?.length ?? 0) - 1" class="ant-space-item-split">
<span
v-if="index < validChildren.length - 1"
class="ant-space-item-split"
>
<slot name="split" />
</div>
</span>
</template>
</template>
<slot v-else />
Expand Down
38 changes: 29 additions & 9 deletions packages/ui/src/components/space/SpaceCompact.vue
Original file line number Diff line number Diff line change
@@ -1,25 +1,45 @@
<script setup lang="ts">
import { computed, provide } from 'vue'
import { computed, useSlots, useAttrs, provide } from 'vue'
import type { SpaceCompactProps } from './types'
import { spaceCompactDefaultProps, spaceCompactContextKey } from './types'
import { spaceCompactDefaultProps, filterEmpty, spaceCompactContextKey } from './types'
import { useConfigInject } from '@/hooks'
import CompactItem from './CompactItem.vue'

defineOptions({ name: 'ASpaceCompact' })
defineOptions({ name: 'ASpaceCompact', inheritAttrs: false })
const props = withDefaults(defineProps<SpaceCompactProps>(), spaceCompactDefaultProps)

provide(spaceCompactContextKey, {
compactSize: computed(() => props.size) as any,
compactDirection: computed(() => props.direction) as any,
})
const slots = useSlots()
const attrs = useAttrs()

// Provide context to mark we're inside a SpaceCompact (for nested detection)
provide(spaceCompactContextKey, true)

// Add RTL support
const { direction: rtlDirection } = useConfigInject()
const isRtl = computed(() => rtlDirection.value === 'rtl')

const classes = computed(() => ({
'ant-space-compact': true,
[`ant-space-compact-${props.direction}`]: true,
'ant-space-compact-block': props.block,
'ant-space-compact-rtl': isRtl.value,
[`ant-space-compact-align-${props.align}`]: !!props.align,
}))

const validChildren = computed(() => filterEmpty(slots.default?.() || []))
</script>

<template>
<div :class="classes" role="group">
<slot />
<div v-if="validChildren.length > 0" :class="[classes, attrs.class]" :style="attrs.style" role="group">
<CompactItem
v-for="(child, index) in validChildren"
:key="child.key ?? index"
:compact-size="props.size"
:compact-direction="props.direction"
:is-first-item="index === 0"
:is-last-item="index === validChildren.length - 1"
>
<component :is="child" />
</CompactItem>
</div>
</template>
Loading