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:
Shivam Mishra
2024-11-22 03:18:25 +05:30
committed by GitHub
parent 79daf56c31
commit 497bc055a2
2 changed files with 90 additions and 34 deletions

View File

@@ -1,46 +1,85 @@
<script setup>
import { computed, useSlots } from 'vue';
import { computed, useSlots, useAttrs } from 'vue';
import Spinner from 'dashboard/components-next/spinner/Spinner.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({
label: {
type: [String, Number],
default: '',
},
label: { type: [String, Number], default: '' },
variant: {
type: String,
default: 'solid',
validator: value =>
['solid', 'outline', 'faded', 'link', 'ghost'].includes(value),
default: null,
validator: value => VARIANT_OPTIONS.includes(value) || value === null,
},
color: {
type: String,
default: 'blue',
validator: value =>
['blue', 'ruby', 'amber', 'slate', 'teal'].includes(value),
default: null,
validator: value => COLOR_OPTIONS.includes(value) || value === null,
},
size: {
type: String,
default: 'md',
validator: value => ['xs', 'sm', 'md', 'lg'].includes(value),
},
icon: {
type: String,
default: '',
},
trailingIcon: {
type: Boolean,
default: false,
},
isLoading: {
type: Boolean,
default: false,
default: null,
validator: value => SIZE_OPTIONS.includes(value) || value === null,
},
icon: { type: [String, Object, Function], default: '' },
trailingIcon: { type: Boolean, default: false },
isLoading: { type: Boolean, default: false },
});
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 = {
colors: {
@@ -113,23 +152,24 @@ const STYLE_CONFIG = {
const variantClasses = computed(() => {
const variantMap = {
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`,
outline: STYLE_CONFIG.colors[props.color].outline,
faded: STYLE_CONFIG.colors[props.color].faded,
solid: STYLE_CONFIG.colors[props.color].solid,
link: `${STYLE_CONFIG.colors[computedColor.value].link} p-0 font-medium underline-offset-4`,
outline: STYLE_CONFIG.colors[computedColor.value].outline,
faded: STYLE_CONFIG.colors[computedColor.value].faded,
solid: STYLE_CONFIG.colors[computedColor.value].solid,
};
return variantMap[props.variant];
return variantMap[computedVariant.value];
});
const isIconOnly = computed(() => !props.label && !slots.default);
const isLink = computed(() => props.variant === 'link');
const isLink = computed(() => computedVariant.value === 'link');
const buttonClasses = computed(() => {
const sizeConfig = isIconOnly.value ? 'iconOnly' : 'regular';
const classes = [
variantClasses.value,
props.variant !== 'link' && STYLE_CONFIG.sizes[sizeConfig][props.size],
computedVariant.value !== 'link' &&
STYLE_CONFIG.sizes[sizeConfig][computedSize.value],
].filter(Boolean);
return classes.join(' ');
@@ -138,7 +178,7 @@ const buttonClasses = computed(() => {
const linkButtonClasses = computed(() => {
const classes = [
variantClasses.value,
STYLE_CONFIG.sizes.link[props.size],
STYLE_CONFIG.sizes.link[computedSize.value],
].filter(Boolean);
return classes.join(' ');
@@ -147,10 +187,11 @@ const linkButtonClasses = computed(() => {
<template>
<button
v-bind="filteredAttrs"
:class="{
[STYLE_CONFIG.base]: true,
[isLink ? linkButtonClasses : buttonClasses]: true,
[STYLE_CONFIG.fontSize[size]]: true,
[STYLE_CONFIG.fontSize[computedSize]]: true,
'flex-row-reverse': trailingIcon && !isIconOnly,
}"
>