Skip to content

Commit 63b0be2

Browse files
OPBRclaude
andcommitted
fix(space): review and enhance Space component
## Changes ### Space.vue - 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 ### SpaceCompact.vue - Add RTL support - Add $attrs passthrough - Add nested SpaceCompact detection via provide/inject - Add default size ('md') - Add align prop support ### CompactItem.vue (new) - Internal component for providing context to children - Support nested SpaceCompact detection - Provide isFirstItem/isLastItem context ### types.ts - Add GLOBAL_SIZE_MAP for ConfigProvider size conversion - Add spaceCompactContextKey for nested detection - Enhance filterEmpty to handle Fragment children - Export SizeType ### style/index.css - Add RTL styles for Space and SpaceCompact - Update selectors for CompactItem wrapper - Add align styles for SpaceCompact ### Tests - 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 f58239d commit 63b0be2

File tree

8 files changed

+378
-88
lines changed

8 files changed

+378
-88
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>

packages/ui/src/components/space/__tests__/__snapshots__/demo.test.ts.snap

Lines changed: 46 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -14,40 +14,52 @@ exports[`Space demos > demo: Basic 1`] = `
1414
`;
1515

1616
exports[`Space demos > demo: Compact 1`] = `
17-
"<div class="ant-space-compact ant-space-compact-horizontal" role="group"><button class="ant-btn ant-btn-outlined ant-btn-md" type="button">
18-
<wave-stub disabled="false"></wave-stub>
19-
<!--v-if--><span class="ant-btn-content">Button 1</span>
20-
</button><button class="ant-btn ant-btn-outlined ant-btn-md" type="button">
21-
<wave-stub disabled="false"></wave-stub>
22-
<!--v-if--><span class="ant-btn-content">Button 2</span>
23-
</button><button class="ant-btn ant-btn-outlined ant-btn-md" type="button">
24-
<wave-stub disabled="false"></wave-stub>
25-
<!--v-if--><span class="ant-btn-content">Button 3</span>
26-
</button></div>"
17+
"<div class="ant-space-compact ant-space-compact-horizontal" role="group">
18+
<div class="ant-space-compact-item"><button class="ant-btn ant-btn-outlined ant-btn-md" type="button">
19+
<wave-stub disabled="false"></wave-stub>
20+
<!--v-if--><span class="ant-btn-content">Button 1</span>
21+
</button></div>
22+
<div class="ant-space-compact-item"><button class="ant-btn ant-btn-outlined ant-btn-md" type="button">
23+
<wave-stub disabled="false"></wave-stub>
24+
<!--v-if--><span class="ant-btn-content">Button 2</span>
25+
</button></div>
26+
<div class="ant-space-compact-item"><button class="ant-btn ant-btn-outlined ant-btn-md" type="button">
27+
<wave-stub disabled="false"></wave-stub>
28+
<!--v-if--><span class="ant-btn-content">Button 3</span>
29+
</button></div>
30+
</div>"
2731
`;
2832

