Skip to content

Commit

Permalink
feat: 增加按钮组
Browse files Browse the repository at this point in the history
  • Loading branch information
moyuand committed Nov 26, 2024
1 parent 7805085 commit d69095b
Show file tree
Hide file tree
Showing 8 changed files with 251 additions and 79 deletions.
126 changes: 124 additions & 2 deletions packages/components/Button/Button.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { describe, it, expect } from 'vitest'
import { describe, it, expect, test } from 'vitest'
import { mount } from '@vue/test-utils'
import Button from './Button.vue'
import Icon from '../Icon/Icon.vue';
import ButtonGroup from './ButtonGroup.vue';
import type { ButtonType, ButtonSize } from './types';

describe('Button.vue', ()=> {
Expand Down Expand Up @@ -67,4 +69,124 @@ describe('Button.vue', ()=> {
await wrapper.trigger("click");
expect(wrapper.emitted().click).toHaveLength(1);
});
})

// Exception Handling: loading state
it("should display loading icon and not emit click event when button is loading", async () => {
const wrapper = mount(Button, {
props: { loading: true },
global: {
stubs: ["MyIcon"],
}
});
const iconElement = wrapper.findComponent(Icon);

expect(wrapper.find('.loading-icon').exists()).toBe(true);
expect(iconElement.exists()).toBeTruthy();
expect(iconElement.attributes('icon')).toBe('spinner');
await wrapper.trigger('click');
expect(wrapper.emitted('click')).toBeUndefined();
})

test("loading button", () => {
const wrapper = mount(Button, {
props: {
loading: true,
},
slots: {
default: "loading button",
},
global: {
stubs: ["MyIcon"],
},
});

// class
expect(wrapper.classes()).toContain("is-loading");

// attrs
expect(wrapper.attributes("disabled")).toBeDefined();
expect(wrapper.find("button").element.disabled).toBeTruthy();

// events
wrapper.get("button").trigger("click");
expect(wrapper.emitted()).not.toHaveProperty("click");

// icon
const iconElement = wrapper.findComponent(Icon);
expect(iconElement.exists()).toBeTruthy();
expect(iconElement.attributes("icon")).toBe("spinner");
});

test("icon button", () => {
const wrapper = mount(Button, {
props: {
icon: "arrow-up",
},
slots: {
default: "icon button",
},
global: {
stubs: ["MyIcon"],
},
});

const iconElement = wrapper.findComponent(Icon);
expect(iconElement.exists()).toBeTruthy();
expect(iconElement.attributes("icon")).toBe("arrow-up");
});
})

describe("ButtonGroup.vue", () => {
test("basic button group", async () => {
const wrapper = mount(() => (
<ButtonGroup>
<Button>button 1</Button>
<Button>button 2</Button>
</ButtonGroup>
));

expect(wrapper.classes()).toContain("my-button-group");
});

test("button group size", () => {
const sizes = ["large", "default", "small"];
sizes.forEach((size) => {
const wrapper = mount(() => (
<ButtonGroup size={size as any}>
<Button>button 1</Button>
<Button>button 2</Button>
</ButtonGroup>
));

const buttonWrapper = wrapper.findComponent(Button);
expect(buttonWrapper.classes()).toContain(`my-button--${size}`);
});
});

test("button group type", () => {
const types = ["primary", "success", "warning", "danger", "info"];
types.forEach((type) => {
const wrapper = mount(() => (
<ButtonGroup type={type as any}>
<Button>button 1</Button>
<Button>button 2</Button>
</ButtonGroup>
));

const buttonWrapper = wrapper.findComponent(Button);
expect(buttonWrapper.classes()).toContain(`my-button--${type}`);
});
});

test("button group disabled", () => {
const wrapper = mount(() => (
<ButtonGroup disabled>
<Button>button 1</Button>
<Button>button 2</Button>
</ButtonGroup>
));

const buttonWrapper = wrapper.findComponent(Button);
expect(buttonWrapper.classes()).toContain(`is-disabled`);
});
});
8 changes: 7 additions & 1 deletion packages/components/Button/Button.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
<script setup lang="ts">
import type { ButtonProps, ButtonEmits, ButtonInstance } from './types';
import { ref, type Ref, computed } from 'vue';
import { ref, type Ref, computed, inject } from 'vue';
import { throttle } from 'lodash-es'
import MyIcon from '../Icon/Icon.vue';
import {BUTTON_GROUP_CTX_KEY} from './contants'
defineOptions({
name: 'MyButton'
Expand All @@ -22,6 +23,11 @@ const slots = defineSlots();
const _ref = ref<HTMLButtonElement>();
const ctx = inject(BUTTON_GROUP_CTX_KEY, void 0)
const size = computed(() => ctx?.size ?? props.size ?? 'default')
const disabled = computed(() => ctx?.disabled || props.disabled || false)
const type = computed(() => ctx?.type ?? props.type ?? 'primary')
const iconStyle = computed(() => ({
marginRight: slots.default ? '6px' : '0'
}))
Expand Down
24 changes: 24 additions & 0 deletions packages/components/Button/ButtonGroup.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<script lang="ts" setup>
import type { ButtonGroupProps, } from './types';
import { provide, toRef, reactive } from 'vue';
import { BUTTON_GROUP_CTX_KEY } from './contants';
defineOptions({
name: 'MyButtonGroup',
})
const props = defineProps<ButtonGroupProps>()
provide(BUTTON_GROUP_CTX_KEY, reactive({
disabled: toRef(props, 'disabled'),
size: toRef(props, 'size'),
type: toRef(props, 'type'),
}))
</script>
<template>
<div class="my-button-group">
<slot></slot>
</div>
</template>
<style scoped>
@import './style.css';
</style>
6 changes: 6 additions & 0 deletions packages/components/Button/contants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { InjectionKey } from "vue";
import type { ButtonGroupContext } from "./types";

export const BUTTON_GROUP_CTX_KEY: InjectionKey<ButtonGroupContext> =
Symbol("BUTTON_GROUP_CTX_KEY");

2 changes: 2 additions & 0 deletions packages/components/Button/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import Button from './Button.vue'
import ButtonGroup from './ButtonGroup.vue'
import { withInstall } from '@moyu-ui/utils'

export const MyButton = withInstall(Button)
export const MyButtonGroup = withInstall(ButtonGroup)

export * from './types'
14 changes: 13 additions & 1 deletion packages/components/Button/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,16 @@ export interface ButtonInstance {
// disabled: ComputedRef<boolean>;
// size: ComputedRef<string>;
// type: ComputedRef<string>;
}
}

export interface ButtonGroupProps {
size?: string;
disabled?: boolean;
type?: string;
}

export interface ButtonGroupContext {
size?: string;
disabled?: boolean;
type? : string;
}
4 changes: 2 additions & 2 deletions packages/core/components.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { MyButton, MyIcon } from "@moyu-ui/components";
import { MyButton, MyIcon, MyButtonGroup } from "@moyu-ui/components";
import type { Plugin } from "vue";

export default [MyButton, MyIcon ] as Plugin[];
export default [MyButton, MyIcon, MyButtonGroup ] as Plugin[];
146 changes: 73 additions & 73 deletions packages/play/src/stories/Button.stories.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Meta, StoryObj, ArgTypes } from "@storybook/vue3";
import { fn, within, userEvent, expect } from "@storybook/test";
import { MyButton } from "moyu-ui";
import { MyButton, MyButtonGroup } from "moyu-ui";
// import "moyu-ui/dist/theme/Button.css";

