* initial commit for tabs

* added tabs-items

* updated docs

* Tweak styling, update stories

* Update structure, readme's and storybook entry

* Add tests for v-tabs

* Add tests for v-tab

* Add tests for v-tabs-items

* Fix typo

* Fix test

Co-authored-by: rijkvanzanten <rijkvanzanten@me.com>
This commit is contained in:
Nitwel
2020-03-19 16:50:09 +01:00
committed by GitHub
parent 784139ba1d
commit e8267df99d
19 changed files with 647 additions and 0 deletions

View File

@@ -26,6 +26,7 @@ import VSheet from './v-sheet/';
import VSlider from './v-slider/';
import VSwitch from './v-switch/';
import VTable from './v-table/';
import VTabs, { VTab, VTabsItems, VTabItem } from './v-tabs/';
Vue.component('v-avatar', VAvatar);
Vue.component('v-button', VButton);
@@ -53,6 +54,10 @@ Vue.component('v-sheet', VSheet);
Vue.component('v-slider', VSlider);
Vue.component('v-switch', VSwitch);
Vue.component('v-table', VTable);
Vue.component('v-tabs', VTabs);
Vue.component('v-tab', VTab);
Vue.component('v-tabs-items', VTabsItems);
Vue.component('v-tab-item', VTabItem);
import DrawerDetail from '@/views/private/components/drawer-detail/';

View File