2933
exports[`Space demos > demo: CompactButtons 1`] = `
30-
"<div class="ant-space-compact ant-space-compact-horizontal" role="group"><span class="ant-trigger-wrapper"><button class="ant-btn ant-btn-outlined ant-btn-md" type="button"><wave-stub disabled="false"></wave-stub><!--v-if--><span class="ant-btn-content">Like</span></button></span>
31-
<!--teleport start-->
32-
<transition-stub name="ant-zoom-big-fast" appear="false" persisted="false" css="true">
33-
<!--v-if-->
34-
</transition-stub>
35-
<!--teleport end--><span class="ant-trigger-wrapper"><button class="ant-btn ant-btn-outlined ant-btn-md" type="button"><wave-stub disabled="false"></wave-stub><!--v-if--><span class="ant-btn-content">Comment</span></button></span>
36-
<!--teleport start-->
37-
<transition-stub name="ant-zoom-big-fast" appear="false" persisted="false" css="true">
38-
<!--v-if-->
39-
</transition-stub>
40-
<!--teleport end--><span class="ant-trigger-wrapper"><button class="ant-btn ant-btn-outlined ant-btn-md" type="button"><wave-stub disabled="false"></wave-stub><!--v-if--><span class="ant-btn-content">Star</span></button></span>
41-
<!--teleport start-->
42-
<transition-stub name="ant-zoom-big-fast" appear="false" persisted="false" css="true">
43-
<!--v-if-->
44-
</transition-stub>
45-
<!--teleport end--><span class="ant-trigger-wrapper"><button class="ant-btn ant-btn-outlined ant-btn-md" type="button"><wave-stub disabled="false"></wave-stub><!--v-if--><span class="ant-btn-content">More</span></button></span>
46-
<!--teleport start-->
47-
<transition-stub name="ant-slide-up" appear="false" persisted="false" css="true">
48-
<!--v-if-->
49-
</transition-stub>
50-
<!--teleport end-->
34+
"<div class="ant-space-compact ant-space-compact-horizontal" role="group">
35+
<div class="ant-space-compact-item"><span class="ant-trigger-wrapper"><button class="ant-btn ant-btn-outlined ant-btn-md" type="button"><wave-stub disabled="false"></wave-stub><!--v-if--><span class="ant-btn-content">Like</span></button></span>
36+
<!--teleport start-->
37+
<transition-stub name="ant-zoom-big-fast" appear="false" persisted="false" css="true">
38+
<!--v-if-->
39+
</transition-stub>
40+
<!--teleport end-->
41+
</div>
42+
<div class="ant-space-compact-item"><span class="ant-trigger-wrapper"><button class="ant-btn ant-btn-outlined ant-btn-md" type="button"><wave-stub disabled="false"></wave-stub><!--v-if--><span class="ant-btn-content">Comment</span></button></span>
43+
<!--teleport start-->
44+
<transition-stub name="ant-zoom-big-fast" appear="false" persisted="false" css="true">
45+
<!--v-if-->
46+
</transition-stub>
47+
<!--teleport end-->
48+
</div>
49+
<div class="ant-space-compact-item"><span class="ant-trigger-wrapper"><button class="ant-btn ant-btn-outlined ant-btn-md" type="button"><wave-stub disabled="false"></wave-stub><!--v-if--><span class="ant-btn-content">Star</span></button></span>
50+
<!--teleport start-->
51+
<transition-stub name="ant-zoom-big-fast" appear="false" persisted="false" css="true">
52+
<!--v-if-->
53+
</transition-stub>
54+
<!--teleport end-->
55+
</div>
56+
<div class="ant-space-compact-item"><span class="ant-trigger-wrapper"><button class="ant-btn ant-btn-outlined ant-btn-md" type="button"><wave-stub disabled="false"></wave-stub><!--v-if--><span class="ant-btn-content">More</span></button></span>
57+
<!--teleport start-->
58+
<transition-stub name="ant-slide-up" appear="false" persisted="false" css="true">
59+
<!--v-if-->
60+
</transition-stub>
61+
<!--teleport end-->
62+
</div>
5163
</div>"
5264
`;
5365

@@ -122,20 +134,10 @@ exports[`Space demos > demo: Split 1`] = `
122134
"<div class="ant-space ant-space-horizontal ant-space-align-center" style="column-gap: 4px; row-gap: 8px;">
123135
<div class="ant-space-item"><a class="ant-typography">Link 1
124136
<!--v-if-->
125-
</a></div>
126-
<div class="ant-space-item-split">
127-
<div class="ant-divider ant-divider-vertical" role="separator" aria-orientation="vertical">
128-
<!--v-if-->
129-
</div>
130-
</div>
137+
</a></div><span class="ant-space-item-split"><div class="ant-divider ant-divider-vertical" role="separator" aria-orientation="vertical"><!--v-if--></div></span>
131138
<div class="ant-space-item"><a class="ant-typography">Link 2
132139
<!--v-if-->
133-
</a></div>
134-
<div class="ant-space-item-split">
135-
<div class="ant-divider ant-divider-vertical" role="separator" aria-orientation="vertical">
136-
<!--v-if-->
137-
</div>
138-
</div>
140+
</a></div><span class="ant-space-item-split"><div class="ant-divider ant-divider-vertical" role="separator" aria-orientation="vertical"><!--v-if--></div></span>
139141
<div class="ant-space-item"><a class="ant-typography">Link 3
140142
<!--v-if-->
141143
</a></div>
@@ -157,7 +159,7 @@ exports[`Space demos > demo: Vertical 1`] = `
157159
`;
158160
159161
exports[`Space demos > demo: Wrap 1`] = `
160-
"<div class="ant-space ant-space-horizontal ant-space-align-center" style="column-gap: 8px; row-gap: 8px; flex-wrap: wrap;"><button class="ant-btn ant-btn-outlined ant-btn-md" type="button">
162+
"<div class="ant-space ant-space-horizontal ant-space-align-center" style="column-gap: 8px; row-gap: 8px; flex-wrap: wrap; margin-bottom: -8px;"><button class="ant-btn ant-btn-outlined ant-btn-md" type="button">
161163
<wave-stub disabled="false"></wave-stub>
162164
<!--v-if--><span class="ant-btn-content">Button 1</span>
163165
</button><button class="ant-btn ant-btn-outlined ant-btn-md" type="button">

0 commit comments

Comments
 (0)