diff --git a/packages/api-generator/src/locale/en/Stepper.json b/packages/api-generator/src/locale/en/Stepper.json new file mode 100644 index 00000000000..797be50f8e6 --- /dev/null +++ b/packages/api-generator/src/locale/en/Stepper.json @@ -0,0 +1,27 @@ +{ + "props": { + "altLabels": "Places the labels beneath the step.", + "editable": "Marks step as editable.", + "hideActions": "Hide actions bar (prev and next buttons).", + "itemTitle": "Property on supplied `items` that contains its title.", + "itemValue": "Property on supplied `items` that contains its value.", + "mobile": "Forces the stepper into a mobile state, removing labels from stepper items.", + "nextText": "The text used for the Next button.", + "prevText": "The text used for the Prev button.", + "nonLinear": "Allow user to jump to any step." + }, + "slots": { + "[`header-item.${string}`]": "Slot for customizing header items when using the [items](/api/v-stepper/#props-items) prop.", + "[`item.${string}`]": "Slot for customizing the content for each step.", + "actions": "Slot for customizing [v-stepper-actions](/api/v-stepper-actions/).", + "header": "Slot for customizing the header.", + "header-item": "Slot for customizing all header items.", + "icon": "Slot for customizing all stepper item icons.", + "next": "Slot for customizing the next step functionailty", + "prev": "Slot for customizing the prev step functionality" + }, + "exposed": { + "next": "Move to the next step.", + "prev": "Move to the prev step." + } +} diff --git a/packages/api-generator/src/locale/en/StepperItem.json b/packages/api-generator/src/locale/en/StepperItem.json new file mode 100644 index 00000000000..3f1ddeb13a5 --- /dev/null +++ b/packages/api-generator/src/locale/en/StepperItem.json @@ -0,0 +1,12 @@ +{ + "props": { + "complete": "Marks step as complete.", + "completeIcon": "Icon to display when step is marked as completed.", + "editable": "Marks step as editable.", + "editIcon": "Icon to display when step is editable.", + "errorIcon": "Icon to display when step has an error.", + "error": "Puts the stepper item in a manual error state.", + "rules": "Accepts a mixed array of types `function`, `boolean` and `string`. Functions pass an input value as an argument and must return either `true` / `false` or a `string` containing an error message. The input field will enter an error state if a function returns (or any value in the array contains) `false` or is a `string`.", + "step": "Content to display inside step circle." + } +} diff --git a/packages/api-generator/src/locale/en/VExpansionPanelTitle.json b/packages/api-generator/src/locale/en/VExpansionPanelTitle.json index 814c6cca0c8..c3d385c2bf4 100644 --- a/packages/api-generator/src/locale/en/VExpansionPanelTitle.json +++ b/packages/api-generator/src/locale/en/VExpansionPanelTitle.json @@ -2,6 +2,7 @@ "props": { "collapseIcon": "Icon used when the expansion panel is in a collapsable state.", "expandIcon": "Icon used when the expansion panel is in a expandable state.", - "hideActions": "Hide the expand icon in the content title." + "hideActions": "Hide the expand icon in the content title.", + "static": "Remove title size expansion when selected." } } diff --git a/packages/api-generator/src/locale/en/VStepper.json b/packages/api-generator/src/locale/en/VStepper.json index 43b4515f855..ae3f59342c2 100644 --- a/packages/api-generator/src/locale/en/VStepper.json +++ b/packages/api-generator/src/locale/en/VStepper.json @@ -1,26 +1,6 @@ { "props": { - "altLabels": "Places the labels beneath the step.", - "editable": "Marks step as editable.", - "flat": "Removes the stepper's elevation.", - "hideActions": "Hide actions bar (prev and next buttons).", - "itemTitle": "Property on supplied `items` that contains its title.", - "itemValue": "Property on supplied `items` that contains its value.", - "mobile": "Forces the stepper into a mobile state, removing labels from stepper items.", - "nextText": "The text used for the Next button.", - "prevText": "The text used for the Prev button.", - "nonLinear": "Allow user to jump to any step.", - "vertical": "Display steps vertically." - }, - "slots": { - "[`header-item.${string}`]": "Slot for customizing header items when using the [items](/api/v-stepper/#props-items) prop.", - "[`item.${string}`]": "Slot for customizing the content for each step.", - "actions": "Slot for customizing [v-stepper-actions](/api/v-stepper-actions/).", - "header": "Slot for customizing the header.", - "header-item": "Slot for customizing all header items.", - "icon": "Slot for customizing all stepper item icons.", - "next": "Slot for customizing the next step functionailty", - "prev": "Slot for customizing the prev step functionality" + "flat": "Removes the stepper's elevation." }, "exposed": { "next": "Move to the next step.", diff --git a/packages/api-generator/src/locale/en/VStepperItem.json b/packages/api-generator/src/locale/en/VStepperItem.json index 6a4eb04f3bf..6ee1816d2a3 100644 --- a/packages/api-generator/src/locale/en/VStepperItem.json +++ b/packages/api-generator/src/locale/en/VStepperItem.json @@ -1,16 +1,7 @@ { "props": { - "complete": "Marks step as complete.", - "completeIcon": "Icon to display when step is marked as completed.", - "editable": "Marks step as editable.", - "editIcon": "Icon to display when step is editable.", - "errorIcon": "Icon to display when step has an error.", - "error": "Puts the stepper item in a manual error state.", - "rules": "Accepts a mixed array of types `function`, `boolean` and `string`. Functions pass an input value as an argument and must return either `true` / `false` or a `string` containing an error message. The input field will enter an error state if a function returns (or any value in the array contains) `false` or is a `string`.", - "step": "Content to display inside step circle." }, "events": { - "click": "Emitted when component is clicked." }, "slots": { "icon": "Slot for customizing all stepper item icons." diff --git a/packages/api-generator/src/locale/en/VStepperVerticalActions.json b/packages/api-generator/src/locale/en/VStepperVerticalActions.json new file mode 100644 index 00000000000..4ecea34c2de --- /dev/null +++ b/packages/api-generator/src/locale/en/VStepperVerticalActions.json @@ -0,0 +1,9 @@ +{ + "props": { + "finish": "Changes the Next button to use the finish text.", + "finishText": "The text used for the finish button. Shown when using the **finish** prop." + }, + "events": { + "click:finish": "Emitted when the clicking the finish button." + } +} diff --git a/packages/api-generator/src/locale/en/VStepperVerticalItem.json b/packages/api-generator/src/locale/en/VStepperVerticalItem.json new file mode 100644 index 00000000000..ca6696cfa26 --- /dev/null +++ b/packages/api-generator/src/locale/en/VStepperVerticalItem.json @@ -0,0 +1,7 @@ +{ + "events": { + "click:finish": "Event emitted when clicking the finish button", + "click:next": "Event emitted when clicking the next button", + "click:previous": "Event emitted when clicking the previous button" + } +} diff --git a/packages/docs/src/data/nav.json b/packages/docs/src/data/nav.json index 91446a8eadb..fa98c50da22 100644 --- a/packages/docs/src/data/nav.json +++ b/packages/docs/src/data/nav.json @@ -248,6 +248,10 @@ "title": "snackbar-queue", "subfolder": "components" }, + { + "title": "vertical-steppers", + "subfolder": "components" + }, { "title": "time-pickers", "subfolder": "components" diff --git a/packages/docs/src/examples/v-stepper-vertical/slot-actions.vue b/packages/docs/src/examples/v-stepper-vertical/slot-actions.vue new file mode 100644 index 00000000000..8b9b45cc840 --- /dev/null +++ b/packages/docs/src/examples/v-stepper-vertical/slot-actions.vue @@ -0,0 +1,84 @@ + + + + + diff --git a/packages/docs/src/examples/v-stepper-vertical/usage.vue b/packages/docs/src/examples/v-stepper-vertical/usage.vue new file mode 100644 index 00000000000..141dc55bf82 --- /dev/null +++ b/packages/docs/src/examples/v-stepper-vertical/usage.vue @@ -0,0 +1,53 @@ + + + diff --git a/packages/docs/src/pages/en/components/vertical-steppers.md b/packages/docs/src/pages/en/components/vertical-steppers.md new file mode 100644 index 00000000000..f7852a3c159 --- /dev/null +++ b/packages/docs/src/pages/en/components/vertical-steppers.md @@ -0,0 +1,70 @@ +--- +emphasized: true +meta: + nav: Steppers Vertical + title: Vertical Stepper component + description: The vertical stepper component is a navigation element that guides users through a sequence of steps. + keywords: vertical stepper, vuetify vertical stepper component, vue vertical stepper component +related: + - /components/buttons/ + - /components/icons/ + - /styles/transitions/ +features: + report: true +--- + +# Vertical Steppers + +The `v-stepper-vertical` component can be used as a navigation element that guides users through a sequence of steps. + + + +::: warning + +This feature requires [v3.5.14](/getting-started/release-notes/?version=v3.5.14) + +::: + +## Installation + +Labs components require a manual import and installation of the component. + +```js { resource="src/plugins/vuetify.js" } +import { VStepperVertical } from 'vuetify/labs/VStepperVertical' + +export default createVuetify({ + components: { + VStepperVertical, + }, +}) +``` + +## Usage + +Vertical steppers allow users to complete a series of actions in step order. + + + + + +## API + +| Component | Description | +| - | - | +| [v-stepper-vertical](/api/v-stepper-vertical/) | Primary Component | + + + +### Guide + +The `v-stepper-vertical` is the vertical variant of the [v-stepper](/components/steppers/) component. It also extends functionality of [v-expansion-panels](/components/expansion-panels/). + +#### Slots + +The `v-stepper-vertical` component has several slots for customization. + +##### Actions + +Customize the flow of your stepper by hooking into the available **prev** and **next** slots. + + diff --git a/packages/docs/src/pages/en/getting-started/upgrade-guide.md b/packages/docs/src/pages/en/getting-started/upgrade-guide.md index 39923393806..61737ab9450 100644 --- a/packages/docs/src/pages/en/getting-started/upgrade-guide.md +++ b/packages/docs/src/pages/en/getting-started/upgrade-guide.md @@ -232,6 +232,11 @@ app.use(vuetify) - `v-simple-table` has been renamed to `v-table` +### v-stepper (vertical) + +- `v-stepper-step` has been renamed to `v-stepper-vertical-item`. Move content into the **title** slot. +- `v-stepper-content` has been removed. Move content to the default slot of `v-stepper-vertical-item`. + ### v-data-table - Headers objects: diff --git a/packages/vuetify/src/components/VExpansionPanel/VExpansionPanel.tsx b/packages/vuetify/src/components/VExpansionPanel/VExpansionPanel.tsx index 021ae213381..4e913e6ffe5 100644 --- a/packages/vuetify/src/components/VExpansionPanel/VExpansionPanel.tsx +++ b/packages/vuetify/src/components/VExpansionPanel/VExpansionPanel.tsx @@ -124,7 +124,9 @@ export const VExpansionPanel = genericComponent()({ ) }) - return {} + return { + groupItem, + } }, }) diff --git a/packages/vuetify/src/components/VExpansionPanel/VExpansionPanels.tsx b/packages/vuetify/src/components/VExpansionPanel/VExpansionPanels.tsx index ead703dc956..f60ccb44420 100644 --- a/packages/vuetify/src/components/VExpansionPanel/VExpansionPanels.tsx +++ b/packages/vuetify/src/components/VExpansionPanel/VExpansionPanels.tsx @@ -23,6 +23,15 @@ const allowedVariants = ['default', 'accordion', 'inset', 'popout'] as const type Variant = typeof allowedVariants[number] +export type VExpansionPanelSlot = { + prev: () => void + next: () => void +} + +export type VExpansionPanelSlots = { + default: VExpansionPanelSlot +} + export const makeVExpansionPanelsProps = propsFactory({ flat: Boolean, @@ -37,7 +46,7 @@ export const makeVExpansionPanelsProps = propsFactory({ }, }, 'VExpansionPanels') -export const VExpansionPanels = genericComponent()({ +export const VExpansionPanels = genericComponent()({ name: 'VExpansionPanels', props: makeVExpansionPanelsProps(), @@ -47,7 +56,7 @@ export const VExpansionPanels = genericComponent()({ }, setup (props, { slots }) { - useGroup(props, VExpansionPanelSymbol) + const { next, prev } = useGroup(props, VExpansionPanelSymbol) const { themeClasses } = provideTheme(props) @@ -83,11 +92,15 @@ export const VExpansionPanels = genericComponent()({ props.class, ]} style={ props.style } - v-slots={ slots } - /> + > + { slots.default?.({ prev, next }) } + )) - return {} + return { + next, + prev, + } }, }) diff --git a/packages/vuetify/src/components/VStepper/VStepper.tsx b/packages/vuetify/src/components/VStepper/VStepper.tsx index 4e2fa4ebce2..1350134118c 100644 --- a/packages/vuetify/src/components/VStepper/VStepper.tsx +++ b/packages/vuetify/src/components/VStepper/VStepper.tsx @@ -20,13 +20,11 @@ import { genericComponent, getPropertyFromItem, only, propsFactory, useRender } // Types import type { InjectionKey, PropType } from 'vue' -import type { StepperItemSlot } from './VStepperItem' +import type { StepperItem, StepperItemSlot } from './VStepperItem' import type { GroupItemProvide } from '@/composables/group' export const VStepperSymbol: InjectionKey = Symbol.for('vuetify:v-stepper') -export type StepperItem = string | Record - export type VStepperSlot = { prev: () => void next: () => void @@ -48,7 +46,7 @@ export type VStepperSlots = { [key: `item.${string}`]: StepperItem } -export const makeVStepperProps = propsFactory({ +export const makeStepperProps = propsFactory({ altLabels: Boolean, bgColor: String, editable: Boolean, @@ -68,7 +66,10 @@ export const makeVStepperProps = propsFactory({ mobile: Boolean, nonLinear: Boolean, flat: Boolean, +}, 'Stepper') +export const makeVStepperProps = propsFactory({ + ...makeStepperProps(), ...makeGroupProps({ mandatory: 'force' as const, selectedClass: 'v-stepper-item--selected', diff --git a/packages/vuetify/src/components/VStepper/VStepperItem.tsx b/packages/vuetify/src/components/VStepper/VStepperItem.tsx index 129bc0395c0..42766e6221a 100644 --- a/packages/vuetify/src/components/VStepper/VStepperItem.tsx +++ b/packages/vuetify/src/components/VStepper/VStepperItem.tsx @@ -21,6 +21,8 @@ import { genericComponent, propsFactory, useRender } from '@/util' import type { PropType } from 'vue' import type { RippleDirectiveBinding } from '@/directives/ripple' +export type StepperItem = string | Record + export type StepperItemSlot = { canEdit: boolean hasError: boolean @@ -39,7 +41,7 @@ export type VStepperItemSlots = { export type ValidationRule = () => string | boolean -export const makeVStepperItemProps = propsFactory({ +export const makeStepperItemProps = propsFactory({ color: String, title: String, subtitle: String, @@ -67,7 +69,10 @@ export const makeVStepperItemProps = propsFactory({ type: Array as PropType, default: () => ([]), }, +}, 'StepperItem') +export const makeVStepperItemProps = propsFactory({ + ...makeStepperItemProps(), ...makeGroupItemProps(), }, 'VStepperItem') diff --git a/packages/vuetify/src/composables/group.ts b/packages/vuetify/src/composables/group.ts index 42d7e6b14a5..b039bbcecc6 100644 --- a/packages/vuetify/src/composables/group.ts +++ b/packages/vuetify/src/composables/group.ts @@ -47,6 +47,8 @@ export interface GroupProvide { export interface GroupItemProvide { id: number isSelected: Ref + isFirst: Ref + isLast: Ref toggle: () => void select: (value: boolean) => void selectedClass: Ref<(string | undefined)[] | false> @@ -129,6 +131,12 @@ export function useGroupItem ( const isSelected = computed(() => { return group.isSelected(id) }) + const isFirst = computed(() => { + return group.items.value[0].id === id + }) + const isLast = computed(() => { + return group.items.value[group.items.value.length - 1].id === id + }) const selectedClass = computed(() => isSelected.value && [group.selectedClass.value, props.selectedClass]) @@ -139,6 +147,8 @@ export function useGroupItem ( return { id, isSelected, + isFirst, + isLast, toggle: () => group.select(id, !isSelected.value), select: (value: boolean) => group.select(id, value), selectedClass, diff --git a/packages/vuetify/src/labs/VStepperVertical/VStepperVertical.tsx b/packages/vuetify/src/labs/VStepperVertical/VStepperVertical.tsx new file mode 100644 index 00000000000..2cd11d98ae4 --- /dev/null +++ b/packages/vuetify/src/labs/VStepperVertical/VStepperVertical.tsx @@ -0,0 +1,138 @@ +// Components +import { VStepperVerticalItem } from './VStepperVerticalItem' +import { makeVExpansionPanelsProps, VExpansionPanels } from '@/components/VExpansionPanel/VExpansionPanels' +import { makeStepperProps } from '@/components/VStepper/VStepper' + +// Composables +import { provideDefaults } from '@/composables/defaults' +import { useProxiedModel } from '@/composables/proxiedModel' + +// Utilities +import { computed, ref, toRefs } from 'vue' +import { genericComponent, getPropertyFromItem, omit, propsFactory, useRender } from '@/util' + +// Types +import type { VStepperSlot } from '@/components/VStepper/VStepper' +import type { StepperItem, StepperItemSlot } from '@/components/VStepper/VStepperItem' + +export type VStepperVerticalSlots = { + actions: StepperItemSlot + default: VStepperSlot & { step: unknown } + icon: StepperItemSlot + title: StepperItemSlot + subtitle: StepperItemSlot + item: StepperItem + prev: StepperItemSlot + next: StepperItemSlot +} & { + [key: `header-item.${string}`]: StepperItemSlot + [key: `item.${string}`]: StepperItem +} + +export const makeVStepperVerticalProps = propsFactory({ + prevText: { + type: String, + default: '$vuetify.stepper.prev', + }, + nextText: { + type: String, + default: '$vuetify.stepper.next', + }, + + ...makeStepperProps(), + ...omit(makeVExpansionPanelsProps({ + mandatory: 'force' as const, + variant: 'accordion' as const, + }), ['static']), +}, 'VStepperVertical') + +export const VStepperVertical = genericComponent()({ + name: 'VStepperVertical', + + props: makeVStepperVerticalProps(), + + emits: { + 'update:modelValue': (val: any) => true, + }, + + setup (props, { slots }) { + const vExpansionPanelsRef = ref() + const { color, editable, prevText, nextText, hideActions } = toRefs(props) + + const model = useProxiedModel(props, 'modelValue') + const items = computed(() => props.items.map((item, index) => { + const title = getPropertyFromItem(item, props.itemTitle, item) + const value = getPropertyFromItem(item, props.itemValue, index + 1) + + return { + title, + value, + raw: item, + } + })) + + provideDefaults({ + VStepperVerticalItem: { + color, + editable, + prevText, + nextText, + hideActions, + static: true, + }, + VStepperActions: { + color, + }, + }) + + useRender(() => { + const expansionPanelProps = VExpansionPanels.filterProps(props) + + return ( + + {{ + ...slots, + default: ({ + prev, + next, + }) => { + return ( + <> + { items.value.map(({ raw, ...item }) => ( + + {{ + ...slots, + default: slots[`item.${item.value}`], + }} + + ))} + + { slots.default?.({ prev, next, step: model.value }) } + + ) + }, + }} + + ) + }) + + return {} + }, +}) + +export type VStepperVertical = InstanceType diff --git a/packages/vuetify/src/labs/VStepperVertical/VStepperVerticalActions.tsx b/packages/vuetify/src/labs/VStepperVertical/VStepperVerticalActions.tsx new file mode 100644 index 00000000000..0246cdaad84 --- /dev/null +++ b/packages/vuetify/src/labs/VStepperVertical/VStepperVerticalActions.tsx @@ -0,0 +1,51 @@ +// Components +import { makeVStepperActionsProps, VStepperActions } from '@/components/VStepper/VStepperActions' + +// Utilities +import { genericComponent, propsFactory, useRender } from '@/util' + +// Types +import type { VStepperActionsSlots } from '@/components/VStepper/VStepperActions' + +export const makeVStepperVerticalActionsProps = propsFactory({ + ...makeVStepperActionsProps(), +}, 'VStepperActions') + +export const VStepperVerticalActions = genericComponent()({ + name: 'VStepperVerticalActions', + + props: makeVStepperVerticalActionsProps(), + + emits: { + 'click:prev': () => true, + 'click:next': () => true, + }, + + setup (props, { emit, slots }) { + function onClickPrev () { + emit('click:prev') + } + + function onClickNext () { + emit('click:next') + } + + useRender(() => { + const stepperActionsProps = VStepperActions.filterProps(props) + + return ( + + ) + }) + + return {} + }, +}) + +export type VStepperVerticalActions = InstanceType diff --git a/packages/vuetify/src/labs/VStepperVertical/VStepperVerticalItem.sass b/packages/vuetify/src/labs/VStepperVertical/VStepperVerticalItem.sass new file mode 100644 index 00000000000..96967cabc2f --- /dev/null +++ b/packages/vuetify/src/labs/VStepperVertical/VStepperVerticalItem.sass @@ -0,0 +1,74 @@ +@use '../../styles/settings' +@use '../../styles/tools' +@use './variables' as * + +.v-stepper-vertical-item + position: relative + transition-duration: $stepper-vertical-item-transition-duration + transition-property: $stepper-vertical-item-transition-property + transition-timing-function: $stepper-vertical-item-transition-timing-function + + &__title + font-size: 1rem + + &__subtitle + font-size: .75rem + + .v-expansion-panel-text + padding-inline-start: 32px + + &:not(:last-child):before + content: '' + position: absolute + width: 2px + height: calc(100% - 30px) + background: rgba(var(--v-border-color), var(--v-border-opacity)) + left: 35px + top: 44px + z-index: 1 + transition-duration: 300ms + transition-property: height + + &:after + display: none + + &.v-expansion-panel--disabled, + &:not(.v-stepper-vertical-item--editable) + .v-expansion-panel-title + pointer-events: none + + .v-expansion-panel-title__overlay + opacity: 0 + +.v-stepper-vertical-item__avatar.v-avatar + background: rgba(var(--v-theme-surface-variant), var(--v-medium-emphasis-opacity)) + color: rgb(var(--v-theme-on-surface-variant)) + transition-property: background + + .v-icon + font-size: .875rem + + .v-expansion-panel--active & + background: rgb(var(--v-theme-surface-variant)) + + .v-stepper-vertical-item--error & + background: rgb(var(--v-theme-error)) + color: rgb(var(--v-theme-on-error)) + +.v-stepper-vertical-item__title + .v-stepper-vertical-item--error & + color: rgb(var(--v-theme-error)) + +.v-stepper-vertical-item__subtitle + .v-stepper-vertical-item--error & + color: rgb(var(--v-theme-error)) + +.v-stepper-vertical-actions + &.v-stepper-actions + .v-btn + margin-inline-end: 8px + + .v-stepper & + justify-content: flex-end + padding: 24px 0 0 + flex-direction: row-reverse diff --git a/packages/vuetify/src/labs/VStepperVertical/VStepperVerticalItem.tsx b/packages/vuetify/src/labs/VStepperVertical/VStepperVerticalItem.tsx new file mode 100644 index 00000000000..fb2f13c0e7c --- /dev/null +++ b/packages/vuetify/src/labs/VStepperVertical/VStepperVerticalItem.tsx @@ -0,0 +1,206 @@ +// Styles +import './VStepperVerticalItem.sass' + +// Components +import { VStepperVerticalActions } from './VStepperVerticalActions' +import { VAvatar } from '@/components/VAvatar/VAvatar' +import { VDefaultsProvider } from '@/components/VDefaultsProvider/VDefaultsProvider' +import { VExpansionPanel } from '@/components/VExpansionPanel' +import { makeVExpansionPanelProps } from '@/components/VExpansionPanel/VExpansionPanel' +import { VIcon } from '@/components/VIcon/VIcon' +import { makeStepperItemProps } from '@/components/VStepper/VStepperItem' + +// Utilities +import { computed, ref } from 'vue' +import { genericComponent, omit, propsFactory, useRender } from '@/util' + +// Types +import type { StepperItemSlot } from '@/components/VStepper/VStepperItem' + +export type VStepperVerticalItemSlots = { + default: StepperItemSlot + icon: StepperItemSlot + subtitle: StepperItemSlot + title: StepperItemSlot + text: StepperItemSlot + prev: StepperItemSlot + next: StepperItemSlot + actions: StepperItemSlot & { + next: () => void + prev: () => void + } +} + +export const makeVStepperVerticalItemProps = propsFactory({ + hideActions: Boolean, + + ...makeStepperItemProps(), + ...omit(makeVExpansionPanelProps({ + expandIcon: '', + collapseIcon: '', + }), ['hideActions']), +}, 'VStepperVerticalItem') + +export const VStepperVerticalItem = genericComponent()({ + name: 'VStepperVerticalItem', + + props: makeVStepperVerticalItemProps(), + + emits: { + 'click:next': () => true, + 'click:prev': () => true, + 'click:finish': () => true, + }, + + setup (props, { emit, slots }) { + const vExpansionPanelRef = ref() + const step = computed(() => !isNaN(parseInt(props.value)) ? Number(props.value) : props.value) + const groupItem = computed(() => vExpansionPanelRef.value?.groupItem) + const isSelected = computed(() => groupItem.value?.isSelected.value ?? false) + const isValid = computed(() => isSelected.value ? props.rules.every(handler => handler() === true) : null) + const canEdit = computed(() => !props.disabled && props.editable) + const hasError = computed(() => props.error || (isSelected.value && !isValid.value)) + const hasCompleted = computed(() => props.complete || (props.rules.length > 0 && isValid.value === true)) + + const disabled = computed(() => { + if (props.disabled) return props.disabled + if (groupItem.value?.isFirst.value) return 'prev' + + return false + }) + const icon = computed(() => { + if (hasError.value) return props.errorIcon + if (hasCompleted.value) return props.completeIcon + if (groupItem.value?.isSelected.value && props.editable) return props.editIcon + + return props.icon + }) + + const slotProps = computed(() => ({ + canEdit: canEdit.value, + hasError: hasError.value, + hasCompleted: hasCompleted.value, + title: props.title, + subtitle: props.subtitle, + step: step.value, + value: props.value, + })) + + const actionProps = computed(() => ({ + ...slotProps.value, + prev: onClickPrev, + next: onClickNext, + })) + + function onClickNext () { + emit('click:next') + + if (groupItem.value?.isLast.value) return + + groupItem.value.group.next() + } + + function onClickPrev () { + emit('click:prev') + + groupItem.value.group.prev() + } + + useRender(() => { + const hasColor = ( + hasCompleted.value || + groupItem.value?.isSelected.value + ) && ( + !hasError.value && + !props.disabled + ) + + const hasActions = !props.hideActions || !!slots.actions + const expansionPanelProps = VExpansionPanel.filterProps(props) + + return ( + + {{ + title: () => ( + <> + + { slots.icon?.(slotProps.value) ?? ( + icon.value ? ( + + ) : step.value + )} + + +
+
+ { slots.title?.(slotProps.value) ?? props.title } +
+ +
+ { slots.subtitle?.(slotProps.value) ?? props.subtitle } +
+
+ + ), + text: () => ( + <> + { slots.default?.(slotProps.value) ?? props.text } + + { hasActions && ( + + { slots.actions?.(actionProps.value) ?? ( + slots.prev?.(actionProps.value) : undefined, + next: slots.next ? () => slots.next?.(actionProps.value) : undefined, + }} + /> + )} + + )} + + ), + }} +
+ ) + }) + + return {} + }, +}) + +export type VStepperVerticalItem = InstanceType diff --git a/packages/vuetify/src/labs/VStepperVertical/_variables.scss b/packages/vuetify/src/labs/VStepperVertical/_variables.scss new file mode 100644 index 00000000000..eecd20e1bcd --- /dev/null +++ b/packages/vuetify/src/labs/VStepperVertical/_variables.scss @@ -0,0 +1,3 @@ +$stepper-vertical-item-transition-duration: .2s !default; +$stepper-vertical-item-transition-property: opacity !default; +$stepper-vertical-item-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1) !default; diff --git a/packages/vuetify/src/labs/VStepperVertical/index.ts b/packages/vuetify/src/labs/VStepperVertical/index.ts new file mode 100644 index 00000000000..dc0127e0bb8 --- /dev/null +++ b/packages/vuetify/src/labs/VStepperVertical/index.ts @@ -0,0 +1,3 @@ +export { VStepperVertical } from './VStepperVertical' +export { VStepperVerticalItem } from './VStepperVerticalItem' +export { VStepperVerticalActions } from './VStepperVerticalActions' diff --git a/packages/vuetify/src/labs/components.ts b/packages/vuetify/src/labs/components.ts index 7f2adedc388..6b753ff8790 100644 --- a/packages/vuetify/src/labs/components.ts +++ b/packages/vuetify/src/labs/components.ts @@ -2,6 +2,7 @@ export * from './VCalendar' export * from './VDateInput' export * from './VNumberInput' export * from './VPicker' +export * from './VStepperVertical' export * from './VPullToRefresh' export * from './VSnackbarQueue' export * from './VTimePicker'