Add badge component (#24)

* added badge component

* revert button changes

* added color vars

* remove unit test for colors

* finish up badge

* add unit tests

* use css vars for offset, improve readability

* use css for positioning

* use css for size

* fix lint

* fix unit tests

* removed unnecessary span

* Tweak markup, use template instead of span

* Use px for offset

* Ignore zero-no-unit in offset default value

* Allow px after 0 value in css

* Use span instead of template

* Update readme and storybook

* Update the tests

Co-authored-by: Rijk van Zanten <rijkvanzanten@me.com>
This commit is contained in:
Nitwel
2020-02-17 21:12:19 +01:00
committed by GitHub
parent 9fcf702b4f
commit a071730068
7 changed files with 391 additions and 2 deletions

View File

@@ -0,0 +1,4 @@
import VBadge from './v-badge.vue';
export { VBadge };
export default VBadge;

View File

@@ -0,0 +1,43 @@
# Badge
```html
<v-badge value="2"><v-icon>ABC</v-icon></v-badge>
```
## Colors
You can set the color, background color and boder color with the `--v-badge-color`, `--v-badge-background-color` and `--v-badge-border-color` css vars respectively:
```html
<v-badge
value="11"
style="--v-badge-color: var(--red-500);"
>
<v-icon>ABC</v-icon>
</v-badge>
```
## Props
| Prop | Description | Default |
|--------------------------|-----------------------------------------------------------------------------|------------------------------------|
| `value` | The value that will be displayed inside the badge Only 2 characters allowed)| `null` |
| `dot` | Only will show a small dot without any content | `false` |
| `bordered` | Shows a border arround the badge | `false` |
| `left` | Aligns the badge on the left side | `false` |
| `bottom` | Aligns the badge on the bottom side | `false` |
| `icon` | Shows an icon instead of text | `null` |
## Slots
N/A
## Events
N/A
## CSS Variables
| Variable | Default |
|-------------------------------------|-------------------------------------------------|
| `--v-badge-color` | `var(--white)` |
| `--v-badge-background-color` | `var(--danger)` |
| `--v-badge-border-color` | `var(--background-color)` |
| `--v-badge-offset-x` | `0px` |
| `--v-badge-offset-y` | `0px` |

View File

@@ -0,0 +1,169 @@
import { withKnobs, text, boolean, number, optionsKnob as options } from '@storybook/addon-knobs';
import Vue from 'vue';
import VBadge from './v-badge.vue';
import VIcon from '../v-icon/';
import markdown from './v-badge.readme.md';
import withPadding from '../../../.storybook/decorators/with-padding';
Vue.component('v-badge', VBadge);
Vue.component('v-icon', VIcon);
export default {
title: 'Components / Badge',
component: VBadge,
decorators: [withKnobs, withPadding],
parameters: {
notes: markdown
}
};
export const withButton = () => ({
props: {
value: {
default: text('Text in badge', '3', 'Badge')
},
dot: {
default: boolean('Dot', false, 'Badge')
},
left: {
default: boolean('Left', false, 'Badge')
},
bottom: {
default: boolean('Bottom', false, 'Badge')
},
offsetX: {
default: number('Offset X', 0, undefined, 'Badge')
},
offsetY: {
default: number('Offset Y', 0, undefined, 'Badge')
},
icon: {
default: text('Icon', '', 'Badge')
},
bordered: {
default: boolean('Bordered', false, 'Badge')
}
},
template: `
<v-badge
:value="value"
:dot="dot"
:left="left"
:bottom="bottom"
:icon="icon"
:bordered="bordered"
:style="{
'--v-badge-offset-x': (offsetX + 'px'),
'--v-badge-offset-y': (offsetY + 'px')
}"
>
<v-button>Click me!</v-button>
</v-badge>
`
});
export const withText = () => ({
props: {
value: {
default: text('Text in badge', '3', 'Badge')
},
dot: {
default: boolean('Dot', false, 'Badge')
},
left: {
default: boolean('Left', false, 'Badge')
},
bottom: {
default: boolean('Bottom', false, 'Badge')
},
offsetX: {
default: number('Offset X', 0, undefined, 'Badge')
},
offsetY: {
default: number('Offset Y', 0, undefined, 'Badge')
},
icon: {
default: text('Icon', '', 'Badge')
},
bordered: {
default: boolean('Bordered', false, 'Badge')
}
},
template: `
<v-badge
:value="value"
:dot="dot"
:left="left"
:bottom="bottom"
:icon="icon"
:bordered="bordered"
:style="{
'--v-badge-offset-x': (offsetX + 'px'),
'--v-badge-offset-y': (offsetY + 'px')
}"
>
Example text.
</v-badge>
`
});
export const withAvatar = () => ({
props: {
value: {
default: text('Text in badge', '3', 'Badge')
},
dot: {
default: boolean('Dot', false, 'Badge')
},
left: {
default: boolean('Left', false, 'Badge')
},
bottom: {
default: boolean('Bottom', false, 'Badge')
},
icon: {
default: text('Icon', '', 'Badge')
}
},
template: `
<v-badge
:value="value"
:dot="dot"
:left="left"
:bottom="bottom"
:icon="icon"
:bordered="true"
:style="{
'--v-badge-offset-x': '3px',
'--v-badge-offset-y': '3px'
}"
>
<v-avatar>RVZ</v-avatar>
</v-badge>
`
});
export const colors = () => ({
template: `
<div>
<v-badge
style="--v-badge-color: var(--white);--v-badge-background-color: var(--red-500); margin-right: 20px"
value="9+"
>
<v-button>Click me!</v-button>
</v-badge>
<v-badge
style="--v-badge-color: var(--white);--v-badge-background-color: var(--green-500); margin-right: 20px"
icon="spa"
>
<v-button>Click me!</v-button>
</v-badge>
<v-badge
style="--v-badge-color: var(--white);--v-badge-background-color: var(--orange-500)"
icon="notifications_active"
>
<v-button>Click me!</v-button>
</v-badge>
</div>
`
});

