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
20 changes: 14 additions & 6 deletions packages/ui/src/components/steps/Step.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ defineSlots<StepSlots>()
const $slots = useSlots()
const context = inject(stepsContextKey, null)

// Register this step and get its index
// Register this step and get its index (side-effect: increments parent counter)
const stepIndex = context?.registerStep() ?? 0
const initial = computed(() => context?.initial.value ?? 0)

const currentStatus = computed(() => {
if (props.status) return props.status
Expand Down Expand Up @@ -44,25 +45,28 @@ function handleKeydown(event: KeyboardEvent) {
const hasTitle = computed(() => !!props.title || !!$slots.title)
const hasDescription = computed(() => !!props.description || !!$slots.description)
const hasSubTitle = computed(() => !!props.subTitle || !!$slots.subTitle)
const hasCustomIcon = computed(() => !!$slots.icon)
const hasCustomIcon = computed(() => !!props.icon || !!$slots.icon)
const isProgressDot = computed(() => !!context?.progressDot.value)

const isFinish = computed(() => currentStatus.value === 'finish')
const isError = computed(() => currentStatus.value === 'error')
const stepNumber = computed(() => String(stepIndex + 1))
const stepNumber = computed(() => String(stepIndex + initial.value + 1))

const classes = computed(() => ({
'ant-steps-item': true,
[`ant-steps-item-${currentStatus.value}`]: true,
'ant-steps-item-disabled': props.disabled,
'ant-steps-item-clickable': isClickable.value && !props.disabled,
'ant-steps-item-custom': hasCustomIcon.value,
'ant-steps-item-custom': hasCustomIcon.value && !isProgressDot.value,
}))

const stepRole = computed(() => (isClickable.value ? 'button' : undefined))
</script>

<template>
<div
:class="classes"
role="button"
:role="stepRole"
:tabindex="isClickable && !disabled ? 0 : undefined"
:aria-current="currentStatus === 'process' ? 'step' : undefined"
:aria-disabled="disabled || undefined"
Expand All @@ -73,7 +77,11 @@ const classes = computed(() => ({
<div class="ant-steps-item-tail" />
<div class="ant-steps-item-icon">
<slot name="icon">
<span class="ant-steps-icon">
<span v-if="hasCustomIcon && icon" class="ant-steps-icon">
<component :is="icon" />
</span>
<span v-else-if="isProgressDot" class="ant-steps-icon ant-steps-icon-dot" />
<span v-else class="ant-steps-icon">
<CheckOutlined v-if="isFinish" />
<CloseOutlined v-else-if="isError" />
<template v-else>{{ stepNumber }}</template>
Expand Down
45 changes: 38 additions & 7 deletions packages/ui/src/components/steps/Steps.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,25 @@
import { computed, provide, ref, onBeforeUpdate } from 'vue'
import type { StepsProps, StepsEmits, StepsSlots } from './types'
import { stepsDefaultProps, stepsContextKey } from './types'
import { useConfigInject, useBreakpoint } from '@/hooks'
import Step from './Step.vue'

defineOptions({ name: 'ASteps' })
const props = withDefaults(defineProps<StepsProps>(), stepsDefaultProps)
const emit = defineEmits<StepsEmits>()
defineSlots<StepsSlots>()

// Step registration counter. Reset before each render via onBeforeUpdate.
const { size: globalSize } = useConfigInject()
const screens = useBreakpoint()

// Steps only supports 'default' | 'small'; ConfigProvider 'md'/'lg' both map to 'default'
const mergedSize = computed(() => props.size ?? (globalSize.value === 'sm' ? 'small' : 'default'))

const mergedDirection = computed(() => {
if (props.responsive && screens.value.xs) return 'vertical'
return props.direction
})

const stepCounter = ref(0)

onBeforeUpdate(() => {
Expand All @@ -22,11 +34,14 @@ function handleStepClick(index: number) {

provide(stepsContextKey, {
current: computed(() => props.current),
initial: computed(() => props.initial),
status: computed(() => props.status),
size: computed(() => props.size),
direction: computed(() => props.direction),
size: mergedSize,
direction: mergedDirection,
labelPlacement: computed(() => props.labelPlacement),
percent: computed(() => props.percent),
progressDot: computed(() => props.progressDot),
type: computed(() => props.type),
onStepClick: handleStepClick,
registerStep: () => {
return stepCounter.value++
Expand All @@ -36,17 +51,33 @@ provide(stepsContextKey, {
},
})

const isInline = computed(() => props.type === 'inline')

const classes = computed(() => ({
'ant-steps': true,
[`ant-steps-${props.direction}`]: true,
[`ant-steps-${props.size}`]: props.size !== 'default',
'ant-steps-label-vertical': props.labelPlacement === 'vertical' && props.direction === 'horizontal',
[`ant-steps-${mergedDirection.value}`]: true,
[`ant-steps-${mergedSize.value}`]: mergedSize.value !== 'default',
'ant-steps-label-vertical':
props.labelPlacement === 'vertical' && mergedDirection.value === 'horizontal',
'ant-steps-navigation': props.type === 'navigation',
'ant-steps-inline': isInline.value,
'ant-steps-dot': !!props.progressDot && !isInline.value,
}))
</script>

<template>
<div :class="classes" role="navigation" aria-label="Steps">
<slot />
<slot>
<Step
v-for="(item, index) in (items || [])"
:key="index"
:title="item.title"
:sub-title="item.subTitle"
:description="item.description"
:icon="item.icon"
:status="item.status"
:disabled="item.disabled"
/>
</slot>
</div>
</template>
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,187 @@ exports[`Steps demos > demo: Icon 1`] = `
</div>"
`;

exports[`Steps demos > demo: Inline 1`] = `
"<div style="display: flex; flex-direction: column; gap: 24px;">
<div style="display: flex; align-items: center; gap: 16px;">
<div style="min-width: 200px;">
<div style="font-weight: 500;">Ant Design Title 1</div>
<div style="font-size: 12px; color: rgba(0, 0, 0, 0.45);"> Ant Design, a design language for background applications </div>
</div>
<div class="ant-steps ant-steps-horizontal ant-steps-inline" role="navigation" aria-label="Steps" style="flex: 1 1 0%;">
<div class="ant-steps-item ant-steps-item-process ant-steps-item-clickable" role="button" tabindex="0" aria-current="step">
<div class="ant-steps-item-container">
<div class="ant-steps-item-tail"></div>
<div class="ant-steps-item-icon"><span class="ant-steps-icon">1</span></div>
<div class="ant-steps-item-content">
<div class="ant-steps-item-title">Step 1
<!--v-if-->
</div>
<!--v-if-->
</div>
</div>
</div>
<div class="ant-steps-item ant-steps-item-wait ant-steps-item-clickable" role="button" tabindex="0">
<div class="ant-steps-item-container">
<div class="ant-steps-item-tail"></div>
<div class="ant-steps-item-icon"><span class="ant-steps-icon">2</span></div>
<div class="ant-steps-item-content">
<div class="ant-steps-item-title">Step 2
<!--v-if-->
</div>
<!--v-if-->
</div>
</div>
</div>
<div class="ant-steps-item ant-steps-item-wait ant-steps-item-clickable" role="button" tabindex="0">
<div class="ant-steps-item-container">
<div class="ant-steps-item-tail"></div>
<div class="ant-steps-item-icon"><span class="ant-steps-icon">3</span></div>
<div class="ant-steps-item-content">
<div class="ant-steps-item-title">Step 3
<!--v-if-->
</div>
<!--v-if-->
</div>
</div>
</div>
</div>
</div>
<div style="display: flex; align-items: center; gap: 16px;">
<div style="min-width: 200px;">
<div style="font-weight: 500;">Ant Design Title 2</div>
<div style="font-size: 12px; color: rgba(0, 0, 0, 0.45);"> Ant Design, a design language for background applications </div>
</div>
<div class="ant-steps ant-steps-horizontal ant-steps-inline" role="navigation" aria-label="Steps" style="flex: 1 1 0%;">
<div class="ant-steps-item ant-steps-item-finish ant-steps-item-clickable" role="button" tabindex="0">
<div class="ant-steps-item-container">
<div class="ant-steps-item-tail"></div>
<div class="ant-steps-item-icon"><span class="ant-steps-icon"><span role="img" aria-label="check" class="anticon anticon-check"><svg focusable="false" class="" data-icon="check" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 00-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z"></path></svg></span></span></div>
<div class="ant-steps-item-content">
<div class="ant-steps-item-title">Step 1
<!--v-if-->
</div>
<!--v-if-->
</div>
</div>
</div>
<div class="ant-steps-item ant-steps-item-error ant-steps-item-clickable" role="button" tabindex="0">
<div class="ant-steps-item-container">
<div class="ant-steps-item-tail"></div>
<div class="ant-steps-item-icon"><span class="ant-steps-icon"><span role="img" aria-label="close" class="anticon anticon-close"><svg focusable="false" class="" data-icon="close" width="1em" height="1em" fill="currentColor" aria-hidden="true" fill-rule="evenodd" viewBox="64 64 896 896"><path d="M799.86 166.31c.02 0 .04.02.08.06l57.69 57.7c.04.03.05.05.06.08a.12.12 0 010 .06c0 .03-.02.05-.06.09L569.93 512l287.7 287.7c.04.04.05.06.06.09a.12.12 0 010 .07c0 .02-.02.04-.06.08l-57.7 57.69c-.03.04-.05.05-.07.06a.12.12 0 01-.07 0c-.03 0-.05-.02-.09-.06L512 569.93l-287.7 287.7c-.04.04-.06.05-.09.06a.12.12 0 01-.07 0c-.02 0-.04-.02-.08-.06l-57.69-57.7c-.04-.03-.05-.05-.06-.07a.12.12 0 010-.07c0-.03.02-.05.06-.09L454.07 512l-287.7-287.7c-.04-.04-.05-.06-.06-.09a.12.12 0 010-.07c0-.02.02-.04.06-.08l57.7-57.69c.03-.04.05-.05.07-.06a.12.12 0 01.07 0c.03 0 .05.02.09.06L512 454.07l287.7-287.7c.04-.04.06-.05.09-.06a.12.12 0 01.07 0z"></path></svg></span></span></div>
<div class="ant-steps-item-content">
<div class="ant-steps-item-title">Step 2
<!--v-if-->
</div>
<!--v-if-->
</div>
</div>
</div>
<div class="ant-steps-item ant-steps-item-wait ant-steps-item-clickable" role="button" tabindex="0">
<div class="ant-steps-item-container">
<div class="ant-steps-item-tail"></div>
<div class="ant-steps-item-icon"><span class="ant-steps-icon">3</span></div>
<div class="ant-steps-item-content">
<div class="ant-steps-item-title">Step 3
<!--v-if-->
</div>
<!--v-if-->
</div>
</div>
</div>
</div>
</div>
<div style="display: flex; align-items: center; gap: 16px;">
<div style="min-width: 200px;">
<div style="font-weight: 500;">Ant Design Title 3</div>
<div style="font-size: 12px; color: rgba(0, 0, 0, 0.45);"> Ant Design, a design language for background applications </div>
</div>
<div class="ant-steps ant-steps-horizontal ant-steps-inline" role="navigation" aria-label="Steps" style="flex: 1 1 0%;">
<div class="ant-steps-item ant-steps-item-finish ant-steps-item-clickable" role="button" tabindex="0">
<div class="ant-steps-item-container">
<div class="ant-steps-item-tail"></div>
<div class="ant-steps-item-icon"><span class="ant-steps-icon"><span role="img" aria-label="check" class="anticon anticon-check"><svg focusable="false" class="" data-icon="check" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 00-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z"></path></svg></span></span></div>
<div class="ant-steps-item-content">
<div class="ant-steps-item-title">Step 1
<!--v-if-->
</div>
<!--v-if-->
</div>
</div>
</div>
<div class="ant-steps-item ant-steps-item-finish ant-steps-item-clickable" role="button" tabindex="0">
<div class="ant-steps-item-container">
<div class="ant-steps-item-tail"></div>
<div class="ant-steps-item-icon"><span class="ant-steps-icon"><span role="img" aria-label="check" class="anticon anticon-check"><svg focusable="false" class="" data-icon="check" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 00-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z"></path></svg></span></span></div>
<div class="ant-steps-item-content">
<div class="ant-steps-item-title">Step 2
<!--v-if-->
</div>
<!--v-if-->
</div>
</div>
</div>
<div class="ant-steps-item ant-steps-item-process ant-steps-item-clickable" role="button" tabindex="0" aria-current="step">
<div class="ant-steps-item-container">
<div class="ant-steps-item-tail"></div>
<div class="ant-steps-item-icon"><span class="ant-steps-icon">3</span></div>
<div class="ant-steps-item-content">
<div class="ant-steps-item-title">Step 3
<!--v-if-->
</div>
<!--v-if-->
</div>
</div>
</div>
</div>
</div>
<div style="display: flex; align-items: center; gap: 16px;">
<div style="min-width: 200px;">
<div style="font-weight: 500;">Ant Design Title 4</div>
<div style="font-size: 12px; color: rgba(0, 0, 0, 0.45);"> Ant Design, a design language for background applications </div>
</div>
<div class="ant-steps ant-steps-horizontal ant-steps-inline" role="navigation" aria-label="Steps" style="flex: 1 1 0%;">
<div class="ant-steps-item ant-steps-item-finish ant-steps-item-clickable" role="button" tabindex="0">
<div class="ant-steps-item-container">
<div class="ant-steps-item-tail"></div>
<div class="ant-steps-item-icon"><span class="ant-steps-icon"><span role="img" aria-label="check" class="anticon anticon-check"><svg focusable="false" class="" data-icon="check" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 00-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z"></path></svg></span></span></div>
<div class="ant-steps-item-content">
<div class="ant-steps-item-title">Step 1
<!--v-if-->
</div>
<!--v-if-->
</div>
</div>
</div>
<div class="ant-steps-item ant-steps-item-process ant-steps-item-clickable" role="button" tabindex="0" aria-current="step">
<div class="ant-steps-item-container">
<div class="ant-steps-item-tail"></div>
<div class="ant-steps-item-icon"><span class="ant-steps-icon">2</span></div>
<div class="ant-steps-item-content">
<div class="ant-steps-item-title">Step 2
<!--v-if-->
</div>
<!--v-if-->
</div>
</div>
</div>
<div class="ant-steps-item ant-steps-item-wait ant-steps-item-clickable" role="button" tabindex="0">
<div class="ant-steps-item-container">
<div class="ant-steps-item-tail"></div>
<div class="ant-steps-item-icon"><span class="ant-steps-icon">3</span></div>
<div class="ant-steps-item-content">
<div class="ant-steps-item-title">Step 3
<!--v-if-->
</div>
<!--v-if-->
</div>
</div>
</div>
</div>
</div>
</div>"
`;

exports[`Steps demos > demo: LabelPlacement 1`] = `
"<div>
<div class="ant-steps ant-steps-horizontal ant-steps-label-vertical" role="navigation" aria-label="Steps">
Expand Down
2 changes: 2 additions & 0 deletions packages/ui/src/components/steps/__tests__/demo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import ProgressDot from '../demo/progress-dot.vue'
import Progress from '../demo/progress.vue'
import LabelPlacement from '../demo/label-placement.vue'
import CustomizedProgressDot from '../demo/customized-progress-dot.vue'
import Inline from '../demo/inline.vue'

const demos = {
Basic,
Expand All @@ -32,6 +33,7 @@ const demos = {
Progress,
LabelPlacement,
CustomizedProgressDot,
Inline,
}

describe('Steps demos', () => {
Expand Down
Loading