Chip Component (#1)

* initial commit

* finished main features and added docs

* add unit tests

* fix "visible" unit test

* changed active prop to v-if

* sync support for active prop

* clear code

* Remove button description from chip readme

* Misc tweaks in story / readme

* Minor code style tweaks

* fix initial render

* Use 4px based height

* Add missing tests + missing debug route

Co-authored-by: Rijk van Zanten <rijkvanzanten@me.com>
This commit is contained in:
Nitwel
2020-02-12 00:42:31 +01:00
committed by GitHub
parent 773e963524
commit 87c0eaa9a9
9 changed files with 761 additions and 5 deletions

View File

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

View File

@@ -0,0 +1,67 @@
# Chip
```html
<v-chip>I'm a chip!</v-chip>
```
## Sizes
The chip component supports the following sizes through the use of props:
* x-small
* small
* (default)
* large
* x-large
```html
<v-chip x-small>I'm a chip!</v-chip>
```
## Colors
You can set the color, background color, hover color, and background hover color with the `color`, `background-color`, `hover-color`, and `hover-background-color` props respectively:
```html
<v-chip
color="--red"
background-color="--red-50"
hover-color="--white"
hover-background-color="--red"
>
I'm a chip!
</v-chip>
```
## Events
There are two events, one when clicking on the chip called `click` and one when clicking on the enabled close icon called `close`.
```html
<v-chip @click="sayHi">Hello!</v-chip>
<v-chip close @close="close">I'm closeable!</v-chip>
```
| Event | Description |
|---------|------------------------------------------------------------------------------------------------|
| `click` | Triggers when clicked somewhere on the chip |
| `close` | Triggers when the `close` prop is enabled and gets clicked (Doesn't trigger the `click` event) |
## Props
| Prop | Description | Default |
|--------------------------|------------------------------------------------------|-----------------------------------------|
| `active` | Change visibility. Can be reacted to via `sync` | `true` |
| `close` | Displays a close icon which triggers the close event | `false` |
| `closeIcon` | Which icon should be displayed instead of `close ` | `close` |
| `outlined` | No background | `false` |
| `color` | Text color | `--chip-primary-text-color` |
| `hover-color` | Text color on hover | `--chip-primary-text-color` |
| `background-color` | Chip color | `--chip-primary-background-color` |
| `hover-background-color` | Chip color on hover | `--chip-primary-background-color-hover` |
| `label` | Label style | `false` |
| `disabled` | Disabled state | `false` |
| `x-small` | Render extra small | `false` |
| `small` | Render small | `false` |
| `large` | Render large | `false` |
| `x-large` | Render extra large | `false` |

View File

@@ -0,0 +1,261 @@
import { withKnobs, text, boolean, number, optionsKnob as options } from '@storybook/addon-knobs';
import { action } from '@storybook/addon-actions';
import Vue from 'vue';
import VChip from './v-chip.vue';
import VIcon from '../v-icon/';
import markdown from './v-chip.readme.md';
import withPadding from '../../../.storybook/decorators/with-padding';
Vue.component('v-chip', VChip);
Vue.component('v-icon', VIcon);
export default {
title: 'Components / Chip',
component: VChip,
decorators: [withKnobs, withPadding],
parameters: {
notes: markdown
}
};
export const withText = () => ({
methods: { onClick: action('click'), onClose: action('close') },
props: {
text: {
default: text('Text in chip', 'Click me')
},
label: {
default: boolean('Label', false, 'Button')
},
outlined: {
default: boolean('Outlined', false, 'Button')
},
size: {
default: options(
'Size',
{
'Extra Small': 'xSmall',
Small: 'small',
'(default)': 'default',
Large: 'large',
'Extra Large': 'xLarge'
},
'default',
{
display: 'select'
},
'Button'
)
},
close: {
default: boolean('Close', false, 'Button')
},
disabled: {
default: boolean('Disabled', false, 'Button')
},
active: {
default: boolean('Active', true, 'Button')
},
color: {
default: text('Color', '--chip-primary-text-color', 'Colors')
},
backgroundColor: {
default: text('Background Color', '--chip-primary-background-color', 'Colors')
},
hoverColor: {
default: text('Color (hover)', '--chip-primary-text-color', 'Colors')
},
hoverBackgroundColor: {
default: text(
'Background Color (hover)',
'--chip-primary-background-color-hover',
'Colors'
)
}
},
template: `
<v-chip
:active.sync="active"
:label="label"
:outlined="outlined"
:color="color"
:close="close"
:background-color="backgroundColor"
:hover-color="hoverColor"
:hover-background-color="hoverBackgroundColor"
:disabled="disabled"
:x-small="size === 'xSmall'"
:small="size === 'small'"
:large="size === 'large'"
:x-large="size === 'xLarge'"
@click="onClick"
@close="onClose"
>
{{ text }}
</v-chip>
`
});
export const withIcon = () => ({
methods: { onClick: action('click'), onClose: action('close') },
props: {
iconName: {
default: text('Material Icon', 'add')
},
text: {
default: text('Text in chip', 'Click me')
},
label: {
default: boolean('Label', false, 'Button')
},
outlined: {
default: boolean('Outlined', false, 'Button')
},
size: {
default: options(
'Size',
{
'Extra Small': 'xSmall',
Small: 'small',
'(default)': 'default',
Large: 'large',
'Extra Large': 'xLarge'
},
'default',
{
display: 'select'
},
'Button'
)
},
iconSize: {
default: options(
'Size (Icon)',
{
'Extra Small': 'xSmall',
Small: 'small',
'(default)': 'default',
Large: 'large',
'Extra Large': 'xLarge'
},
'default',
{
display: 'select'
},
'Button'
)
},
close: {
default: boolean('Close', false, 'Button')
},
disabled: {
default: boolean('Disabled', false, 'Button')
},
active: {
default: boolean('Active', true, 'Button')
},
color: {
default: text('Color', '--chip-primary-text-color', 'Colors')
},
backgroundColor: {
default: text('Background Color', '--chip-primary-background-color', 'Colors')
},
hoverColor: {
default: text('Color (hover)', '--chip-primary-text-color', 'Colors')
},
hoverBackgroundColor: {
default: text(
'Background Color (hover)',
'--chip-primary-background-color-hover',
'Colors'
)
}
},
template: `
<v-chip
:active="active"
:label="label"
:outlined="outlined"
:color="color"
:close="close"
:background-color="backgroundColor"
:hover-color="hoverColor"
:hover-background-color="hoverBackgroundColor"
:disabled="disabled"
:x-small="size === 'xSmall'"
:small="size === 'small'"
:large="size === 'large'"
:x-large="size === 'xLarge'"
@click="onClick"
@close="onClose"
>
<v-icon
:name="iconName"
:x-small="iconSize === 'xSmall'"
:small="iconSize === 'small'"
:large="iconSize === 'large'"
:x-large="iconSize === 'xLarge'"
left
/>
{{ text }}
</v-chip>
`
});
export const withColor = () => ({
template: `
<div>
<v-chip
color="--white"
background-color="--red-600"
hover-color="--white"
hover-background-color="--red-400"
>
<v-icon
name="delete"
color="--white"
left
/>
Delete
</v-chip>
<v-chip
color="--white"
background-color="--green-600"
hover-color="--white"
hover-background-color="--green-400"
>
<v-icon
name="add"
color="--white"
left
/>
Add Item
</v-chip>
<v-chip
color="--white"
background-color="--amber-600"
hover-color="--white"
hover-background-color="--amber-400"
>
<v-icon
name="warning"
color="--white"
left
/>
Watch out
</v-chip>
</div>
`
});
export const sizes = () => ({
template: `
<div>
<v-chip x-small>Extra small</v-chip>
<v-chip small>Small</v-chip>
<v-chip>Default</v-chip>
<v-chip large>Large</v-chip>
<v-chip x-large>Extra large</v-chip>
</div>
`
});

View File

@@ -0,0 +1,183 @@
import { mount, createLocalVue, Wrapper } from '@vue/test-utils';
import VueCompositionAPI from '@vue/composition-api';
import VChip from './v-chip.vue';
import VIcon from '@/components/v-icon/';
const localVue = createLocalVue();
localVue.use(VueCompositionAPI);
localVue.component('v-icon', VIcon);
describe('Chip', () => {
let component: Wrapper<Vue>;
beforeEach(() => {
component = mount(VChip, { localVue });
});
it('Renders the provided markup in the default slow', () => {
const component = mount(VChip, {
localVue,
slots: {
default: 'Click me'
}
});
expect(component.text()).toContain('Click me');
});
it('Hides the whole component', async () => {
component.setProps({
active: false
});
await component.vm.$nextTick();
expect(component.find('span').exists()).toBe(false);
});
it('Adds the outline class for outline chips', async () => {
component.setProps({
outlined: true
});
await component.vm.$nextTick();
expect(component.classes()).toContain('outlined');
});
it('Adds the label class for block chips', async () => {
component.setProps({
label: true
});
await component.vm.$nextTick();
expect(component.classes()).toContain('label');
});
it('Adds the close icon for icon chips', async () => {
component.setProps({
close: true
});
await component.vm.$nextTick();
expect(component.find('.close-outline').exists()).toBe(true);
});
it('Sets the correct CSS variables for custom colors', async () => {
component.setProps({
color: '--red',
hoverColor: '--blue',
backgroundColor: '--green',
hoverBackgroundColor: '--yellow'
});
await component.vm.$nextTick();
expect((component.vm as any).styles['--_v-chip-color']).toBe('var(--red)');
expect((component.vm as any).styles['--_v-chip-hover-color']).toBe('var(--blue)');
expect((component.vm as any).styles['--_v-chip-background-color']).toBe('var(--green)');
expect((component.vm as any).styles['--_v-chip-hover-background-color']).toBe(
'var(--yellow)'
);
});
it('Emits a click event when chip is not disabled', async () => {
component.setProps({
disabled: false
});
await component.vm.$nextTick();
(component.vm as any).onClick(new Event('click'));
expect(component.emitted('click')[0][0]).toBeInstanceOf(Event);
});
it('Does not emit click when disabled', async () => {
component.setProps({
disabled: true
});
await component.vm.$nextTick();
(component.vm as any).onClick(new Event('click'));
expect(component.emitted('click')).toBe(undefined);
});
it('Emits a click event when chip is not disabled and close button is clicked', async () => {
component.setProps({
disabled: false
});
await component.vm.$nextTick();
(component.vm as any).onCloseClick(new Event('click'));
expect(component.emitted('close')[0][0]).toBeInstanceOf(Event);
});
it('Does not emit click when disabled and close button is clicked', async () => {
component.setProps({
disabled: true
});
await component.vm.$nextTick();
(component.vm as any).onCloseClick(new Event('click'));
expect(component.emitted('click')).toBe(undefined);
});
describe('Sizes', () => {
const component = mount(VChip, {
localVue,
propsData: {
color: '--blue-grey',
name: 'person'
}
});
test('Extra Small', () => {
component.setProps({
xSmall: true,
small: false,
large: false,
xLarge: false
});
component.vm.$nextTick(() => expect(component.classes()).toContain('x-small'));
});
test('Small', () => {
component.setProps({
xSmall: false,
small: true,
large: false,
xLarge: false
});
component.vm.$nextTick(() => expect(component.classes()).toContain('small'));
});
test('Large', () => {
component.setProps({
xSmall: false,
small: false,
large: true,
xLarge: false
});
component.vm.$nextTick(() => expect(component.classes()).toContain('large'));
});
test('Extra Large', () => {
component.setProps({
xSmall: false,
small: false,
large: false,
xLarge: true
});
component.vm.$nextTick(() => expect(component.classes()).toContain('x-large'));
});
});
});

View File

@@ -0,0 +1,218 @@
<template>
<span
v-if="_active"
class="v-chip"
:style="styles"
:class="[sizeClass, { outlined, label, disabled, close }]"
@click="onClick"
>
<span class="chip-content">
<slot />
<span
v-if="close"
class="close-outline"
:class="{ disabled }"
@click.stop="onCloseClick"
>
<v-icon class="close" :name="closeIcon" x-small />
</span>
</span>
</span>
</template>
<script lang="ts">
import { createComponent, ref, computed } from '@vue/composition-api';
import parseCSSVar from '@/utils/parse-css-var';
export default createComponent({
props: {
active: {
type: Boolean,
default: null
},
close: {
type: Boolean,
default: false
},
closeIcon: {
type: String,
default: 'close'
},
outlined: {
type: Boolean,
default: false
},
color: {
type: String,
default: '--chip-primary-text-color'
},
backgroundColor: {
type: String,
default: '--chip-primary-background-color'
},
hoverColor: {
type: String,
default: '--chip-primary-text-color'
},
hoverBackgroundColor: {
type: String,
default: '--chip-primary-background-color-hover'
},
label: {
type: Boolean,
default: false
},
disabled: {
type: Boolean,
default: false
},
xSmall: {
type: Boolean,
default: false
},
small: {
type: Boolean,
default: false
},
large: {
type: Boolean,
default: false
},
xLarge: {
type: Boolean,
default: false
}
},
setup(props, { emit }) {
const _localActive = ref(true);
const _active = computed<boolean>({
get: () => {
if (props.active !== null) return props.active;
return _localActive.value;
},
set: (active: boolean) => {
emit('update:active', active);
_localActive.value = active;
}
});
const styles = computed(() => ({
'--_v-chip-color': parseCSSVar(props.color),
'--_v-chip-background-color': parseCSSVar(props.backgroundColor),
'--_v-chip-hover-color': parseCSSVar(props.hoverColor),
'--_v-chip-hover-background-color': parseCSSVar(props.hoverBackgroundColor)
}));
const sizeClass = computed<string | null>(() => {
if (props.xSmall) return 'x-small';
if (props.small) return 'small';
if (props.large) return 'large';
if (props.xLarge) return 'x-large';
return null;
});
return { styles, sizeClass, _active, onClick, onCloseClick };
function onClick(event: MouseEvent) {
if (props.disabled) return;
emit('click', event);
}
function onCloseClick(event: MouseEvent) {
if (props.disabled) return;
_active.value = !_active.value;
emit('close', event);
}
}
});
</script>
<style lang="scss" scoped>
.v-chip {
display: inline-flex;
height: 32px;
padding: 0 12px;
align-items: center;
color: var(--_v-chip-color);
background-color: var(--_v-chip-background-color);
border-radius: 16px;
font-weight: var(--weight-normal);
&:hover {
color: var(--_v-chip-hover-color);
background-color: var(--_v-chip-hover-background-color);
}
&.label {
border-radius: var(--border-radius);
}
&.outlined {
background-color: transparent;
border: var(--input-border-width) solid var(--_v-chip-background-color);
}
&.disabled {
color: var(--chip-primary-text-color-disabled);
background-color: var(--chip-primary-background-color-disabled);
}
&.x-small {
font-size: 12px;
height: 20px;
border-radius: 10px;
}
&.small {
font-size: 14px;
height: 24px;
border-radius: 12px;
}
&.large {
font-size: 16px;
height: 44px;
border-radius: 22px;
}
&.x-large {
font-size: 18px;
height: 48px;
border-radius: 24px;
}
.chip-content {
display: inline-flex;
align-items: center;
.close-outline {
position: relative;
display: inline-flex;
justify-content: center;
align-items: center;
background-color: var(--chip-primary-close-color);
color: var(--chip-primary-background-color);
border-radius: 10px;
height: 14px;
width: 14px;
right: -4px;
margin-left: 4px;
&.disabled {
background-color: var(--chip-primary-close-color-disabled);
&:hover {
background-color: var(--chip-primary-close-color-disabled);
}
}
&:hover {
background-color: var(--chip-primary-close-color-hover);
}
}
}
}
</style>

View File

@@ -1,4 +1,4 @@
import { mount, createLocalVue, Wrapper } from '@vue/test-utils';
import { shallowMount, createLocalVue, Wrapper } from '@vue/test-utils';
import VueCompositionAPI from '@vue/composition-api';
const localVue = createLocalVue();
@@ -10,19 +10,19 @@ describe('Overlay', () => {
let component: Wrapper<Vue>;
beforeEach(() => {
component = mount(VOverlay, {
component = shallowMount(VOverlay, {
localVue
});
});
it('Is invisible when active prop is false', () => {
expect(component.isVisible()).toBe(false);
expect(component.classes()).toEqual(['v-overlay']);
});
it('Is visible when active is true', async () => {
component.setProps({ active: true });
await component.vm.$nextTick();
expect(component.isVisible()).toBe(true);
expect(component.classes()).toEqual(['v-overlay', 'active']);
});
it('Sets position absolute based on absolute prop', async () => {
@@ -46,7 +46,7 @@ describe('Overlay', () => {
});
it('Adds the has-click class when click event is passed', async () => {
const component = mount(VOverlay, {
const component = shallowMount(VOverlay, {
localVue,
listeners: {
click: () => {}

9
src/routes/debug.vue Normal file
View File

@@ -0,0 +1,9 @@
<template>
<div>Debug</div>
</template>
<script lang="ts">
import { createComponent } from '@vue/composition-api';
export default createComponent({});
</script>

View File

@@ -341,6 +341,18 @@ body {
--button-tertiary-background-color-disabled: var(--blue-grey-400);
--button-tertiary-text-color-disabled: var(--blue-grey-600);
// Chip colors
--chip-primary-text-color: var(--black);
--chip-primary-background-color: var(--blue-grey-100);
--chip-primary-background-color-hover: var(--blue-grey-200);
--chip-primary-close-color: var(--blue-grey-700);
--chip-primary-close-color-hover: var(--blue-grey-800);
--chip-primary-close-color-disabled: var(--blue-grey-200);
--chip-primary-background-color-disabled: var(--blue-grey-50);
--chip-primary-text-color-disabled: var(--blue-grey-300);
// Table
--table-head-border-color: var(--blue-grey-50);

View File

@@ -1,10 +1,12 @@
import VueCompositionAPI from '@vue/composition-api';
import { mount, createLocalVue } from '@vue/test-utils';
import { VTooltip } from 'v-tooltip';
import VIcon from '@/components/v-icon/';
const localVue = createLocalVue();
localVue.use(VueCompositionAPI);
localVue.directive('tooltip', VTooltip);
localVue.component('v-icon', VIcon);
import PublicView from './public-view.vue';