Skip to content

Commit 27dd518

Browse files
OPBRclaude
andcommitted
fix(space): review and enhance Space component
- Add ConfigProvider size inheritance (sm/md/lg -> small/middle/large) - Add RTL support via useConfigInject - Add $attrs passthrough with inheritAttrs: false - Add wrap mode with negative marginBottom - Add empty children handling (return null if no valid children) - Use filterEmpty for VNode filtering with Fragment support - Add RTL support - Add $attrs passthrough - Add nested SpaceCompact detection via provide/inject - Add default size ('md') - Add align prop support - Internal component for providing context to children - Support nested SpaceCompact detection - Provide isFirstItem/isLastItem context - Add GLOBAL_SIZE_MAP for ConfigProvider size conversion - Add spaceCompactContextKey for nested detection - Enhance filterEmpty to handle Fragment children - Export SizeType - Add RTL styles for Space and SpaceCompact - Update selectors for CompactItem wrapper - Add align styles for SpaceCompact - Add tests for RTL support - Add tests for ConfigProvider size inheritance - Add tests for align prop - Add tests for Space.Compact static property - Update snapshots Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent b91dad9 commit 27dd518

File tree

8 files changed

+648
-253
lines changed

8 files changed

+648
-253
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<script setup lang="ts">
2+
import { provide, inject } from 'vue'
3+
import { spaceCompactItemContextKey, spaceCompactContextKey } from './types'
4+
import type { SizeType, SpaceCompactItemContext } from './types'
5+
6+
const props = defineProps<{
7+
compactSize?: SizeType
8+
compactDirection?: 'horizontal' | 'vertical'
9+
isFirstItem: boolean
10+
isLastItem: boolean
11+
}>()
12+
13+
// Detect if we're inside a nested SpaceCompact
14+
const hasParentCompact = inject(spaceCompactContextKey, false)
15+
16+
// If nested, inherit isFirstItem/isLastItem from parent context
17+
const parentContext = inject<SpaceCompactItemContext | undefined>(spaceCompactItemContextKey, undefined)
18+
19+
// Use parent's isFirstItem/isLastItem if nested, otherwise use own props
20+
const resolvedIsFirstItem = hasParentCompact && parentContext ? parentContext.isFirstItem : props.isFirstItem
21+
const resolvedIsLastItem = hasParentCompact && parentContext ? parentContext.isLastItem : props.isLastItem
22+
23+
// Provide context for child components (Button, Input, Select etc.)
24+
provide(spaceCompactItemContextKey, {
25+
get compactSize() {
26+
return props.compactSize
27+
},
28+
get compactDirection() {
29+
return props.compactDirection
30+
},
31+
get isFirstItem() {
32+
return resolvedIsFirstItem
33+
},
34+
get isLastItem() {
35+
return resolvedIsLastItem
36+
},
37+
})
38+
</script>
39+
40+
<template>
41+
<div class="ant-space-compact-item">
42+
<slot />
43+
</div>
44+
</template>
Lines changed: 67 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,124 @@
11
<script setup lang="ts">
2-
import { computed, useSlots } from 'vue'
2+
import { computed, useSlots, useAttrs, type VNode, type CSSProperties } from 'vue'
33
import type { SpaceProps, SpaceSlots, SpaceSize, SpaceSizePreset } from './types'
4-
import { spaceDefaultProps, SPACE_SIZE_MAP } from './types'
4+
import { spaceDefaultProps, SPACE_SIZE_MAP, GLOBAL_SIZE_MAP, filterEmpty } from './types'
5+
import { useConfigInject } from '@/hooks'
56
6-
defineOptions({ name: 'ASpace' })
7+
defineOptions({ name: 'ASpace', inheritAttrs: false })
78
const props = withDefaults(defineProps<SpaceProps>(), spaceDefaultProps)
89
defineSlots<SpaceSlots>()
10+
911
const slots = useSlots()
12+
const attrs = useAttrs()
13+
14+
// Support global size from ConfigProvider and RTL direction
15+
const { size: globalSize, direction: rtlDirection } = useConfigInject()
1016
11-
function resolveSize(size: SpaceSize): number {
17+
function resolveSize(size: SpaceSize | undefined): number {
18+
if (size === undefined) return 0
1219
return typeof size === 'string' ? SPACE_SIZE_MAP[size as SpaceSizePreset] ?? 0 : size
1320
}
1421
22+
// Convert ConfigProvider size (sm/md/lg) to Space size (small/middle/large)
23+
function normalizeGlobalSize(size: string): SpaceSizePreset {
24+
return GLOBAL_SIZE_MAP[size] ?? 'small'
25+
}
26+
27+
// Merge prop size with global size (prop takes precedence)
28+
const mergedSize = computed(() => {
29+
if (props.size !== undefined) return props.size
30+
const global = globalSize.value
31+
// Handle ConfigProvider size format (sm/md/lg)
32+
if (typeof global === 'string' && global in GLOBAL_SIZE_MAP) {
33+
return normalizeGlobalSize(global)
34+
}
35+
return 'small'
36+
})
37+
1538
const gap = computed(() => {
16-
if (Array.isArray(props.size)) {
17-
return [resolveSize(props.size[0]), resolveSize(props.size[1])] as [number, number]
39+
const size = mergedSize.value
40+
if (Array.isArray(size)) {
41+
return [resolveSize(size[0]), resolveSize(size[1])] as [number, number]
1842
}
19-
const s = resolveSize(props.size!)
43+
const s = resolveSize(size as SpaceSize)
2044
return [s, s] as [number, number]
2145
})
2246
2347
const hasSplit = computed(() => !!slots.split)
2448
49+
// Add RTL support
50+
const isRtl = computed(() => rtlDirection.value === 'rtl')
51+
2552
const classes = computed(() => ({
2653
'ant-space': true,
2754
[`ant-space-${props.direction}`]: true,
55+
'ant-space-rtl': isRtl.value,
2856
'ant-space-align-center': !props.align && props.direction === 'horizontal',
2957
[`ant-space-align-${props.align}`]: !!props.align,
3058
}))
3159
3260
const containerStyle = computed(() => {
3361
const [h, v] = gap.value
34-
const style: Record<string, string> = {}
62+
const style: CSSProperties = {}
3563
3664
if (!hasSplit.value) {
3765
style.columnGap = `${h}px`
3866
style.rowGap = `${v}px`
3967
}
4068
69+
// wrap 模式下添加负 marginBottom
4170
if (props.wrap) {
4271
style.flexWrap = 'wrap'
72+
style.marginBottom = `${-v}px`
4373
}
4474
4575
return style
4676
})
4777
4878
const splitItemGap = computed(() => {
4979
const [h, v] = gap.value
50-
return {
80+
const style: CSSProperties = {
5181
columnGap: `${h / 2}px`,
5282
rowGap: `${v}px`,
5383
}
84+
if (props.wrap) {
85+
style.flexWrap = 'wrap'
86+
style.marginBottom = `${-v}px`
87+
}
88+
return style
5489
})
90+
91+
// Filter empty children
92+
function getValidChildren(children: VNode[] | undefined): VNode[] {
93+
return filterEmpty(children)
94+
}
95+
96+
// Get valid children for rendering
97+
const validChildren = computed(() => getValidChildren(slots.default?.()))
98+
99+
// Return null if no children
100+
const shouldRender = computed(() => validChildren.value.length > 0)
55101
</script>
56102

57103
<template>
58-
<div :class="classes" :style="hasSplit ? splitItemGap : containerStyle">
104+
<div
105+
v-if="shouldRender"
106+
:class="[classes, attrs.class]"
107+
:style="[hasSplit ? splitItemGap : containerStyle, attrs.style as CSSProperties]"
108+
>
59109
<template v-if="hasSplit">
60-
<template v-for="(child, index) in $slots.default?.()" :key="index">
110+
<template v-for="(child, index) in validChildren" :key="child.key ?? index">
61111
<div class="ant-space-item">
62112
<component :is="child" />
63113
</div>
64-
<div v-if="index < ($slots.default?.()?.length ?? 0) - 1" class="ant-space-item-split">
114+
<span
115+
v-if="index < validChildren.length - 1"
116+
class="ant-space-item-split"
117+
>
65118
<slot name="split" />
66-
</div>
119+
</span>
67120
</template>
68121
</template>
69122
<slot v-else />
70123
</div>
71-
</template>
124+
</template>
Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,52 @@
11
<script setup lang="ts">
2-
import { computed, provide } from 'vue'
2+
import { computed, useSlots, useAttrs, provide, type VNode } from 'vue'
33
import type { SpaceCompactProps } from './types'
4-
import { spaceCompactDefaultProps, spaceCompactContextKey } from './types'
4+
import { spaceCompactDefaultProps, filterEmpty, spaceCompactContextKey } from './types'
5+
import { useConfigInject } from '@/hooks'
6+
import CompactItem from './CompactItem.vue'
57
6-
defineOptions({ name: 'ASpaceCompact' })
8+
defineOptions({ name: 'ASpaceCompact', inheritAttrs: false })
79
const props = withDefaults(defineProps<SpaceCompactProps>(), spaceCompactDefaultProps)
810
9-
provide(spaceCompactContextKey, {
10-
compactSize: computed(() => props.size) as any,
11-
compactDirection: computed(() => props.direction) as any,
12-
})
11+
const slots = useSlots()
12+
const attrs = useAttrs()
13+
14+
// Provide context to mark we're inside a SpaceCompact (for nested detection)
15+
provide(spaceCompactContextKey, true)
16+
17+
// Add RTL support
18+
const { direction: rtlDirection } = useConfigInject()
19+
const isRtl = computed(() => rtlDirection.value === 'rtl')
1320
1421
const classes = computed(() => ({
1522
'ant-space-compact': true,
1623
[`ant-space-compact-${props.direction}`]: true,
1724
'ant-space-compact-block': props.block,
25+
'ant-space-compact-rtl': isRtl.value,
26+
[`ant-space-compact-align-${props.align}`]: !!props.align,
1827
}))
28+
29+
// Get valid children and filter empty ones
30+
function getValidChildren(): VNode[] {
31+
const children = slots.default?.() || []
32+
return filterEmpty(children)
33+
}
34+
35+
// Pre-compute children to avoid repeated calls in template
36+
const validChildren = computed(() => getValidChildren())
1937
</script>
2038

2139
<template>
22-
<div :class="classes" role="group">
23-
<slot />
40+
<div v-if="validChildren.length > 0" :class="[classes, attrs.class]" :style="attrs.style" role="group">
41+
<CompactItem
42+
v-for="(child, index) in validChildren"
43+
:key="child.key ?? index"
44+
:compact-size="props.size"
45+
:compact-direction="props.direction"
46+
:is-first-item="index === 0"
47+
:is-last-item="index === validChildren.length - 1"
48+
>
49+
<component :is="child" />
50+
</CompactItem>
2451
</div>
25-
</template>
52+
</template>

0 commit comments

Comments
 (0)