@@ -44,6 +44,14 @@ export const basic = () =>
export const withImage = () =>
defineComponent({
props: {
disabled: {
default: boolean('Disabled', false)
},
tile: {
default: boolean('Tile', false)
}
},
template: `
<v-card :disabled="disabled" :tile="tile">
<img src="https://images.unsplash.com/photo-1581587118469-a117038c0249?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=600&ixlib=rb-1.2.1&q=80&w=800" width="800" height="600" style="width: 100%; display: block; height: auto; "/>

View File

@@ -0,0 +1,7 @@
import VTabs from './v-tabs.vue';
import VTab from './v-tab/';
import VTabsItems from './v-tabs-items/';
import VTabItem from './v-tab-item/';
export { VTabs, VTab, VTabsItems, VTabItem };
export default VTabs;

View File

@@ -0,0 +1,55 @@
# Tabs
Tabs can be used for hiding content behind a selectable item. It can be used as a navigational
device.
## Usage
```html
<template>
<v-tabs v-model="selection">
<v-tab><v-icon name="home" left /> Home</v-tab>
<v-tab><v-icon name="notifications" left /> News</v-tab>
<v-tab><v-icon name="help" left /> Help</v-tab>
</v-tabs>
<v-tabs-items>
<v-tab-item>I'm the content for Home!</v-tab-item>
<v-tab-item>I'm the content for News!</v-tab-item>
<v-tab-item>I'm the content for Help!</v-tab-item>
</v-tabs-items>
</template>
<script lang="ts">
import { defineComponent, ref } from '@vue/composition-api';
export default defineComponent({
setup() {
const selection = ref([]);
return { selection };
}
});
</script>
```
## Props
| Prop | Description | Default |
|------------|------------------------------------|---------|
| `vertical` | Render the tabs vertically | `false` |
| `value` | v-model value for active selection | -- |
## Events
| Event | Description | Value |
|---------|--------------------------|--------------------------------|
| `input` | Update current selection | `readonly (string | number)[]` |
## Slots
| Slot | Description | Data |
|-----------|-------------|------|
| _default_ | | |
## CSS Variables
| Variable | Default |
|----------------------------|---------------------------|
| `--v-tabs-underline-color` | `var(--foreground-color)` |

View File

@@ -0,0 +1,4 @@
import VTabItem from './v-tab-item.vue';
export { VTabItem };
export default VTabItem;

View File

@@ -0,0 +1,50 @@
# Tab Item
Individual tab content. To be used in a `v-tabs-items` context.
## Usage
```html
<v-tabs-items>
<v-tab-item>
This is the content for the first tab.
</v-tab-item>
<v-tab-item>
This is the content for the second tab.
</v-tab-item>
</v-tabs-items>
```
If you're using a custom value in the `value` prop, make sure the corresponding tab uses the same value to match:
```html
<v-tabs v-model="selection">
<v-tab value="home">Home</v-tab>
<v-tab>Settings</v-tab>
</v-tabs>
<v-tabs-items v-model="selection">
<v-tab-item value="home">
This is the content for home.
</v-tab-item>
<v-tab-item>
Settings content
</v-tab-item>
</v-tabs-items>
```
## Props
| Prop | Description | Default |
|---------|-----------------------------------------|---------|
| `value` | Custom value to use for selection state | -- |
## Events
n/a
## Slots
| Slot | Description | Data |
|-----------|------------------|-------------------------------------------|
| _default_ | Tab item content | `{ active: boolean, toggle: () => void }` |
## CSS Variables
n/a

View File

@@ -0,0 +1,35 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import VueCompositionAPI from '@vue/composition-api';
import VTabItem from './v-tab-item.vue';
const localVue = createLocalVue();
localVue.use(VueCompositionAPI);
jest.mock('@/compositions/groupable', () => ({
useGroupable: () => ({
active: { value: null },
toggle: jest.fn()
})
}));
describe('Components / Tabs / Tab', () => {
it('Renders when active', () => {
const component = shallowMount(VTabItem, {
localVue,
data: () => ({
active: true
})
});
expect(component.find('.v-tab-item').exists()).toBe(true);
});
it('Does not render when inactive', () => {
const component = shallowMount(VTabItem, {
localVue,
data: () => ({
active: false
})
});
expect(component.find('.v-tab-item').exists()).toBe(false);
});
});

View File

@@ -0,0 +1,23 @@
<template>
<div v-if="active" class="v-tab-item">
<slot v-bind="{ active, toggle }" />
</div>
</template>
<script lang="ts">
import { defineComponent } from '@vue/composition-api';
import { useGroupable } from '@/compositions/groupable';
export default defineComponent({
props: {
value: {
type: String,
default: null
}
},
setup(props) {
const { active, toggle } = useGroupable(props.value);
return { active, toggle };
}
});
</script>

View File

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

View File

@@ -0,0 +1,34 @@
# Tab
Individual tab. To be used inside a `v-tabs` context.
## Usage
```html
<v-tabs>
<v-tab>Schema</v-tab>
<v-tab>Options</v-tab>
</v-tabs>
```
## Props
| Prop | Description | Default |
|------------|--------------------------------------------------------|---------|
| `disabled` | Disable the tab | `false` |
| `value` | A custom value to be used in the selection of `v-tabs` | |
## Events
n/a
## Slots
| Slot | Description | Data |
|-----------|-------------|--------------------------------------------|
| _default_ | | `{ active: boolean, toggle: () => void; }` |
## CSS Variables
| Variable | Default |
|-----------------------------------|---------------------------------|
| `--v-tab-color` | `var(--input-foreground-color)` |
| `--v-tab-background-color` | `var(--input-background-color)` |
| `--v-tab-color-active` | `var(--input-foreground-color)` |
| `--v-tab-background-color-active` | `var(--input-background-color)` |

View File

@@ -0,0 +1,37 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import VueCompositionAPI from '@vue/composition-api';
import VTab from './v-tab.vue';
const mockUseGroupableContent = {
active: {
value: false
},
toggle: jest.fn()
};
jest.mock('@/compositions/groupable', () => ({
useGroupable: () => mockUseGroupableContent
}));
const localVue = createLocalVue();
localVue.use(VueCompositionAPI);
localVue.component('v-tab', VTab);
describe('Components / Tabs / Tab', () => {
it('Calls toggle on click', () => {
const component = shallowMount(VTab, { localVue });
component.trigger('click');
expect(mockUseGroupableContent.toggle).toHaveBeenCalled();
});
it('Does not call toggle when disabled', () => {
const component = shallowMount(VTab, {
localVue,
propsData: {
disabled: true
}
});
component.trigger('click');
expect(mockUseGroupableContent.toggle).not.toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,56 @@
<template>
<div class="v-tab" :class="{ active, disabled }" @click="onClick">
<slot v-bind="{ active, toggle }" />
</div>
</template>
<script lang="ts">
import { defineComponent } from '@vue/composition-api';
import { useGroupable } from '@/compositions/groupable';
export default defineComponent({
props: {
disabled: {
type: Boolean,
default: false
},
value: {
type: String,
default: null
}
},
setup(props) {
const { active, toggle } = useGroupable(props.value);
return { active, toggle, onClick };
function onClick() {
if (props.disabled === false) toggle();
}
}
});
</script>
<style lang="scss" scoped>
.v-tab {
--v-tab-color: var(--input-foreground-color);
--v-tab-background-color: var(--input-background-color);
--v-tab-color-active: var(--input-foreground-color);
--v-tab-background-color-active: var(--input-background-color);
color: var(--v-tab-color);
font-weight: 500;
font-size: 12px;
text-transform: uppercase;
background-color: var(--v-tab-background-color);
&.active {
color: var(--v-tab-color-active);
background-color: var(--v-tab-background-color-active);
}
&.disabled {
cursor: not-allowed;
opacity: 0.5;
}
}
</style>

View File

@@ -0,0 +1,4 @@
import VTabsItems from './v-tabs-items.vue';
export { VTabsItems };
export default VTabsItems;

View File

@@ -0,0 +1,34 @@
# Tabs Items
Tabs Items mirror a tab and display information for a selected tab.
If a tab item is not selected, it automaticly gets hidden.
## Usage
```html
<v-tabs-items v-model="selection">
<v-tab-item>Home Section</v-tab-item>
<v-tab-item>News Section</v-tab-item>
<v-tab-item>Help Section</v-tab-item>
</v-tabs-items>
```
## Props
| Prop | Description | Default |
|---------|---------------|---------|
| `value` | v-model value | -- |
## Events
| Event | Description | Value |
|---------|-----------------|--------------------------------|
| `input` | Updates v-model | `readonly (string | number)[]` |
## Slots
| Slot | Description | Data |
|-----------|-------------|------|
| _default_ | | |
## CSS Variables
n/a

View File

@@ -0,0 +1,18 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import VueCompositionAPI from '@vue/composition-api';
import VTabsItems from './v-tabs-items.vue';
import VItemGroup from '@/components/v-item-group';
const localVue = createLocalVue();
localVue.use(VueCompositionAPI);
localVue.component('v-item-group', VItemGroup);
describe('Components / Tabs / Tabs Items', () => {
it('Emits the new selection on update', () => {
const component = shallowMount(VTabsItems, { localVue });
(component.vm as any).update([1]);
expect(component.emitted('input')?.[0][0]).toEqual([1]);
});
});

View File

@@ -0,0 +1,25 @@
<template>
<v-item-group class="v-tabs-items" :value="value" @input="update">
<slot />
</v-item-group>
</template>
<script lang="ts">
import { defineComponent, PropType } from '@vue/composition-api';
export default defineComponent({
props: {
value: {
type: Array as PropType<(string | number)[]>,
default: undefined
}
},
setup(props, { emit }) {
function update(newSelection: readonly (string | number)[]) {
emit('input', newSelection);
}
return { update };
}
});
</script>

View File

@@ -0,0 +1,91 @@
import { withKnobs, boolean } from '@storybook/addon-knobs';
import markdown from './readme.md';
import withPadding from '../../../.storybook/decorators/with-padding';
import withBackground from '../../../.storybook/decorators/with-background';
import VTabs from './v-tabs.vue';
import VTab from './v-tab/';
import VTabsItems from './v-tabs-items/';
import VTabItem from './v-tab-item/';
import { defineComponent, ref } from '@vue/composition-api';
export default {
title: 'Components / Tabs',
decorators: [withKnobs, withPadding, withBackground],
parameters: {
notes: markdown
}
};
export const basic = () =>
defineComponent({
components: { VTabs, VTab, VTabsItems, VTabItem },
props: {
withIcons: {
default: boolean('With Icons', false)
}
},
setup() {
const selection = ref([]);
return { selection };
},
template: `
<div>
<v-tabs v-model="selection">
<v-tab><v-icon small v-if="withIcons" name="home" left />Home</v-tab>
<v-tab><v-icon small v-if="withIcons" name="notifications" left />News</v-tab>
<v-tab disabled><v-icon small v-if="withIcons" name="help" left />Help</v-tab>
<v-tab><v-icon small v-if="withIcons" name="chat" left />Chat</v-tab>
<v-tab><v-icon small v-if="withIcons" name="settings" left />Settings</v-tab>
</v-tabs>
<v-tabs-items v-model="selection" style="margin-top: 20px">
<v-tab-item>Home Section</v-tab-item>
<v-tab-item>News Section</v-tab-item>
<v-tab-item>Help Section</v-tab-item>
<v-tab-item>Chat Section</v-tab-item>
<v-tab-item>Settings Section</v-tab-item>
</v-tabs-items>
<pre style="max-width: max-content; margin-top: 20px; background-color: #eee; font-family: monospace; padding: 0.5rem; border-radius: 8px;">v-model value: {{JSON.stringify(selection)}}</pre>
</div>
`
});
export const vertical = () =>
defineComponent({
components: { VTabs, VTab },
props: {
withIcons: {
default: boolean('With Icons', false)
}
},
setup() {
const selection = ref([]);
return { selection };
},
template: `
<div>
<div style="display: flex;">
<v-tabs v-model="selection" :vertical="true" style="width: 160px">
<v-tab><v-icon small v-if="withIcons" name="home" left /> Home</v-tab>
<v-tab><v-icon small v-if="withIcons" name="notifications" left /> News</v-tab>
<v-tab><v-icon small v-if="withIcons" name="help" left /> Help</v-tab>
<v-tab><v-icon small v-if="withIcons" name="chat" left /> Chat</v-tab>
<v-tab><v-icon small v-if="withIcons" name="settings" left /> Settings</v-tab>
</v-tabs>
<v-tabs-items v-model="selection" style="margin-left: 20px;">
<v-tab-item>Home Section</v-tab-item>
<v-tab-item>News Section</v-tab-item>
<v-tab-item>Help Section</v-tab-item>
<v-tab-item>Chat Section</v-tab-item>
<v-tab-item>Settings Section</v-tab-item>
</v-tabs-items>
</div>
<pre style="max-width: max-content; margin-top: 20px; background-color: #eee; font-family: monospace; padding: 0.5rem; border-radius: 8px;">v-model value: {{JSON.stringify(selection)}}</pre>
</div>
`
});

View File

@@ -0,0 +1,55 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import VueCompositionAPI from '@vue/composition-api';
import VTabs from './v-tabs.vue';
const localVue = createLocalVue();
localVue.use(VueCompositionAPI);
localVue.component('v-item-group', VTabs);
jest.mock('@/compositions/groupable', () => ({
useGroupableParent: () => {
return {
items: {
value: [
{
active: {
value: false
}
},
{
active: {
value: false
}
},
{
active: {
value: true
}
},
{
active: {
value: false
}
}
]
}
} as any;
}
}));
describe('Components / Tabs', () => {
it('Emits the input event on update', () => {
const component = shallowMount(VTabs, { localVue });
(component.vm as any).update(['a']);
expect(component.emitted('input')?.[0][0]).toEqual(['a']);
});
it('Calculates the correct css variables based on children groupable items', () => {
const component = shallowMount(VTabs, { localVue });
expect((component.vm as any).slideStyle).toEqual({
'--_v-tabs-items': 4,
'--_v-tabs-selected': 2
});
});
});

View File

@@ -0,0 +1,102 @@
<template>
<div class="v-tabs" :class="{ vertical }">
<slot />
<div class="slider" :style="slideStyle"></div>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType, toRefs, computed } from '@vue/composition-api';
import { useGroupableParent } from '@/compositions/groupable';
export default defineComponent({
props: {
vertical: {
type: Boolean,
default: false
},
value: {
type: Array as PropType<(string | number)[]>,
default: undefined
}
},
setup(props, { emit }) {
const { value: selection } = toRefs(props);
const options = toRefs({
multiple: false,
max: -1,
mandatory: true
});
const { items } = useGroupableParent(
{
selection: selection,
onSelectionChange: update
},
options
);
const slideStyle = computed(() => {
const activeIndex = items.value.findIndex(item => item.active.value);
return {
'--_v-tabs-items': items.value.length,
'--_v-tabs-selected': activeIndex
};
});
function update(newSelection: readonly (string | number)[]) {
emit('input', newSelection);
}
return { update, slideStyle };
}
});
</script>
<style lang="scss" scoped>
.v-tabs {
--v-tabs-underline-color: var(--foreground-color);
position: relative;
display: flex;
::v-deep .v-tab {
display: flex;
flex-basis: 0px;
flex-grow: 1;
flex-shrink: 0;
align-items: center;
justify-content: center;
height: 44px;
padding: 12px 20px;
cursor: pointer;
}
.slider {
position: absolute;
bottom: 0;
left: calc(100% / var(--_v-tabs-items) * var(--_v-tabs-selected));
width: calc(100% / var(--_v-tabs-items));
height: 2px;
background-color: var(--v-tabs-underline-color);
transition: var(--medium) cubic-bezier(0.66, 0, 0.33, 1);
transition-property: left, top;
}
&.vertical {
flex-direction: column;
::v-deep .v-tab {
justify-content: flex-start;
}
.slider {
top: calc(100% / var(--_v-tabs-items) * var(--_v-tabs-selected));
left: 0;
width: 2px;
height: calc(100% / var(--_v-tabs-items));
}
}
}
</style>