Migrate existing (finished) base components

This commit is contained in:
rijkvanzanten
2020-02-05 15:11:40 -05:00
parent eb011906e7
commit 55e56e30ec
77 changed files with 8530 additions and 0 deletions

View File

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

View File

@@ -0,0 +1,86 @@
# Button
```html
<v-button>Click me!</v-button>
```
## Sizes
The button component supports the following sizes through the use of props:
* x-small
* small
* (default)
* large
* x-large
Alternatively, you can force the font-size through the `size` prop:
```html
<v-button :size="64">Click me!</v-button>
```
## 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-button
color="--red"
background-color="--red-50"
hover-color="--white"
hover-background-color="--red"
>
Click me
</v-button>
```
## Events
The only event that can be added to the button is the `click` event:
```html
<v-button @click="sayHi">Hello!</v-button>
```
## Loading
The button has a loading state that can be enabled with the `loading` prop. By default, the button will render a `v-spinner`. You can override what's being shown during the loading state by using the `#loading` slot:
```html
<v-button>
<template #loading>
... Almost done ...
</template>
</v-button>
```
The loading slot is rendered _on top_ of the content that was there before. Make sure that your loading content doesn't exceed the size of the default state content. This restriction is put in place to prevent jumps when going from and to the loading state.
## Props
| Prop | Description | Default |
|--------------------------|---------------------------------------------------------------------------|-------------------------------------------|
| `block` | Enable ull width (display block) | `false` |
| `icon` | Remove padding / min-width. Meant to be used with just an icon as content | `false` |
| `outlined` | No background | `false` |
| `rounded` | Enable rounded corners | `false` |
| `color` | Text / icon color | `--button-primary-text-color` |
| `hover-color` | Text / icon color on hover | `--button-primary-text-color` |
| `background-color` | Button color | `--button-primary-background-color` |
| `hover-background-color` | Button color on hover | `--button-primary-background-color-hover` |
| `type` | HTML `type` attribute | `button` |
| `disabled` | Disabled state | `false` |
| `loading` | Loading state | `false` |
| `width` | Width in px | -- |
| `size` | Size of the text in the button in px | -- |
| `x-small` | Render extra small | `false` |
| `small` | Render small | `false` |
| `large` | Render large | `false` |
| `x-large` | Render extra large | `false` |
## Slots
| Slot | Description |
|-----------|----------------------------------------------|
| `loading` | Content that's rendered during loading state |

View File