type Story = StoryObj<typeof MyButton> & { argTypes?: ArgTypes };
Expand Down Expand Up @@ -87,80 +87,80 @@ export const Default: Story & { args: { content: string } } = {
},
};

// export const Circle: Story = {
// args: {
// icon: "search",
// },
// render: (args) => ({
// components: { MyButton },
// setup() {
// return { args };
// },
// template: container(`
// <er-button circle v-bind="args"/>
// `),
// }),
// play: async ({ canvasElement, args, step }) => {
// const canvas = within(canvasElement);
// await step("click button", async () => {
// await userEvent.click(canvas.getByRole("button"));
// });
export const Circle: Story = {
args: {
icon: "search",
},
render: (args) => ({
components: { MyButton },
setup() {
return { args };
},
template: container(`
<my-button circle v-bind="args"/>
`),
}),
play: async ({ canvasElement, args, step }) => {
const canvas = within(canvasElement);
await step("click button", async () => {
await userEvent.click(canvas.getByRole("button"));
});

// expect(args.onClick).toHaveBeenCalled();
// },
// };
expect(args.onClick).toHaveBeenCalled();
},
};

// Circle.parameters = {};
Circle.parameters = {};

// export const Group: Story & { args: { content1: string; content2: string } } = {
// argTypes: {
// groupType: {
// control: { type: "select" },
// options: ["primary", "success", "warning", "danger", "info", ""],
// },
// groupSize: {
// control: { type: "select" },
// options: ["large", "default", "small", ""],
// },
// groupDisabled: {
// control: "boolean",
// },
// content1: {
// control: { type: "text" },
// defaultValue: "Button1",
// },
// content2: {
// control: { type: "text" },
// defaultValue: "Button2",
// },
// },
// args: {
// round: true,
// content1: "Button1",
// content2: "Button2",
// },
// render: (args) => ({
// components: { ErButton, ErButtonGroup },
// setup() {
// return { args };
// },
// template: container(`
// <er-button-group :type="args.groupType" :size="args.groupSize" :disabled="args.groupDisabled">
// <er-button v-bind="args">{{args.content1}}</er-button>
// <er-button v-bind="args">{{args.content2}}</er-button>
// </er-button-group>
// `),
// }),
// play: async ({ canvasElement, args, step }) => {
// const canvas = within(canvasElement);
// await step("click btn1", async () => {
// await userEvent.click(canvas.getByText("Button1"));
// });
// await step("click btn2", async () => {
// await userEvent.click(canvas.getByText("Button2"));
// });
// expect(args.onClick).toHaveBeenCalled();
// },
// };
export const Group: Story & { args: { content1: string; content2: string } } = {
argTypes: {
groupType: {
control: { type: "select" },
options: ["primary", "success", "warning", "danger", "info", ""],
},
groupSize: {
control: { type: "select" },
options: ["large", "default", "small", ""],
},
groupDisabled: {
control: "boolean",
},
content1: {
control: { type: "text" },
defaultValue: "Button1",
},
content2: {
control: { type: "text" },
defaultValue: "Button2",
},
},
args: {
round: true,
content1: "Button1",
content2: "Button2",
},
render: (args) => ({
components: { MyButton, MyButtonGroup },
setup() {
return { args };
},
template: container(`
<my-button-group :type="args.groupType" :size="args.groupSize" :disabled="args.groupDisabled">
<my-button v-bind="args">{{args.content1}}</my-button>
<my-button v-bind="args">{{args.content2}}</my-button>
</my-button-group>
`),
}),
play: async ({ canvasElement, args, step }) => {
const canvas = within(canvasElement);
await step("click btn1", async () => {
await userEvent.click(canvas.getByText("Button1"));
});
await step("click btn2", async () => {
await userEvent.click(canvas.getByText("Button2"));
});
expect(args.onClick).toHaveBeenCalled();
},
};

export default meta;

0 comments on commit d69095b

Please sign in to comment.