feat: add button component (#515)

Signed-off-by: Ryan Wang <i@ryanc.cc>
pull/548/head
Ryan Wang 3 years ago committed by GitHub
parent 6858ab3be0
commit 9815a30744
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -7,20 +7,22 @@
"build": "vue-tsc --noEmit && vite build",
"preview": "vite preview --port 5050",
"test:unit": "vitest --environment jsdom --run",
"test:unit:watch": "vitest --environment jsdom --watch",
"test:unit:coverage": "vitest run --environment jsdom --coverage",
"test:e2e": "start-server-and-test preview http://127.0.0.1:5050/ 'cypress open'",
"test:e2e:ci": "start-server-and-test preview http://127.0.0.1:5050/ 'cypress run'",
"typecheck": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false",
"typecheck": "vue-tsc --noEmit -p tsconfig.app.json --composite false",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
"story:dev": "histoire dev",
"story:build": "histoire build"
},
"dependencies": {
"pinia": "^2.0.11",
"pinia": "^2.0.12",
"vue": "^3.2.31",
"vue-router": "^4.0.13"
"vue-router": "^4.0.14"
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.1.0",
"@rushstack/eslint-patch": "^1.1.1",
"@types/jsdom": "^16.2.14",
"@types/node": "^16.11.26",
"@vitejs/plugin-vue": "^2.2.4",
@ -29,16 +31,17 @@
"@vue/eslint-config-typescript": "^10.0.0",
"@vue/test-utils": "^2.0.0-rc.18",
"@vue/tsconfig": "^0.1.3",
"autoprefixer": "^10.4.2",
"cypress": "^9.5.1",
"eslint": "^8.10.0",
"autoprefixer": "^10.4.4",
"c8": "^7.11.0",
"cypress": "^9.5.2",
"eslint": "^8.11.0",
"eslint-plugin-cypress": "^2.12.1",
"eslint-plugin-vue": "^8.5.0",
"histoire": "^0.1.3",
"histoire": "^0.2.0",
"husky": "^7.0.4",
"jsdom": "^19.0.0",
"postcss": "^8.4.8",
"prettier": "^2.5.1",
"postcss": "^8.4.12",
"prettier": "^2.6.0",
"sass": "^1.49.9",
"start-server-and-test": "^1.14.0",
"tailwindcss": "^3.0.23",

File diff suppressed because it is too large Load Diff

@ -2,10 +2,39 @@
<Story title="Button">
<Variant title="Type">
<template #default>
<div class="themeable-default">
<VButton type="primary"> Hello world </VButton>
<VButton type="secondary"> Hello world </VButton>
</div>
<VButton type="primary"> primary </VButton>
<VButton type="secondary"> secondary </VButton>
<VButton type="danger"> danger </VButton>
</template>
</Variant>
<Variant title="Size">
<template #default>
<VButton type="secondary" size="lg"> lg </VButton>
<VButton type="secondary"> default </VButton>
<VButton type="secondary" size="sm"> sm </VButton>
<VButton type="secondary" size="xs"> xs </VButton>
</template>
</Variant>
<Variant title="Circle">
<template #default>
<VButton type="secondary" size="lg" circle> lg </VButton>
<VButton type="secondary" circle> d </VButton>
<VButton type="secondary" size="sm" circle> sm </VButton>
<VButton type="secondary" size="xs" circle> xs </VButton>
</template>
</Variant>
<Variant title="Block">
<template #default>
<VButton type="primary" block> primary </VButton>
<VButton type="secondary" block> secondary </VButton>
<VButton type="danger" block> danger </VButton>
</template>
</Variant>
<Variant title="Disabled">
<template #default>
<VButton type="primary" disabled> primary </VButton>
<VButton type="secondary" disabled> secondary </VButton>
<VButton type="danger" disabled> danger </VButton>
</template>
</Variant>
</Story>

@ -1,13 +1,19 @@
<template>
<button :class="[`btn-${size}`, `btn-${type}`]" class="btn">
<button
class="btn"
:class="classes"
:disabled="disabled"
@click="handleClick"
>
<slot />
</button>
</template>
<script lang="ts" setup>
import type { PropType } from "vue";
import type { Size, Type } from "@/components/base/button/interface";
import { computed } from "vue";
defineProps({
const props = defineProps({
type: {
type: String as PropType<Type>,
default: "default",
@ -16,26 +22,124 @@ defineProps({
type: String as PropType<Size>,
default: "md",
},
circle: {
type: Boolean,
default: false,
},
block: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
loading: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(["click"]);
const classes = computed(() => {
return [
`btn-${props.size}`,
`btn-${props.type}`,
{ "btn-circle": props.circle },
{ "btn-block": props.block },
];
});
function handleClick() {
if (props.disabled || props.loading) return;
emit("click");
}
</script>
<style>
<style lang="scss">
.btn {
border-radius: 2px;
border-radius: 4px;
@apply inline-flex;
@apply flex-shrink-0;
@apply cursor-pointer;
@apply border-none;
@apply select-none;
@apply flex-wrap;
@apply items-center;
@apply justify-center;
@apply transition-all;
@apply text-center;
@apply text-sm;
@apply no-underline;
@apply h-9;
@apply px-4;
@apply outline-0;
&:hover {
@apply opacity-90;
}
&:active {
@apply opacity-100;
}
&:disabled {
@apply opacity-50;
}
}
.btn-primary {
background: #4ccba0;
@apply text-white;
}
.btn-secondary {
@apply bg-themeable-secondary;
background: #0e1731;
@apply text-white;
}
.btn-primary {
@apply bg-themeable-primary;
@apply text-black;
.btn-danger {
background: #d71d1d;
@apply text-white;
}
.btn-block {
@apply w-full;
@apply block;
}
.btn-lg {
@apply h-11;
@apply px-5;
@apply text-lg;
}
.btn-sm {
@apply h-7;
@apply px-3;
@apply text-xs;
}
.btn-xs {
@apply h-6;
@apply px-2;
@apply text-xs;
}
.btn-circle {
@apply w-9;
@apply p-0;
@apply rounded-full;
}
.btn-lg.btn-circle {
@apply w-11;
}
.btn-sm.btn-circle {
@apply w-7;
}
.btn-xs.btn-circle {
@apply w-6;
}
</style>

@ -0,0 +1,74 @@
import { describe, expect, it, vi } from "vitest";
import { VButton } from "../index";
import { mount } from "@vue/test-utils";
describe("Button", () => {
it("should render", () => {
expect(mount(VButton).html()).contains("button");
});
it("should render with text", () => {
expect(mount(VButton, { slots: { default: "Hello Halo" } }).text()).toBe(
"Hello Halo"
);
});
it("should work with type prop", () => {
// default button type is default
expect(mount(VButton).find(".btn").classes()).toContain("btn-default");
["primary", "secondary", "danger"].forEach((type) => {
const button = mount(VButton, { props: { type } });
expect(button.find(".btn").classes()).toContain(`btn-${type}`);
button.unmount();
});
});
it("should work with size prop", async () => {
// default button size is md
expect(mount(VButton).find(".btn").classes()).toContain("btn-md");
["lg", "sm", "xs"].forEach((size) => {
const button = mount(VButton, { props: { size } });
expect(button.find(".btn").classes()).toContain(`btn-${size}`);
button.unmount();
});
});
it("should work with circle prop", async () => {
const button = mount(VButton);
// default: false
expect(button.find(".btn").classes()).not.toContain("btn-circle");
await button.setProps({ circle: true });
expect(button.find(".btn").classes()).toContain("btn-circle");
});
it("should work with block prop", async () => {
const button = mount(VButton);
// default: false
expect(button.find(".btn").classes()).not.toContain("btn-block");
await button.setProps({ block: true });
expect(button.find(".btn").classes()).toContain("btn-block");
});
it("should work with disabled prop", async () => {
const onClick = vi.fn(() => 1);
// default: false
const button = mount(VButton, {
emits: { click: onClick },
});
await button.trigger("click");
expect(onClick).toHaveBeenCalled();
onClick.mockReset();
await button.setProps({ disabled: true });
await button.trigger("click");
expect(onClick).not.toHaveBeenCalled();
});
});
Loading…
Cancel
Save