@@ -0,0 +1,279 @@
import { withKnobs, text, boolean, number, optionsKnob as options } from '@storybook/addon-knobs';
import { action } from '@storybook/addon-actions';
import Vue from 'vue';
import VButton from './v-button.vue';
import VIcon from '../v-icon/';
import markdown from './v-button.readme.md';
Vue.component('v-button', VButton);
Vue.component('v-icon', VIcon);
export default {
title: 'Components / Button',
component: VButton,
decorators: [withKnobs],
parameters: {
notes: markdown
}
};
export const withText = () => ({
methods: { onClick: action('click') },
props: {
text: {
default: text('Text in button', 'Click me')
},
block: {
default: boolean('Block', false, 'Button')
},
rounded: {
default: boolean('Rounded', false, 'Button')
},
icon: {
default: boolean('Icon mode', false, 'Button')
},
outlined: {
default: boolean('Outlined', false, 'Button')
},
type: {
default: text('Type attribute', 'button', 'Button')
},
loading: {
default: boolean('Loading', false, 'Button')
},
width: {
default: number('Width', 0, {}, 'Button')
},
size: {
default: options(
'Size',
{
'Extra Small': 'xSmall',
Small: 'small',
'(default)': 'default',
Large: 'large',
'Extra Large': 'xLarge'
},
'default',
{
display: 'select'
},
'Button'
)
},
disabled: {
default: boolean('Disabled', false, 'Button')
},
color: {
default: text('Color', '--button-primary-text-color', 'Colors')
},
backgroundColor: {
default: text('Background Color', '--button-primary-background-color', 'Colors')
},
hoverColor: {
default: text('Color (hover)', '--white', 'Colors')
},
hoverBackgroundColor: {
default: text('Background Color (hover)', '--black', 'Colors')
}
},
template: `
<v-button
:block="block"
:rounded="rounded"
:outlined="outlined"
:icon="icon"
:color="color"
:background-color="backgroundColor"
:hover-color="hoverColor"
:hover-background-color="hoverBackgroundColor"
:type="type"
:disabled="disabled"
:loading="loading"
:width="width"
:x-small="size === 'xSmall'"
:small="size === 'small'"
:large="size === 'large'"
:x-large="size === 'xLarge'"
@click="onClick"
>
{{ text }}
</v-button>
`
});
export const withIcon = () => ({
methods: { onClick: action('click') },
props: {
iconName: {
default: text('Material Icon', 'add')
},
block: {
default: boolean('Block', false, 'Button')
},
rounded: {
default: boolean('Rounded', true, 'Button')
},
icon: {
default: boolean('Icon mode', true, 'Button')
},
outlined: {
default: boolean('Outlined', false, 'Button')
},
type: {
default: text('Type attribute', 'button', 'Button')
},
loading: {
default: boolean('Loading', false, 'Button')
},
width: {
default: number('Width', 0, {}, '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'
)
},
disabled: {
default: boolean('Disabled', false, 'Button')
},
color: {
default: text('Color', '--button-primary-text-color', 'Colors')
},
backgroundColor: {
default: text('Background Color', '--button-primary-background-color', 'Colors')
},
hoverColor: {
default: text('Color (hover)', '--white', 'Colors')
},
hoverBackgroundColor: {
default: text('Background Color (hover)', '--black', 'Colors')
}
},
template: `
<v-button
:block="block"
:rounded="rounded"
:outlined="outlined"
:icon="icon"
:color="color"
:background-color="backgroundColor"
:hover-color="hoverColor"
:hover-background-color="hoverBackgroundColor"
:type="type"
:disabled="disabled"
:loading="loading"
:width="width"
:x-small="size === 'xSmall'"
:small="size === 'small'"
:large="size === 'large'"
:x-large="size === 'xLarge'"
@click="onClick"
>
<v-icon
:name="iconName"
:x-small="iconSize === 'xSmall'"
:small="iconSize === 'small'"
:large="iconSize === 'large'"
:x-large="iconSize === 'xLarge'"
/>
</v-button>
`
});
export const sizes = () => `
<div>
<v-button x-small>Extra small</v-button>
<v-button small>Small</v-button>
<v-button>Default</v-button>
<v-button large>Large</v-button>
<v-button x-large>Extra Large</v-button>
</div>
`;
export const colors = () => `
<div>
<v-button
color="--red"
background-color="--red-50"
hover-color="--white"
hover-background-color="--red"
>
Delete
</v-button>
<v-button
color="--white"
background-color="--green"
hover-background-color="--green-800"
>
Save
</v-button>
<v-button
color="--white"
background-color="--amber"
hover-background-color="--amber-800"
>
Warn
</v-button>
<v-button
color="--blue-grey-800"
background-color="--blue-grey-50"
hover-color="--red"
hover-background-color="--white"
>
Hover
</v-button>
<v-button
color="--blue-grey-800"
background-color="transparent"
hover-color="--black"
hover-background-color="--blue-grey-100"
>
Transparent
</v-button>
</div>
`;
export const customLoading = () => ({
props: {
loading: {
default: boolean('Loading', true)
}
},
template: `
<v-button :loading="loading">
Hello, World!
<template #loading>
..Loading..
</template>
</v-button>`
});

View File

@@ -0,0 +1,157 @@
import { mount, createLocalVue } from '@vue/test-utils';
import VueCompositionAPI from '@vue/composition-api';
const localVue = createLocalVue();
localVue.use(VueCompositionAPI);
localVue.component('v-spinner', VSpinner);
import VButton from './v-button.vue';
import VSpinner from '../v-spinner/';
describe('Button', () => {
it('Renders the provided markup in the default slow', () => {
const component = mount(VButton, {
localVue,
slots: {
default: 'Click me'
}
});
expect(component.text()).toContain('Click me');
});
it('Adds the outline class for outline buttons', () => {
const component = mount(VButton, {
localVue,
propsData: {
outlined: true
}
});
expect(component.classes()).toContain('outlined');
});
it('Adds the block class for block buttons', () => {
const component = mount(VButton, {
localVue,
propsData: {
block: true
}
});
expect(component.classes()).toContain('block');
});
it('Adds the rounded class for rounded buttons', () => {
const component = mount(VButton, {
localVue,
propsData: {
rounded: true
}
});
expect(component.classes()).toContain('rounded');
});
it('Adds the icon class for icon buttons', () => {
const component = mount(VButton, {
localVue,
propsData: {
icon: true
}
});
expect(component.classes()).toContain('icon');
});
it('Adds the loading class for loading buttons', () => {
const component = mount(VButton, {
localVue,
propsData: {
loading: true
}
});
expect(component.classes()).toContain('loading');
});
it('Sets the correct CSS variables for custom colors', () => {
const component = mount(VButton, {
localVue,
propsData: {
color: '--red',
hoverColor: '--blue',
backgroundColor: '--green',
hoverBackgroundColor: '--yellow'
}
});
expect((component.vm as any).styles['--_v-button-color']).toBe('var(--red)');
expect((component.vm as any).styles['--_v-button-hover-color']).toBe('var(--blue)');
expect((component.vm as any).styles['--_v-button-background-color']).toBe('var(--green)');
expect((component.vm as any).styles['--_v-button-hover-background-color']).toBe(
'var(--yellow)'
);
});
describe('Sizes', () => {
const component = mount(VButton, {
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'));
});
it('Sets the correct custom width', () => {
const component = mount(VButton, {
localVue,
propsData: {
width: 56
}
});
expect((component.vm as any).styles.width).toBe('56px');
});
});
});

