From 202059ad02b7507b64e23325c49d12e8fc64e7e1 Mon Sep 17 00:00:00 2001 From: Joseph Ojoko Date: Fri, 15 Sep 2023 23:01:27 +0100 Subject: [PATCH] feat: adds badge component --- example/cypress/e2e/overlays/badge.cy.ts | 29 + example/src/App.vue | 1 + example/src/main.ts | 2 + example/src/router/index.ts | 5 + example/src/views/BadgeView.vue | 766 ++++++++++++++++++ package.json | 1 + pnpm-lock.yaml | 23 + src/components/index.ts | 6 + src/components/overlays/badge/Badge.spec.ts | 80 ++ .../overlays/badge/Badge.stories.ts | 162 ++++ src/components/overlays/badge/Badge.vue | 237 ++++++ src/components/tabs/tab-item/TabItem.spec.ts | 4 +- src/components/tabs/tabs/Tabs.stories.ts | 2 +- tests/setup-files/setup.ts | 8 + 14 files changed, 1323 insertions(+), 3 deletions(-) create mode 100644 example/cypress/e2e/overlays/badge.cy.ts create mode 100644 example/src/views/BadgeView.vue create mode 100644 src/components/overlays/badge/Badge.spec.ts create mode 100644 src/components/overlays/badge/Badge.stories.ts create mode 100644 src/components/overlays/badge/Badge.vue diff --git a/example/cypress/e2e/overlays/badge.cy.ts b/example/cypress/e2e/overlays/badge.cy.ts new file mode 100644 index 0000000..739aefb --- /dev/null +++ b/example/cypress/e2e/overlays/badge.cy.ts @@ -0,0 +1,29 @@ +// https://docs.cypress.io/api/introduction/api.html + +describe('Badge', () => { + beforeEach(() => { + cy.visit('/badges'); + }); + + it('checks for and trigger badge', () => { + cy.contains('h2[data-cy=badges]', 'Badges'); + + cy.get('div[data-cy=badge-0]').as('defaultBadge'); + cy.get('@defaultBadge').find('div[role=status]').should('exist'); + + cy.get('@defaultBadge').find('> button').click(); + cy.get('@defaultBadge').find('div[role=status]').should('not.exist'); + cy.get('@defaultBadge').find('> button').click(); + cy.get('@defaultBadge').find('div[role=status]').should('exist'); + }); + + it('checks for and trigger dot badge', () => { + cy.get('div[data-cy=badge-84]').as('dotBadge'); + cy.get('@dotBadge').find('div[role=status]').should('exist'); + + cy.get('@dotBadge').find('> button').click(); + cy.get('@dotBadge').find('div[role=status]').should('not.exist'); + cy.get('@dotBadge').find('> button').click(); + cy.get('@dotBadge').find('div[role=status]').should('exist'); + }); +}); diff --git a/example/src/App.vue b/example/src/App.vue index a89bbe1..66790d8 100644 --- a/example/src/App.vue +++ b/example/src/App.vue @@ -23,6 +23,7 @@ const navigation = ref([ { to: { name: 'data-tables' }, title: 'Data Tables' }, { to: { name: 'cards' }, title: 'Cards' }, { to: { name: 'tabs' }, title: 'Tabs' }, + { to: { name: 'badges' }, title: 'Badges' }, ], }, ]); diff --git a/example/src/main.ts b/example/src/main.ts index f7b7b1a..10c21b2 100644 --- a/example/src/main.ts +++ b/example/src/main.ts @@ -18,6 +18,7 @@ import { RiInformationLine, RiMacbookLine, RiMoonLine, + RiStarFill, RiSunLine, RuiPlugin, } from '@rotki/ui-library-compat'; @@ -29,6 +30,7 @@ Vue.use(PiniaVuePlugin); Vue.use(RuiPlugin, { icons: [ RiMoonLine, + RiStarFill, RiSunLine, RiMacbookLine, RiArrowLeftLine, diff --git a/example/src/router/index.ts b/example/src/router/index.ts index 9c07cb3..a3dfc38 100644 --- a/example/src/router/index.ts +++ b/example/src/router/index.ts @@ -95,6 +95,11 @@ const router = new VueRouter({ name: 'tabs', component: () => import('@/views/TabView.vue'), }, + { + path: '/badges', + name: 'badges', + component: () => import('@/views/BadgeView.vue'), + }, ], }); diff --git a/example/src/views/BadgeView.vue b/example/src/views/BadgeView.vue new file mode 100644 index 0000000..e57cc88 --- /dev/null +++ b/example/src/views/BadgeView.vue @@ -0,0 +1,766 @@ + + + diff --git a/package.json b/package.json index 60c8635..fa664e2 100644 --- a/package.json +++ b/package.json @@ -119,6 +119,7 @@ "@vue/test-utils": "1.3.6", "@vue/tsconfig": "0.4.0", "@vueuse/core": "10.3.0", + "@vueuse/math": "10.4.1", "@vueuse/shared": "10.3.0", "argparse": "2.0.1", "autoprefixer": "10.4.14", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b973cf5..3c43d2d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -81,6 +81,9 @@ importers: '@vueuse/core': specifier: 10.3.0 version: 10.3.0(vue@2.7.14) + '@vueuse/math': + specifier: 10.4.1 + version: 10.4.1(vue@2.7.14) '@vueuse/shared': specifier: 10.3.0 version: 10.3.0(vue@2.7.14) @@ -4803,6 +4806,16 @@ packages: - vue dev: true + /@vueuse/math@10.4.1(vue@2.7.14): + resolution: {integrity: sha512-8XAssBPg6jQ9Z/oD4Yq+gkSjr/r2Sm7pyloWf7i8RQNXiXvf39N0rNZBufFXezKeDa2JmsuMR8JsqlIW7AnG/w==} + dependencies: + '@vueuse/shared': 10.4.1(vue@2.7.14) + vue-demi: 0.14.6(vue@2.7.14) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + dev: true + /@vueuse/metadata@10.3.0: resolution: {integrity: sha512-Ema3YhNOa4swDsV0V7CEY5JXvK19JI/o1szFO1iWxdFg3vhdFtCtSTP26PCvbUpnUtNHBY2wx5y3WDXND5Pvnw==} dev: true @@ -4816,6 +4829,15 @@ packages: - vue dev: true + /@vueuse/shared@10.4.1(vue@2.7.14): + resolution: {integrity: sha512-vz5hbAM4qA0lDKmcr2y3pPdU+2EVw/yzfRsBdu+6+USGa4PxqSQRYIUC9/NcT06y+ZgaTsyURw2I9qOFaaXHAg==} + dependencies: + vue-demi: 0.14.6(vue@2.7.14) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + dev: true + /@webassemblyjs/ast@1.11.6: resolution: {integrity: sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==} dependencies: @@ -12424,6 +12446,7 @@ packages: /vue-router@3.6.5(vue@2.7.14): resolution: {integrity: sha512-VYXZQLtjuvKxxcshuRAwjHnciqZVoXAjTjcqBTz4rKc8qih9g9pI3hbDjmqXaHdgL3v8pV6P8Z335XvHzESxLQ==} + requiresBuild: true peerDependencies: vue: ^2 dependencies: diff --git a/src/components/index.ts b/src/components/index.ts index a840dd4..08266b2 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -20,6 +20,10 @@ import { default as RuiTooltip, type Props as TooltipProps, } from '@/components/overlays/tooltip/Tooltip.vue'; +import { + type Props as BadgeProps, + default as RuiBadge, +} from '@/components/overlays/badge/Badge.vue'; import { default as RuiFooterStepper } from '@/components/steppers/FooterStepper.vue'; import { type Props as ProgressProps, @@ -51,6 +55,7 @@ import { default as RuiTabItem } from '@/components/tabs/tab-item/TabItem.vue'; export { RuiAlert, + RuiBadge, RuiButton, RuiButtonGroup, RuiCheckbox, @@ -83,4 +88,5 @@ export { DataTableOptions, ButtonProps, CardProps, + BadgeProps, }; diff --git a/src/components/overlays/badge/Badge.spec.ts b/src/components/overlays/badge/Badge.spec.ts new file mode 100644 index 0000000..ffa6074 --- /dev/null +++ b/src/components/overlays/badge/Badge.spec.ts @@ -0,0 +1,80 @@ +import { describe, expect, it } from 'vitest'; +import { mount } from '@vue/test-utils'; +import Button from '@/components/buttons/button/Button.vue'; +import Badge from '@/components/overlays/badge/Badge.vue'; + +const createWrapper = (options?: any) => + mount(Badge, { + ...options, + slots: { + default: 'Badge', + }, + global: { + stubs: { 'rui-button': Button }, + }, + }); + +describe('Badge', () => { + it('renders properly', async () => { + const wrapper = createWrapper({ + props: { + text: 'Badge content', + value: false, + }, + }); + + expect(wrapper.find('div[role=status]').exists()).toBeFalsy(); + + await wrapper.setProps({ value: true }); + + expect(wrapper.find('div[role=status]').exists()).toBeTruthy(); + + expect(wrapper.get('div[role=status]').classes()).toMatch(/_badge_/); + expect(wrapper.get('div[role=status]').classes()).toMatch( + /_placement__top_/, + ); + expect(wrapper.get('div[role=status]').classes()).toMatch( + /_rounded__full_/, + ); + expect(wrapper.get('div[role=status]').classes()).toMatch(/_size__md_/); + expect(wrapper.get('div[role=status]').classes()).toMatch(/_primary_/); + }); + + it('passes props correctly', async () => { + const wrapper = createWrapper({ + props: { + text: 'Badge content', + value: false, + }, + }); + + expect(wrapper.find('div[role=status]').exists()).toBeFalsy(); + + await wrapper.setProps({ value: true }); + + expect(wrapper.find('span[class*=_content_]').exists()).toBeTruthy(); + + expect(wrapper.find('div[role=status]').exists()).toBeTruthy(); + + expect(wrapper.find('svg[class*=_remixicon_]').exists()).toBeFalsy(); + + await wrapper.setProps({ icon: 'star-line' }); + + expect(wrapper.find('svg[class*=_remixicon_]').exists()).toBeTruthy(); + + expect(wrapper.get('div[role=status]').classes()).toMatch( + /_rounded__full_/, + ); + + expect(wrapper.get('div[role=status]').classes()).not.toMatch( + /_rounded__sm_/, + ); + + await wrapper.setProps({ rounded: 'sm' }); + + expect(wrapper.get('div[role=status]').classes()).toMatch(/_rounded__sm_/); + expect(wrapper.get('div[role=status]').classes()).not.toMatch( + /_rounded__full_/, + ); + }); +}); diff --git a/src/components/overlays/badge/Badge.stories.ts b/src/components/overlays/badge/Badge.stories.ts new file mode 100644 index 0000000..92f8098 --- /dev/null +++ b/src/components/overlays/badge/Badge.stories.ts @@ -0,0 +1,162 @@ +import { objectOmit } from '@vueuse/shared'; +import Icon from '@/components/icons/Icon.vue'; +import Button from '@/components/buttons/button/Button.vue'; +import { contextColors } from '@/consts/colors'; +import * as Icons from '@/all-icons'; +import Badge, { type Props as BadgeProps } from './Badge.vue'; +import type { Meta, StoryFn, StoryObj } from '@storybook/vue'; + +type Props = BadgeProps & { + buttonText?: string | null; +}; + +const render: StoryFn = (args) => ({ + components: { Badge, Icon, Button }, + setup() { + const modelValue = computed({ + get() { + return args.value; + }, + set(val) { + args.value = val; + }, + }); + + const badgeArgs = computed(() => objectOmit(args, ['buttonText'])); + + return { args, badgeArgs, modelValue }; + }, + template: ` +
+ + + + +
`, +}); + +const meta: Meta = { + title: 'Components/Overlays/Badge', + component: Badge, + tags: ['autodocs'], + render, + argTypes: { + text: { + control: 'text', + }, + icon: { + control: 'select', + options: [null, ...Object.values(Icons).map(({ name }) => name.slice(3))], + }, + color: { control: 'select', options: ['default', ...contextColors] }, + rounded: { control: 'select', options: ['full', 'sm', 'md', 'lg'] }, + placement: { control: 'select', options: ['top', 'center', 'bottom'] }, + size: { control: 'select', options: ['sm', 'md', 'lg'] }, + offsetX: { + control: 'number', + }, + offsetY: { + control: 'number', + }, + }, + args: { + text: '1', + buttonText: 'Badge', + color: 'primary', + rounded: 'full', + size: 'md', + value: true, + icon: null, + dot: false, + left: false, + offsetX: 0, + offsetY: 0, + }, + parameters: { + docs: { + controls: { exclude: ['default', 'badge'] }, + }, + }, +}; + +type Story = StoryObj; + +export const Default: Story = { + args: {}, +}; + +export const Left: Story = { + args: { + left: true, + }, +}; + +export const Center: Story = { + args: { + placement: 'center', + }, +}; + +export const CenterLeft: Story = { + args: { + left: true, + placement: 'center', + }, +}; + +export const Bottom: Story = { + args: { + placement: 'bottom', + }, +}; + +export const BottomLeft: Story = { + args: { + left: true, + placement: 'bottom', + }, +}; + +export const Dot: Story = { + args: { dot: true }, +}; + +export const DotLeft: Story = { + args: { dot: true, left: true }, +}; + +export const DotCenter: Story = { + args: { + dot: true, + placement: 'center', + }, +}; + +export const DotCenterLeft: Story = { + args: { + dot: true, + left: true, + placement: 'center', + }, +}; + +export const DotBottom: Story = { + args: { + dot: true, + placement: 'bottom', + }, +}; + +export const DotBottomLeft: Story = { + args: { + dot: true, + left: true, + placement: 'bottom', + }, +}; + +export default meta; diff --git a/src/components/overlays/badge/Badge.vue b/src/components/overlays/badge/Badge.vue new file mode 100644 index 0000000..0d25ec8 --- /dev/null +++ b/src/components/overlays/badge/Badge.vue @@ -0,0 +1,237 @@ + + + + + diff --git a/src/components/tabs/tab-item/TabItem.spec.ts b/src/components/tabs/tab-item/TabItem.spec.ts index 93fe800..3abff56 100644 --- a/src/components/tabs/tab-item/TabItem.spec.ts +++ b/src/components/tabs/tab-item/TabItem.spec.ts @@ -1,8 +1,8 @@ -import { type ComponentMountingOptions, mount } from '@vue/test-utils'; +import { mount } from '@vue/test-utils'; import { describe, expect, it } from 'vitest'; import TabItem from '@/components/tabs/tab-item/TabItem.vue'; -const createWrapper = (options?: ComponentMountingOptions) => +const createWrapper = (options?: any) => mount(TabItem, { ...options, propsData: { diff --git a/src/components/tabs/tabs/Tabs.stories.ts b/src/components/tabs/tabs/Tabs.stories.ts index 4d1d633..1c89d94 100644 --- a/src/components/tabs/tabs/Tabs.stories.ts +++ b/src/components/tabs/tabs/Tabs.stories.ts @@ -1,4 +1,4 @@ -import { type Meta, type StoryFn, type StoryObj } from '@storybook/vue3'; +import { type Meta, type StoryFn, type StoryObj } from '@storybook/vue'; import { contextColors } from '@/consts/colors'; import Icon from '@/components/icons/Icon.vue'; import Card from '@/components/cards/Card.vue'; diff --git a/tests/setup-files/setup.ts b/tests/setup-files/setup.ts index 8d6763e..c44787b 100644 --- a/tests/setup-files/setup.ts +++ b/tests/setup-files/setup.ts @@ -7,6 +7,14 @@ import { useIcons } from '../../src/composables/icons'; const { registerIcons } = useIcons(); registerIcons(Object.values(Icons)); +const ResizeObserverMock = vi.fn(() => ({ + observe: vi.fn(), + unobserve: vi.fn(), + disconnect: vi.fn(), +})); + +vi.stubGlobal('ResizeObserver', ResizeObserverMock); + vi.mock('vue', async () => { const mod = await vi.importActual('vue'); mod.default.config.devtools = false;