View File

@@ -0,0 +1,68 @@
import { mount, createLocalVue, Wrapper } from '@vue/test-utils';
import VueCompositionAPI from '@vue/composition-api';
const localVue = createLocalVue();
localVue.use(VueCompositionAPI);
localVue.component('v-icon', VIcon);
import VBadge from './v-badge.vue';
import VIcon from '../v-icon/';
describe('Chip', () => {
let component: Wrapper<Vue>;
beforeEach(() => {
component = mount(VBadge, { localVue });
});
it('Adds the dot prop to the badge', async () => {
component.setProps({
dot: true
});
await component.vm.$nextTick();
expect(component.find('.badge').classes()).toContain('dot');
});
it('Adds the bordered prop to the badge', async () => {
component.setProps({
bordered: true
});
await component.vm.$nextTick();
expect(component.find('.badge').classes()).toContain('bordered');
});
it('Display the badge on the left', async () => {
component.setProps({
left: true
});
await component.vm.$nextTick();
expect(component.find('.badge').classes()).toContain('left');
});
it('Display the badge on the bottom', async () => {
component.setProps({
bottom: true
});
await component.vm.$nextTick();
expect(component.find('.badge').classes()).toContain('bottom');
});
it('Checks if the icon exists and if the name matches', async () => {
component.setProps({
icon: 'add'
});
await component.vm.$nextTick();
expect(component.find('.v-icon').exists()).toBe(true);
expect(component.find('.v-icon').props().name).toBe('add');
});
});

View File

@@ -0,0 +1,105 @@
<template>
<div class="v-badge" :class="{ dot, bordered }">
<span class="badge" :class="{ dot, bordered, left, bottom }">
<v-icon v-if="icon" :name="icon" :color="color" x-small />
<span v-else>{{ value }}</span>
</span>
<slot></slot>
</div>
</template>
<script lang="ts">
import { createComponent, reactive, computed, Ref } from '@vue/composition-api';
export default createComponent({
props: {
value: {
type: String,
default: null
},
dot: {
type: Boolean,
default: false
},
left: {
type: Boolean,
default: false
},
bottom: {
type: Boolean,
default: false
},
icon: {
type: String,
default: null
},
bordered: {
type: Boolean,
default: false
}
}
});
</script>
<style lang="scss" scoped>
.v-badge {
--v-badge-color: var(--white);
--v-badge-background-color: var(--danger);
--v-badge-border-color: var(--background-color);
--v-badge-offset-x: 0px;
--v-badge-offset-y: 0px;
--v-badge-size: 20px;
position: relative;
display: inline-block;
&.bordered {
--v-badge-size: 24px;
}
&.dot {
--v-badge-size: 12px;
&.bordered {
--v-badge-size: 16px;
}
}
.badge {
position: absolute;
top: calc(var(--v-badge-size) / -2 + var(--v-badge-offset-y));
right: calc(var(--v-badge-size) / -2 + var(--v-badge-offset-x));
z-index: 10;
display: inline-flex;
align-items: center;
justify-content: center;
width: max-content;
min-width: var(--v-badge-size);
height: var(--v-badge-size);
padding: 0 4px;
color: var(--v-badge-color);
font-size: 12px;
background-color: var(--v-badge-background-color);
border-radius: calc(var(--v-badge-size) / 2);
&.left {
right: unset;
left: calc(var(--v-badge-size) / -2 + var(--v-badge-offset-x));
}
&.bottom {
top: unset;
bottom: calc(var(--v-badge-size) / -2 + var(--v-badge-offset-y));
}
&.dot * {
display: none;
}
&.bordered {
border: 2px solid var(--v-badge-border-color);
}
}
}
</style>

View File

@@ -1,4 +1,3 @@
/* stylelint-disable custom-property-empty-line-before */
:root {
--transition: cubic-bezier(0.4, 0, 0.2, 1);
--transition-in: cubic-bezier(0, 0, 0.2, 1);