View File

@@ -0,0 +1,236 @@
<template>
<button
class="v-button"
:class="[sizeClass, { block, rounded, icon, outlined, loading }]"
:type="type"
:style="styles"
:disabled="disabled"
@click="!loading ? $emit('click') : null"
>
<span class="content" :class="{ invisible: loading }"><slot /></span>
<div class="spinner">
<slot v-if="loading" name="loading">
<v-spinner :x-small="xSmall" :small="small" />
</slot>
</div>
</button>
</template>
<script lang="ts">
import { createComponent, reactive, computed, Ref } from '@vue/composition-api';
import parseCSSVar from '@/utils/parse-css-var';
export default createComponent({
props: {
block: {
type: Boolean,
default: false
},
rounded: {
type: Boolean,
default: false
},
outlined: {
type: Boolean,
default: false
},
icon: {
type: Boolean,
default: false
},
color: {
type: String,
default: '--button-primary-text-color'
},
backgroundColor: {
type: String,
default: '--button-primary-background-color'
},
hoverColor: {
type: String,
default: '--button-primary-text-color'
},
hoverBackgroundColor: {
type: String,
default: '--button-primary-background-color-hover'
},
type: {
type: String,
default: 'button'
},
disabled: {
type: Boolean,
default: false
},
loading: {
type: Boolean,
default: false
},
width: {
type: Number,
default: null
},
xSmall: {
type: Boolean,
default: false
},
small: {
type: Boolean,
default: false
},
large: {
type: Boolean,
default: false
},
xLarge: {
type: Boolean,
default: false
}
},
setup(props) {
interface Styles {
'--_v-button-color': string;
'--_v-button-background-color': string;
'--_v-button-hover-color': string;
'--_v-button-hover-background-color': string;
width?: string;
}
const styles = computed<Styles>(() => {
let styles: Styles = {
'--_v-button-color': parseCSSVar(props.color),
'--_v-button-background-color': parseCSSVar(props.backgroundColor),
'--_v-button-hover-color': parseCSSVar(props.hoverColor),
'--_v-button-hover-background-color': parseCSSVar(props.hoverBackgroundColor)
};
if (props.width && +props.width > 0) {
styles.width = props.width + 'px';
}
return styles;
});
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 };
}
});
</script>
<style lang="scss" scoped>
.v-button {
--_v-button-height: 44px;
color: var(--_v-button-color);
background-color: var(--_v-button-background-color);
border-radius: var(--border-radius);
font-weight: var(--weight-bold);
cursor: pointer;
border: var(--input-border-width) solid var(--_v-button-background-color);
font-size: 14px;
padding: 0 19px;
min-width: 78px;
height: var(--_v-button-height);
transition: var(--fast) var(--transition);
transition-property: background-color border;
position: relative;
&:focus {
outline: 0;
}
&:not(.loading):not(:disabled):hover {
color: var(--_v-button-hover-color);
background-color: var(--_v-button-hover-background-color);
border: var(--input-border-width) solid var(--_v-button-hover-background-color);
}
&.block {
display: block;
min-width: 100%;
}
&.rounded {
border-radius: calc(var(--button-height) / 2);
}
&.outlined {
background-color: transparent;
}
&:disabled {
background-color: var(--button-primary-background-color-disabled);
border: var(--input-border-width) solid var(--button-primary-background-color-disabled);
color: var(--button-primary-text-color-disabled);
cursor: not-allowed;
}
&.x-small {
--_v-button-height: 28px;
font-size: 12px;
padding: 0 12px;
min-width: 48px;
}
&.small {
--_v-button-height: 36px;
font-size: 14px;
padding: 0 16px;
min-width: 64px;
}
&.large {
--_v-button-height: var(--button-height);
font-size: var(--button-font-size);
padding: 0 23px;
min-width: 92px;
}
&.x-large {
--_v-button-height: 58px;
font-size: 18px;
padding: 0 32px;
min-width: 120px;
}
&.icon {
min-width: 0;
padding: 0;
width: var(--_v-button-height);
}
.content,
.spinner {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
max-width: 100%;
}
.content {
position: relative;
top: -1;
&.invisible {
opacity: 0;
}
}
.spinner {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}
</style>