Skip to content

Commit

Permalink
✨ feat(button): add disabled and loading state
Browse files Browse the repository at this point in the history
  • Loading branch information
HoshinoSuzumi committed Nov 21, 2024
1 parent d913f91 commit ed419ef
Show file tree
Hide file tree
Showing 8 changed files with 267 additions and 14 deletions.
2 changes: 1 addition & 1 deletion docs/content/1.getting-started/1.index.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ title: Introduction
description: Multi-purpose customizable components for RayineSoft projects
---

Rayine UI is a collection of multi-purpose customizable components for RayineSoft projects.
RayineUI is a multi-purpose customizable UI library for RayineSoft projects.

This project aims to facilitate sharing a component library across multiple projects for my own use. Open-sourcing it is just a bonus. Therefore, I am under no obligation to meet your requirements, and breaking changes may occur at any time. Of course, pull requests are welcome.

Expand Down
52 changes: 52 additions & 0 deletions docs/content/2.components/button.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ Button

### Variants

#### soft

::ComponentPreview
---
props:
Expand All @@ -20,6 +22,36 @@ props:
Button
::

#### outline

::ComponentPreview
---
props:
variant: outline
---
Button
::

#### ghost

::ComponentPreview
---
props:
variant: ghost
---
Button
::

#### link

::ComponentPreview
---
props:
variant: link
---
Button
::

### Colors

::ComponentPreview
Expand All @@ -29,3 +61,23 @@ props:
---
Button
::

### Disabled

::ComponentPreview
---
props:
disabled: true
---
Button
::

### Loading

