diff --git a/.gitignore b/.gitignore index 791af041b7..4794e3ca67 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ logs .DS_Store .fleet .idea +.vscode # Local env files .env diff --git a/docs/content/docs/2.components/button.md b/docs/content/docs/2.components/button.md index 1aa5258525..3157eb679b 100644 --- a/docs/content/docs/2.components/button.md +++ b/docs/content/docs/2.components/button.md @@ -283,6 +283,47 @@ You can customize this icon globally in your `vite.config.ts` under `ui.icons.lo ::: :: +### Loading Position + +Use the `loading-position` prop to control where the loading icon appears. The prop accepts three values: +- `left`: displays loading icon on the left (default) +- `center`: displays loading icon in the center, hiding the label +- `right`: displays loading icon on the right + +::component-code +--- +props: + loading: true + loadingPosition: 'left' + label: Button +--- +Button +:: + +::component-code +--- +props: + loading: true + loadingPosition: 'center' + label: Button +--- +Button +:: + +::component-code +--- +props: + loading: true + loadingPosition: 'right' + label: Button +--- +Button +:: + +:::tip +The `loading-position` prop maintains full backward compatibility. Existing usage with `leading` and `trailing` props for loading buttons continues to work without any changes. When `loading-position` is not specified, the component automatically determines the position based on the `trailing` prop. +::: + ### Disabled Use the `disabled` prop to disable the Button. diff --git a/playgrounds/nuxt/app/pages/components/button.vue b/playgrounds/nuxt/app/pages/components/button.vue index a8d25610eb..b733ad23c2 100644 --- a/playgrounds/nuxt/app/pages/components/button.vue +++ b/playgrounds/nuxt/app/pages/components/button.vue @@ -28,8 +28,9 @@ function onClick() { - - + + + diff --git a/src/runtime/components/Button.vue b/src/runtime/components/Button.vue index defb42f768..69201074d9 100644 --- a/src/runtime/components/Button.vue +++ b/src/runtime/components/Button.vue @@ -30,6 +30,11 @@ export interface ButtonProps extends UseComponentIconsProps, Omit void | Promise) | Array<((event: MouseEvent) => void | Promise)> class?: any ui?: Button['slots'] @@ -83,9 +88,36 @@ const isLoading = computed(() => { return props.loading || (props.loadingAuto && (loadingAutoState.value || (formLoading?.value && props.type === 'submit'))) }) -const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons( - computed(() => ({ ...props, loading: isLoading.value })) -) +const loadingPosition = computed(() => { + if (props.loadingPosition) { + return props.loadingPosition + } + return props.trailing ? 'right' : 'left' +}) + +const iconProps = computed(() => { + const baseProps = { ...props, loading: isLoading.value } + + if (isLoading.value) { + if (loadingPosition.value === 'right') { + return { ...baseProps, trailing: true } + } else if (loadingPosition.value === 'left') { + return { ...baseProps, trailing: false } + } + return { ...baseProps, trailing: false } + } + + return baseProps +}) + +const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons(iconProps) + +const centerLoadingIcon = computed(() => { + if (isLoading.value && loadingPosition.value === 'center') { + return props.loadingIcon || appConfig.ui.icons.loading + } + return undefined +}) const ui = computed(() => tv({ extend: tv(theme), @@ -126,7 +158,7 @@ const ui = computed(() => tv({ v-bind="slotProps" data-slot="base" :class="ui.base({ - class: [props.ui?.base, props.class], + class: [props.ui?.base, props.class, ...(loadingPosition === 'center' && isLoading ? ['relative'] : [])], active, ...(active && activeVariant ? { variant: activeVariant } : {}), ...(active && activeColor ? { color: activeColor } : {}) @@ -134,18 +166,28 @@ const ui = computed(() => tv({ @click="onClickWrapper" > - - + + - - {{ label }} - + + - + diff --git a/src/theme/button.ts b/src/theme/button.ts index 12998dad05..25aae3f98a 100644 --- a/src/theme/button.ts +++ b/src/theme/button.ts @@ -8,7 +8,9 @@ export default (options: Required) => ({ leadingIcon: 'shrink-0', leadingAvatar: 'shrink-0', leadingAvatarSize: '', - trailingIcon: 'shrink-0' + trailingIcon: 'shrink-0', + loadingIcon: 'shrink-0 size-5', + loadingLabel: 'opacity-0 h-0' }, variants: { ...fieldGroupVariant, @@ -164,6 +166,11 @@ export default (options: Required) => ({ class: { trailingIcon: 'animate-spin' } + }, { + loading: true, + class: { + loadingIcon: 'animate-spin' + } }], defaultVariants: { color: 'primary', diff --git a/test/components/Button.spec.ts b/test/components/Button.spec.ts index 75afdf4f23..c98877e446 100644 --- a/test/components/Button.spec.ts +++ b/test/components/Button.spec.ts @@ -32,6 +32,9 @@ describe('Button', () => { ['with loading and avatar', { props: { loading: true, avatar: { src: 'https://github.com/benjamincanac.png' } } }], ['with loading trailing', { props: { loading: true, trailing: true } }], ['with loading trailing and avatar', { props: { loading: true, trailing: true, avatar: { src: 'https://github.com/benjamincanac.png' } } }], + ['with loadingPosition left', { props: { loading: true, loadingPosition: 'left', label: 'Button' } }], + ['with loadingPosition center', { props: { loading: true, loadingPosition: 'center', label: 'Button' } }], + ['with loadingPosition right', { props: { loading: true, loadingPosition: 'right', label: 'Button' } }], ['with loadingIcon', { props: { loading: true, loadingIcon: 'i-lucide-loader' } }], ['with disabled', { props: { label: 'Button', disabled: true } }], ['with disabled and with link', { props: { label: 'Button', disabled: true, to: '/link' } }], diff --git a/test/components/__snapshots__/Button-vue.spec.ts.snap b/test/components/__snapshots__/Button-vue.spec.ts.snap index b899958e59..cf182a2855 100644 --- a/test/components/__snapshots__/Button-vue.spec.ts.snap +++ b/test/components/__snapshots__/Button-vue.spec.ts.snap @@ -187,6 +187,26 @@ exports[`Button > renders with loadingIcon correctly 1`] = ` " `; +exports[`Button > renders with loadingPosition center correctly 1`] = ` +"" +`; + +exports[`Button > renders with loadingPosition left correctly 1`] = ` +"" +`; + +exports[`Button > renders with loadingPosition right correctly 1`] = ` +"" +`; + exports[`Button > renders with neutral variant ghost correctly 1`] = ` "" `; +exports[`Button > renders with loadingPosition center correctly 1`] = ` +"" +`; + +exports[`Button > renders with loadingPosition left correctly 1`] = ` +"" +`; + +exports[`Button > renders with loadingPosition right correctly 1`] = ` +"" +`; + exports[`Button > renders with neutral variant ghost correctly 1`] = ` "