feat: Attributify button component (#10473)
This PR allows attributify for `variant`, `size` and `color` props. This allows using shorthands, instant of writing full props. We also added a small computed method to ensure these does not show up in the DOM and pollute it --------- Co-authored-by: Pranav <pranav@chatwoot.com>
This commit is contained in:
@@ -1,46 +1,85 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, useSlots } from 'vue';
|
import { computed, useSlots, useAttrs } from 'vue';
|
||||||
|
|
||||||
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
|
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
|
||||||
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||||
|
import {
|
||||||
|
VARIANT_OPTIONS,
|
||||||
|
COLOR_OPTIONS,
|
||||||
|
SIZE_OPTIONS,
|
||||||
|
EXCLUDED_ATTRS,
|
||||||
|
} from './constants.js';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
label: {
|
label: { type: [String, Number], default: '' },
|
||||||
type: [String, Number],
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
variant: {
|
variant: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'solid',
|
default: null,
|
||||||
validator: value =>
|
validator: value => VARIANT_OPTIONS.includes(value) || value === null,
|
||||||
['solid', 'outline', 'faded', 'link', 'ghost'].includes(value),
|
|
||||||
},
|
},
|
||||||
color: {
|
color: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'blue',
|
default: null,
|
||||||
validator: value =>
|
validator: value => COLOR_OPTIONS.includes(value) || value === null,
|
||||||
['blue', 'ruby', 'amber', 'slate', 'teal'].includes(value),
|
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'md',
|
default: null,
|
||||||
validator: value => ['xs', 'sm', 'md', 'lg'].includes(value),
|
validator: value => SIZE_OPTIONS.includes(value) || value === null,
|
||||||
},
|
|
||||||
icon: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
trailingIcon: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
isLoading: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
},
|
||||||
|
icon: { type: [String, Object, Function], default: '' },
|
||||||
|
trailingIcon: { type: Boolean, default: false },
|
||||||
|
isLoading: { type: Boolean, default: false },
|
||||||
});
|
});
|
||||||
|
|
||||||
const slots = useSlots();
|
const slots = useSlots();
|
||||||
|
const attrs = useAttrs();
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
inheritAttrs: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const filteredAttrs = computed(() => {
|
||||||
|
const standardAttrs = {};
|
||||||
|
|
||||||
|
Object.entries(attrs)
|
||||||
|
.filter(([key]) => !EXCLUDED_ATTRS.includes(key))
|
||||||
|
.forEach(([key, value]) => {
|
||||||
|
standardAttrs[key] = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
return standardAttrs;
|
||||||
|
});
|
||||||
|
|
||||||
|
const computedVariant = computed(() => {
|
||||||
|
if (props.variant) return props.variant;
|
||||||
|
// The useAttrs method returns attributes values an empty string (not boolean value as in props).
|
||||||
|
if (attrs.solid || attrs.solid === '') return 'solid';
|
||||||
|
if (attrs.outline || attrs.outline === '') return 'outline';
|
||||||
|
if (attrs.faded || attrs.faded === '') return 'faded';
|
||||||
|
if (attrs.link || attrs.link === '') return 'link';
|
||||||
|
if (attrs.ghost || attrs.ghost === '') return 'ghost';
|
||||||
|
return 'solid'; // Default variant
|
||||||
|
});
|
||||||
|
|
||||||
|
const computedColor = computed(() => {
|
||||||
|
if (props.color) return props.color;
|
||||||
|
if (attrs.blue || attrs.blue === '') return 'blue';
|
||||||
|
if (attrs.ruby || attrs.ruby === '') return 'ruby';
|
||||||
|
if (attrs.amber || attrs.amber === '') return 'amber';
|
||||||
|
if (attrs.slate || attrs.slate === '') return 'slate';
|
||||||
|
if (attrs.teal || attrs.teal === '') return 'teal';
|
||||||
|
return 'blue'; // Default color
|
||||||
|
});
|
||||||
|
|
||||||
|
const computedSize = computed(() => {
|
||||||
|
if (props.size) return props.size;
|
||||||
|
if (attrs.xs || attrs.xs === '') return 'xs';
|
||||||
|
if (attrs.sm || attrs.sm === '') return 'sm';
|
||||||
|
if (attrs.md || attrs.md === '') return 'md';
|
||||||
|
if (attrs.lg || attrs.lg === '') return 'lg';
|
||||||
|
return 'md';
|
||||||
|
});
|
||||||
|
|
||||||
const STYLE_CONFIG = {
|
const STYLE_CONFIG = {
|
||||||
colors: {
|
colors: {
|
||||||
@@ -113,23 +152,24 @@ const STYLE_CONFIG = {
|
|||||||
const variantClasses = computed(() => {
|
const variantClasses = computed(() => {
|
||||||
const variantMap = {
|
const variantMap = {
|
||||||
ghost: 'text-n-slate-12 hover:bg-n-alpha-2 outline-transparent',
|
ghost: 'text-n-slate-12 hover:bg-n-alpha-2 outline-transparent',
|
||||||
link: `${STYLE_CONFIG.colors[props.color].link} p-0 font-medium underline-offset-4`,
|
link: `${STYLE_CONFIG.colors[computedColor.value].link} p-0 font-medium underline-offset-4`,
|
||||||
outline: STYLE_CONFIG.colors[props.color].outline,
|
outline: STYLE_CONFIG.colors[computedColor.value].outline,
|
||||||
faded: STYLE_CONFIG.colors[props.color].faded,
|
faded: STYLE_CONFIG.colors[computedColor.value].faded,
|
||||||
solid: STYLE_CONFIG.colors[props.color].solid,
|
solid: STYLE_CONFIG.colors[computedColor.value].solid,
|
||||||
};
|
};
|
||||||
|
|
||||||
return variantMap[props.variant];
|
return variantMap[computedVariant.value];
|
||||||
});
|
});
|
||||||
|
|
||||||
const isIconOnly = computed(() => !props.label && !slots.default);
|
const isIconOnly = computed(() => !props.label && !slots.default);
|
||||||
const isLink = computed(() => props.variant === 'link');
|
const isLink = computed(() => computedVariant.value === 'link');
|
||||||
|
|
||||||
const buttonClasses = computed(() => {
|
const buttonClasses = computed(() => {
|
||||||
const sizeConfig = isIconOnly.value ? 'iconOnly' : 'regular';
|
const sizeConfig = isIconOnly.value ? 'iconOnly' : 'regular';
|
||||||
const classes = [
|
const classes = [
|
||||||
variantClasses.value,
|
variantClasses.value,
|
||||||
props.variant !== 'link' && STYLE_CONFIG.sizes[sizeConfig][props.size],
|
computedVariant.value !== 'link' &&
|
||||||
|
STYLE_CONFIG.sizes[sizeConfig][computedSize.value],
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
|
|
||||||
return classes.join(' ');
|
return classes.join(' ');
|
||||||
@@ -138,7 +178,7 @@ const buttonClasses = computed(() => {
|
|||||||
const linkButtonClasses = computed(() => {
|
const linkButtonClasses = computed(() => {
|
||||||
const classes = [
|
const classes = [
|
||||||
variantClasses.value,
|
variantClasses.value,
|
||||||
STYLE_CONFIG.sizes.link[props.size],
|
STYLE_CONFIG.sizes.link[computedSize.value],
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
|
|
||||||
return classes.join(' ');
|
return classes.join(' ');
|
||||||
@@ -147,10 +187,11 @@ const linkButtonClasses = computed(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<button
|
<button
|
||||||
|
v-bind="filteredAttrs"
|
||||||
:class="{
|
:class="{
|
||||||
[STYLE_CONFIG.base]: true,
|
[STYLE_CONFIG.base]: true,
|
||||||
[isLink ? linkButtonClasses : buttonClasses]: true,
|
[isLink ? linkButtonClasses : buttonClasses]: true,
|
||||||
[STYLE_CONFIG.fontSize[size]]: true,
|
[STYLE_CONFIG.fontSize[computedSize]]: true,
|
||||||
'flex-row-reverse': trailingIcon && !isIconOnly,
|
'flex-row-reverse': trailingIcon && !isIconOnly,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
|
|||||||
15
app/javascript/dashboard/components-next/button/constants.js
Normal file
15
app/javascript/dashboard/components-next/button/constants.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
export const VARIANT_OPTIONS = ['solid', 'outline', 'faded', 'link', 'ghost'];
|
||||||
|
export const COLOR_OPTIONS = ['blue', 'ruby', 'amber', 'slate', 'teal'];
|
||||||
|
export const SIZE_OPTIONS = ['xs', 'sm', 'md', 'lg'];
|
||||||
|
|
||||||
|
export const EXCLUDED_ATTRS = [
|
||||||
|
'variant',
|
||||||
|
'color',
|
||||||
|
'size',
|
||||||
|
'icon',
|
||||||
|
'trailingIcon',
|
||||||
|
'isLoading',
|
||||||
|
...VARIANT_OPTIONS,
|
||||||
|
...COLOR_OPTIONS,
|
||||||
|
...SIZE_OPTIONS,
|
||||||
|
];
|
||||||
Reference in New Issue
Block a user