::ComponentPreview
---
props:
loading: true
---
Button
::
4 changes: 2 additions & 2 deletions docs/pages/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ const router = useRouter()
</div>
</div>
<div class="flex justify-center items-center gap-4">
<RayButton @click="router.push('/getting-started')">
<RayButton to="/getting-started">
Getting Started
</RayButton>
<RayButton variant="outline" @click="router.push('/components')">
<RayButton variant="outline" to="/components">
Explore Components
</RayButton>
</div>
Expand Down
47 changes: 36 additions & 11 deletions src/runtime/components/elements/Button.vue
Original file line number Diff line number Diff line change
@@ -1,18 +1,27 @@
<script lang="ts" setup>
import { twJoin, twMerge } from 'tailwind-merge'
import { computed, toRef, type PropType } from 'vue'
import { button } from '../../ui.config'
import type { DeepPartial, Strategy } from '../../types/utils'
import type { ButtonColor, ButtonSize, ButtonVariant } from '../../types/button'
import { getNonUndefinedValuesFromObject } from '../../utils'
import { nuxtLinkProps } from '../../utils/link'
import { button } from '../../ui.config'
import { useRayUI } from '#build/imports'
const config = button
const props = defineProps({
...nuxtLinkProps,
class: {
type: String,
default: '',
},
disabled: {
type: Boolean,
default: false,
},
loading: {
type: Boolean,
default: false,
},
padded: {
type: Boolean,
default: true,
Expand All @@ -25,6 +34,14 @@ const props = defineProps({
type: Boolean,
default: false,
},
label: {
type: String,
default: '',
},
to: {
type: String,
default: '',
},
size: {
type: String as PropType<ButtonSize>,
default: () => button.default.size,
Expand All @@ -37,13 +54,19 @@ const props = defineProps({
type: String as PropType<ButtonVariant>,
default: () => button.default.variant,
},
loadingIcon: {
type: String,
default: () => button.default.loadingIcon,
},
ui: {
type: Object as PropType<DeepPartial<typeof config> & { strategy?: Strategy }>,
type: Object as PropType<DeepPartial<typeof button> & { strategy?: Strategy }>,
default: () => ({}),
},
})
const { ui, attrs } = useRayUI('button', toRef(props, 'ui'), config)
const extProps = computed(() => getNonUndefinedValuesFromObject(props, nuxtLinkProps))
const { ui, attrs } = useRayUI('button', toRef(props, 'ui'), button)
const buttonClass = computed(() => {
// @ts-ignore
Expand All @@ -61,12 +84,14 @@ const buttonClass = computed(() => {
</script>

<template>
<button
:class="buttonClass"
v-bind="{ ...attrs }"
>
<slot />
</button>
<RayLink type="button" :disabled="disabled || loading" :class="buttonClass" v-bind="{ ...extProps, ...attrs }">
<slot name="leading" :disabled="disabled" :loading="loading">
<IconSpinner v-if="loading" class="mr-1" />
</slot>
<slot>
<span v-if="label">{{ label }}</span>
</slot>
</RayLink>
</template>

<style scoped></style>
65 changes: 65 additions & 0 deletions src/runtime/components/elements/Link.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<script lang="ts" setup>
import { nuxtLinkProps } from '../../utils/link'
const props = defineProps({
...nuxtLinkProps,
to: {
type: String,
default: '',
},
as: {
type: String,
default: 'button',
},
type: {
type: String,
default: 'button',
},
disabled: {
type: Boolean,
default: false,
},
active: {
type: Boolean,
default: undefined,
},
activeClass: {
type: String,
default: undefined,
},
inactiveClass: {
type: String,
default: undefined,
},
})
</script>

<template>
<component
:is="as"
v-if="!to"
:type="type"
:class="active ? activeClass : inactiveClass"
:disabled="disabled"
v-bind="$attrs"
>
<slot v-bind="{ isActive: active }" />
</component>
<NuxtLink v-else v-slot="{ href, target, rel, navigate, isActive, isExternal }" v-bind="props" custom>
<a
v-bind="$attrs"
:href="!disabled ? href : undefined"
:aria-disabled="disabled ? 'true' : undefined"
:role="disabled ? 'link' : undefined"
:rel="rel"
:target="target"
:class="active !== undefined ? (active ? activeClass : inactiveClass) : { [activeClass]: isActive, [inactiveClass]: !isActive }"
:tabindex="!disabled ? undefined : -1"
@click="(e) => (!disabled && !isExternal) && navigate(e)"
>
<slot v-bind="{ isActive: active !== undefined ? active : isActive }" />
</a>
</NuxtLink>
</template>

<style scoped></style>
13 changes: 13 additions & 0 deletions src/runtime/ui.config/elements/button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,18 @@ export default {
'lg': 'p-2.5',
'xl': 'p-2.5',
},
icon: {
base: 'flex-shrink-0',
loading: 'animate-spin',
size: {
'2xs': 'h-4 w-4',
'xs': 'h-4 w-4',
'sm': 'h-5 w-5',
'md': 'h-5 w-5',
'lg': 'h-5 w-5',
'xl': 'h-6 w-6',
},
},
color: {},
variant: {
solid:
Expand All @@ -43,5 +55,6 @@ export default {
size: 'sm',
color: 'primary',
variant: 'solid',
loadingIcon: 'loading',
},
}
83 changes: 83 additions & 0 deletions src/runtime/utils/link.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import type { PropType } from 'vue'
import type { RouteLocationRaw } from '#vue-router'
import type { NuxtLinkProps } from '#app'

// NuxtLink props
// ref: https://github.com/nuxt/ui/blob/51c8b8e3e59d7eceff72625650a199fcf7c6feca/src/runtime/utils/link.ts#L5-L81
export const nuxtLinkProps = {
to: {
type: [String, Object] as PropType<RouteLocationRaw>,
default: undefined,
required: false,
},
href: {
type: [String, Object] as PropType<RouteLocationRaw>,
default: undefined,
required: false,
},

// Attributes
target: {
type: String as PropType<NuxtLinkProps['target']>,
default: undefined,
required: false,
},
rel: {
type: String as PropType<any>,
default: undefined,
required: false,
},
noRel: {
type: Boolean as PropType<NuxtLinkProps['noRel']>,
default: undefined,
required: false,
},

// Prefetching
prefetch: {
type: Boolean as PropType<NuxtLinkProps['prefetch']>,
default: undefined,
required: false,
},
noPrefetch: {
type: Boolean as PropType<NuxtLinkProps['noPrefetch']>,
default: undefined,
required: false,
},

// Styling
activeClass: {
type: String as PropType<NuxtLinkProps['activeClass']>,
default: undefined,
required: false,
},
exactActiveClass: {
type: String as PropType<NuxtLinkProps['exactActiveClass']>,
default: undefined,
required: false,
},
prefetchedClass: {
type: String as PropType<NuxtLinkProps['prefetchedClass']>,
default: undefined,
required: false,
},

// Vue Router's `<RouterLink>` additional props
replace: {
type: Boolean as PropType<NuxtLinkProps['replace']>,
default: undefined,
required: false,
},
ariaCurrentValue: {
type: String as PropType<NuxtLinkProps['ariaCurrentValue']>,
default: undefined,
required: false,
},

// Edge cases handling
external: {
type: Boolean as PropType<NuxtLinkProps['external']>,
default: undefined,
required: false,
},
} as const
15 changes: 15 additions & 0 deletions src/runtime/utils/objectUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,18 @@ export const getValueByPath = (

return result !== undefined ? result : defaultValue
}

export const getNonUndefinedValuesFromObject = (
obj: object,
srcObj: object,
): object => {
const keys = Object.keys(srcObj) as []

return keys.reduce((acc, key) => {
if (obj[key] !== undefined) {
acc[key] = obj[key]
}

return acc
}, {})
}

0 comments on commit ed419ef

Please sign in to comment.