-
Notifications
You must be signed in to change notification settings - Fork 468
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(Ladder): new component #976
Changes from all commits
cff504a
42d9c04
8a15097
5e0e209
84643c9
8c5b98e
15124b1
26b1638
90d26d6
4eb5090
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
<template> | ||
<ULadder :steps="steps" /> | ||
</template> | ||
|
||
<script setup> | ||
const steps = [ | ||
{ label: 'Address', icon: 'i-heroicons-home', color: 'red' }, | ||
{ label: 'Shipping', icon: 'i-heroicons-truck', inactiveColor: 'cyan' }, | ||
{ label: 'Trade-in', icon: 'i-heroicons-arrow-path', disabled: true }, | ||
{ label: 'Payment', icon: 'i-heroicons-banknotes', click: () => alert('You clicked Payment!') }, | ||
{ label: 'Checkout', icon: 'i-heroicons-check' } | ||
] | ||
</script> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
--- | ||
description: 'Display a progress as a list of ordered steps.' | ||
links: | ||
- label: GitHub | ||
icon: i-simple-icons-github | ||
to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/data/Table.vue | ||
--- | ||
|
||
## Usage | ||
|
||
Pass the step index as `v-model`, an array of steps as strings to the `steps` prop of the Ladder component. | ||
|
||
::component-card | ||
--- | ||
extraClass: overflow-x-auto | ||
baseProps: | ||
steps: | ||
- Address | ||
- Shipping | ||
- Trade-in | ||
- Payment | ||
- Checkout | ||
props: | ||
modelValue: 3 | ||
options: | ||
- name: modelValue | ||
restriction: only | ||
values: | ||
- 0 | ||
- 1 | ||
- 2 | ||
- 3 | ||
- 4 | ||
--- | ||
:: | ||
|
||
You may also pass an array of objects with following properties: | ||
|
||
- `label` - The label of the step. | ||
- `icon` - The icon of the step. | ||
- `disabled` - Whether the step is disabled. | ||
- `color` - The active color of the step. | ||
- `inactiveColor` - The inactive color of the step. | ||
- `click` - The click handler of the step. | ||
- `to` - Execute navigation to the set destination. | ||
|
||
:component-example{component="ladder-example-step-objects" extraClass="overflow-x-auto"} | ||
|
||
::callout{icon="i-heroicons-exclamation-triangle"} | ||
The `click` handler will take precedence over the navigation handler. If `click` is set to `false`, the navigation handler is disabled, even if `to` is declared. | ||
:: | ||
|
||
## Click | ||
|
||
Steps don't change when the user clicks on it. To enable this behaviour, you can either set the `click` prop to `true`, or on each step setting `click` to `true`. | ||
|
||
::component-card | ||
--- | ||
extraClass: overflow-x-auto | ||
baseProps: | ||
modelValue: 0 | ||
steps: | ||
- Address | ||
- Shipping | ||
- Trade-in | ||
- Payment | ||
- Checkout | ||
props: | ||
click: true | ||
--- | ||
:: | ||
|
||
## Separators | ||
|
||
The steps are separated with horizontal lines that can be disabled by setting `separators` to `false`. | ||
|
||
::component-card | ||
--- | ||
extraClass: overflow-x-auto | ||
baseProps: | ||
steps: | ||
- Address | ||
- Shipping | ||
- Trade-in | ||
- Payment | ||
- Checkout | ||
props: | ||
separators: false | ||
--- | ||
:: | ||
|
||
## Style | ||
|
||
Steps can be either active or inactive. You may change the color of each state using the corresponding prop: | ||
|
||
- `color` for an active step | ||
- `inactiveColor` for an inactive step | ||
|
||
Besides all the colors from the `ui.colors` object, you can also use the `gray`. | ||
|
||
::component-card | ||
--- | ||
extraClass: overflow-x-auto | ||
baseProps: | ||
steps: | ||
- Address | ||
- Shipping | ||
- Trade-in | ||
- Payment | ||
- Checkout | ||
click: true | ||
props: | ||
color: red | ||
inactiveColor: blue | ||
--- | ||
:: | ||
|
||
## Props | ||
|
||
:component-props | ||
|
||
## Config | ||
|
||
:component-preset |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,185 @@ | ||
<template> | ||
<ol :class="ui.base" v-bind="attrs"> | ||
<template v-for="(wrapper, key) in computedSteps" :key="key"> | ||
<li :class="ui.container"> | ||
<div :class="ui.step"> | ||
<div :class="ui.indicator"> | ||
<div v-if="hasSeparators" :class="ui.separator.wrapper"> | ||
<div :class="[ui.separator.base, ui.separator.color, key < 1 ? 'invisible' : '']" /> | ||
<div :class="[ui.separator.base, ui.separator.color, key > computedSteps.length - 2 ? 'invisible' : '']" class="ml-10" /> | ||
</div> | ||
<div :class="ui.icon.wrapper"> | ||
<component | ||
:is="wrapper.step.click || wrapper.step.to ? 'UButton' : 'div'" | ||
:class="[ui.icon.base, ui.icon.rounded, ui.icon.shadow, ui.icon.size, ui.icon.ring, getIconClass(wrapper)]" | ||
type="button" | ||
variant="ghost" | ||
v-bind="omit(wrapper.step, ['label', 'click', 'icon'])" | ||
@click="wrapper.step.click" | ||
> | ||
<UIcon v-if="wrapper.step.icon" :name="wrapper.step.icon" /> | ||
<div v-else> | ||
{{ key + 1 }} | ||
</div> | ||
</component> | ||
</div> | ||
</div> | ||
|
||
<div :class="[ui.label.base, ui.label.size, getLabelClass(wrapper)]"> | ||
{{ wrapper.step.label }} | ||
</div> | ||
</div> | ||
</li> | ||
</template> | ||
</ol> | ||
</template> | ||
|
||
<script lang="ts"> | ||
import { computed, defineComponent, toRef } from 'vue' | ||
import type { ComputedRef, PropType } from 'vue' | ||
import { twJoin } from 'tailwind-merge' | ||
import { useUI } from '../../composables/useUI' | ||
import { mergeConfig, omit } from '../../utils' | ||
import type { Strategy, LadderSize, LadderStep, LadderStepWrapper, LadderStepColor } from '../../types' | ||
// @ts-expect-error | ||
import appConfig from '#build/app.config' | ||
import { ladder } from '#ui/ui.config' | ||
import UButton from './Link.vue' | ||
|
||
const config = mergeConfig<typeof ladder>(appConfig.ui.strategy, appConfig.ui.ladder, ladder) | ||
|
||
export default defineComponent({ | ||
components: { | ||
UButton | ||
}, | ||
inheritAttrs: false, | ||
props: { | ||
modelValue: { | ||
type: Number, | ||
default: 0 | ||
}, | ||
steps: { | ||
type: Array as PropType<String[] | LadderStep[]>, | ||
default: () => [] | ||
}, | ||
separators: { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could be nice to call this |
||
type: Boolean, | ||
default: true | ||
}, | ||
color: { | ||
type: String as PropType<LadderStepColor>, | ||
default: () => config.default.color, | ||
validator (value: string) { | ||
return [...appConfig.ui.colors, 'gray'].includes(value) | ||
} | ||
}, | ||
inactiveColor: { | ||
type: String as PropType<LadderStepColor>, | ||
default: () => config.default.inactiveColor, | ||
validator (value: string) { | ||
return [...appConfig.ui.colors, 'gray'].includes(value) | ||
} | ||
}, | ||
click: { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't this be handled in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You know? You're right if you think that the normal implementation for this would be to not have any click behavior and let the changes be done outside, like a form. I just thought that it would be faster to implement a way to let the dev handle the click for changing steps in one variable rather than adding the handler on each item. Can I set then the |
||
type: Boolean, | ||
default: false | ||
}, | ||
class: { | ||
type: [String, Object, Array] as PropType<any>, | ||
default: undefined | ||
}, | ||
ui: { | ||
type: Object as PropType<Partial<typeof config & { strategy?: Strategy }>>, | ||
default: undefined | ||
} | ||
}, | ||
emits: ['update:modelValue'], | ||
setup (props, { emit }) { | ||
const { ui, attrs } = useUI('ladder', toRef(props, 'ui'), config, toRef(props, 'class')) | ||
|
||
const hasSeparators = computed(() => props.separators) | ||
|
||
function getIconClass (wrapper: LadderStepWrapper) { | ||
const color = { | ||
key: 'inactive', | ||
name: wrapper.step.inactiveColor ?? props.inactiveColor | ||
} | ||
|
||
if (wrapper.isActive) { | ||
color.key = 'active' | ||
color.name = wrapper.step.color ?? props.color | ||
} | ||
|
||
return twJoin( | ||
ui.value.icon[color.key].background.replaceAll('{color}', color.name), | ||
ui.value.icon[color.key].ring.replaceAll('{color}', color.name), | ||
ui.value.icon[color.key].color.replaceAll('{color}', color.name), | ||
ui.value.icon[color.key].shadow | ||
) | ||
} | ||
|
||
function getLabelClass (wrapper: LadderStepWrapper) { | ||
const color = { | ||
key: 'inactive', | ||
name: wrapper.step.inactiveColor ?? props.inactiveColor | ||
} | ||
|
||
if (wrapper.isActive) { | ||
color.key = 'active' | ||
color.name = wrapper.step.color ?? props.color | ||
} | ||
|
||
return twJoin( | ||
ui.value.label[color.key].replaceAll('{color}', color.name) | ||
) | ||
} | ||
|
||
const ladderStepActiveIconClass = computed(() => { | ||
return twJoin( | ||
ui.value.icon.active.background.replaceAll('{color}', props.color), | ||
ui.value.icon.active.ring.replaceAll('{color}', props.color), | ||
ui.value.icon.active.color.replaceAll('{color}', props.color), | ||
ui.value.icon.active.shadow | ||
) | ||
}) | ||
|
||
const ladderStepInactiveIconClass = computed(() => { | ||
return twJoin( | ||
ui.value.icon.inactive.background.replaceAll('{color}', props.inactiveColor), | ||
ui.value.icon.inactive.ring.replaceAll('{color}', props.inactiveColor), | ||
ui.value.icon.inactive.color.replaceAll('{color}', props.inactiveColor), | ||
ui.value.icon.inactive.shadow | ||
) | ||
}) | ||
|
||
const computedSteps: ComputedRef<LadderStepWrapper[]> = computed(() => { | ||
return props.steps.map((receivedStep, index: number) => { | ||
const step = typeof receivedStep === 'string' ? { label: receivedStep } : receivedStep | ||
|
||
const isActive = index === props.modelValue | ||
|
||
if (isActive) { | ||
step.click = false | ||
} else if (props.click || step.click === true) { | ||
step.click = () => emit('update:modelValue', index) | ||
} | ||
|
||
return { step, isActive } | ||
}) | ||
}) | ||
|
||
return { | ||
attrs, | ||
// eslint-disable-next-line vue/no-dupe-keys | ||
ui, | ||
hasSeparators, | ||
getIconClass, | ||
getLabelClass, | ||
ladderStepActiveIconClass, | ||
ladderStepInactiveIconClass, | ||
computedSteps, | ||
omit | ||
} | ||
} | ||
}) | ||
</script> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import { RouteParams } from 'vue-router' | ||
import { ladder } from '../ui.config' | ||
import colors from '#ui-colors' | ||
|
||
export type LadderSize = keyof typeof ladder.icon.size | ||
export type LadderStepColor = 'gray' | typeof colors[number] | ||
|
||
export interface LadderStep { | ||
label: string, | ||
to?: RouteParams | string, | ||
click?: Function | boolean, | ||
icon?: string, | ||
disabled?: boolean, | ||
color?: LadderStepColor, | ||
inactiveColor?: LadderStepColor | ||
} | ||
|
||
export interface LadderStepWrapper { | ||
step: LadderStep, | ||
isActive: boolean, | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we can call this
items
like inFeed
?