Skip to content

Commit

Permalink
feat(Stepper): add component
Browse files Browse the repository at this point in the history
  • Loading branch information
romhml committed Nov 22, 2024
1 parent 3baddfd commit ef4cb5b
Show file tree
Hide file tree
Showing 8 changed files with 206 additions and 0 deletions.
32 changes: 32 additions & 0 deletions docs/content/3.components/stepper.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
description:
links:
- label: Stepper
icon: i-custom-radix-vue
to: https://www.radix-vue.com/components/stepper.html
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxt/ui/tree/v3/src/runtime/components/Stepper.vue
---

## Usage

## Examples

## API

### Props

:component-props

### Slots

:component-slots

### Emits

:component-emits

## Theme

:component-theme
1 change: 1 addition & 0 deletions playground/app/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ const components = [
'skeleton',
'slideover',
'slider',
'stepper',
'switch',
'tabs',
'table',
Expand Down
43 changes: 43 additions & 0 deletions playground/app/pages/components/stepper.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<script setup lang="ts">
const items = [
{
slot: 'address',
title: 'Address',
description: 'Add your address here',
icon: 'i-lucide-house'
}, {
slot: 'shipping',
title: 'Shipping',
description: 'Set your preferred shipping method',
icon: 'i-lucide-truck'
}, {
slot: 'checkout',
title: 'Checkout',
description: 'Confirm your order'
}
]
</script>

<template>
<div>
<UStepper :items="items" :default-value="2">
<template #address>
<Placeholder class="w-full aspect-video">
Address
</Placeholder>
</template>

<template #shipping>
<Placeholder class="w-full aspect-video">
Shipping
</Placeholder>
</template>

<template #checkout>
<Placeholder class="w-full aspect-video">
Checkout
</Placeholder>
</template>
</UStepper>
</div>
</template>
97 changes: 97 additions & 0 deletions src/runtime/components/Stepper.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<script lang="ts">
import { tv, type VariantProps } from 'tailwind-variants'
import type { StepperRootProps, StepperRootEmits } from 'radix-vue'
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
import theme from '#build/ui/stepper'
import type { DynamicSlots } from '../types/utils'
const appConfig = _appConfig as AppConfig & { ui: { stepper: Partial<typeof theme> } }
const stepper = tv({ extend: tv(theme), ...(appConfig.ui?.stepper || {}) })
type _StepperVariants = VariantProps<typeof stepper>
export interface StepperItem {
slot?: string
title?: string
description?: string
icon: string
content: string
}
export interface StepperProps<T extends StepperItem> extends StepperRootProps {
items: Array<T>
class?: any
ui?: Partial<typeof stepper.slots>
}
export interface StepperEmits extends StepperRootEmits {}
type SlotProps<T extends StepperItem> = (props: { item: T }) => any
export type StepperSlots<T extends StepperItem> = {} & DynamicSlots<T, SlotProps<T>>
</script>
<script setup lang="ts" generic="T extends StepperItem">
import { computed, ref } from 'vue'
import { StepperRoot, StepperItem, StepperTrigger, StepperIndicator, StepperSeparator, StepperTitle, StepperDescription, useForwardPropsEmits } from 'radix-vue'
import { reactivePick } from '@vueuse/core'
import UIcon from './Icon.vue'
const props = defineProps<StepperProps<T>>()
const emits = defineEmits<StepperEmits>()
defineSlots<StepperSlots<T>>()
const rootProps = useForwardPropsEmits(reactivePick(props), emits)
// eslint-disable-next-line vue/no-dupe-keys
const ui = stepper()
const currentStepIndex = ref(0)
const currentStep = computed(() => props.items?.[currentStepIndex.value] ?? props.items?.[0])
const modelValue = defineModel<string>({

Check failure on line 53 in src/runtime/components/Stepper.vue

View workflow job for this annotation

GitHub Actions / ci (ubuntu-latest, 20)

No overload matches this call.
get: () => currentStep.value.slot,
set(value: number) {
return props.items?.[value]?.slot
}
})
</script>

<template>
<StepperRoot v-bind="rootProps" v-model="currentStepIndex" :class="ui.root({ class: [props.class, props.ui?.root] })">
<div :class="ui.header({ class: props.ui?.header })">
<StepperItem v-for="item, count in items" :key="item.slot" :step="count" :class="ui.item({ class: props.ui?.item })">
<StepperTrigger :class="ui.trigger({ class: props.ui?.trigger })">
<StepperIndicator :class="ui.indicator({ class: props.ui?.indicator })">
<UIcon v-if="item.icon" :name="item.icon" />
<p v-else>
{{ count + 1 }}
</p>
</StepperIndicator>
</StepperTrigger>

<StepperSeparator
v-if="item.slot !== items[items.length - 1]?.slot"
:class="ui.separator({ class: props.ui?.separator })"
/>

<div>
<StepperTitle
:class="ui.title({ class: props.ui?.title })"
>
{{ item.title }}
</StepperTitle>
<StepperDescription
:class="ui.description({ class: props.ui?.description })"
>
{{ item.description }}
</StepperDescription>
</div>
</StepperItem>
</div>
<slot :name="modelValue || 'content'" :item="currentStep">

Check failure on line 93 in src/runtime/components/Stepper.vue

View workflow job for this annotation

GitHub Actions / ci (ubuntu-latest, 20)

Type 'ModelRef<string, string, T, T>' cannot be used as an index type.
{{ currentStep.content }}
</slot>
</StepperRoot>
</template>
1 change: 1 addition & 0 deletions src/runtime/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export * from '../components/Separator.vue'
export * from '../components/Skeleton.vue'
export * from '../components/Slideover.vue'
export * from '../components/Slider.vue'
export * from '../components/Stepper.vue'
export * from '../components/Switch.vue'
export * from '../components/Table.vue'
export * from '../components/Tabs.vue'
Expand Down
1 change: 1 addition & 0 deletions src/theme/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export { default as separator } from './separator'
export { default as skeleton } from './skeleton'
export { default as slideover } from './slideover'
export { default as slider } from './slider'
export { default as stepper } from './stepper'
export { default as switch } from './switch'
export { default as table } from './table'
export { default as tabs } from './tabs'
Expand Down
14 changes: 14 additions & 0 deletions src/theme/stepper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export default {
slots: {
root: 'flex flex-col gap-4',
header: 'flex',
item: 'group text-center relative w-full',
content: '',
trigger: 'flex-none',
indicator: 'rounded-full font-medium text-center align-middle flex items-center justify-center p-2 group-data-[state=completed]:text-[var(--ui-bg)] text-[var(--ui-text-accented)] bg-[var(--ui-bg-accented)] group-data-[state=completed]:bg-[var(--ui-primary)] size-10',
separator: 'absolute top-4.5 block left-[calc(50%+25px)] right-[calc(-50%+25px)] h-0.5 rounded-full group-data-[disabled]:opacity-75 bg-[var(--ui-border-accented)] group-data-[state=completed]:bg-[var(--ui-primary)] shrink-0',
title: 'font-medium mt-1',
description: 'text-wrap text-sm'

}
}
17 changes: 17 additions & 0 deletions test/components/Stepper.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { describe, it, expect } from 'vitest'
import Stepper, { type StepperProps, type StepperSlots } from '../../src/runtime/components/Stepper.vue'
import ComponentRender from '../component-render'

describe('Stepper', () => {
it.each([
// Props
['with as', { props: { as: 'div' } }],
['with class', { props: { class: '' } }],
['with ui', { props: { ui: {} } }],
// Slots
['with default slot', { slots: { default: () => 'Default slot' } }]
])('renders %s correctly', async (nameOrHtml: string, options: { props?: StepperProps<any>, slots?: Partial<StepperSlots<any>> }) => {

Check failure on line 13 in test/components/Stepper.spec.ts

View workflow job for this annotation

GitHub Actions / ci (ubuntu-latest, 20)

Argument of type '(nameOrHtml: string, options: { props?: StepperProps<any>; slots?: Partial<StepperSlots<any>>; }) => Promise<void>' is not assignable to parameter of type '(...args: [string, { props: { as: string; }; }] | [string, { props: { class: string; }; }] | [string, { props: { ui: {}; }; }] | [string, { slots: { default: () => "Default slot"; }; }]) => Awaitable<void>'.
const html = await ComponentRender(nameOrHtml, options, Stepper)
expect(html).toMatchSnapshot()
})
})

0 comments on commit ef4cb5b

Please sign in to comment.