mirror of
https://github.com/directus/directus.git
synced 2026-01-27 02:48:02 -05:00
Tabs (#210)
* 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:
@@ -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/';
|
||||
|
||||
|
||||
@@ -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; "/>
|
||||
|
||||
7
src/components/v-tabs/index.ts
Normal file
7
src/components/v-tabs/index.ts
Normal 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;
|
||||
55
src/components/v-tabs/readme.md
Normal file
55
src/components/v-tabs/readme.md
Normal 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)` |
|
||||
4
src/components/v-tabs/v-tab-item/index.ts
Normal file
4
src/components/v-tabs/v-tab-item/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import VTabItem from './v-tab-item.vue';
|
||||
|
||||
export { VTabItem };
|
||||
export default VTabItem;
|
||||
50
src/components/v-tabs/v-tab-item/readme.md
Normal file
50
src/components/v-tabs/v-tab-item/readme.md
Normal 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
|
||||
35
src/components/v-tabs/v-tab-item/v-tab-item.test.ts
Normal file
35
src/components/v-tabs/v-tab-item/v-tab-item.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
23
src/components/v-tabs/v-tab-item/v-tab-item.vue
Normal file
23
src/components/v-tabs/v-tab-item/v-tab-item.vue
Normal 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>
|
||||
4
src/components/v-tabs/v-tab/index.ts
Normal file
4
src/components/v-tabs/v-tab/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import VTab from './v-tab.vue';
|
||||
|
||||
export { VTab };
|
||||
export default VTab;
|
||||
34
src/components/v-tabs/v-tab/readme.md
Normal file
34
src/components/v-tabs/v-tab/readme.md
Normal 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)` |
|
||||
37
src/components/v-tabs/v-tab/v-tab.test.ts
Normal file
37
src/components/v-tabs/v-tab/v-tab.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
56
src/components/v-tabs/v-tab/v-tab.vue
Normal file
56
src/components/v-tabs/v-tab/v-tab.vue
Normal 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>
|
||||
4
src/components/v-tabs/v-tabs-items/index.ts
Normal file
4
src/components/v-tabs/v-tabs-items/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import VTabsItems from './v-tabs-items.vue';
|
||||
|
||||
export { VTabsItems };
|
||||
export default VTabsItems;
|
||||
34
src/components/v-tabs/v-tabs-items/readme.md
Normal file
34
src/components/v-tabs/v-tabs-items/readme.md
Normal 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
|
||||
18
src/components/v-tabs/v-tabs-items/v-tabs-items.test.ts
Normal file
18
src/components/v-tabs/v-tabs-items/v-tabs-items.test.ts
Normal 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]);
|
||||
});
|
||||
});
|
||||
25
src/components/v-tabs/v-tabs-items/v-tabs-items.vue
Normal file
25
src/components/v-tabs/v-tabs-items/v-tabs-items.vue
Normal 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>
|
||||
91
src/components/v-tabs/v-tabs.story.ts
Normal file
91
src/components/v-tabs/v-tabs.story.ts
Normal 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>
|
||||
`
|
||||
});
|
||||
55
src/components/v-tabs/v-tabs.test.ts
Normal file
55
src/components/v-tabs/v-tabs.test.ts
Normal 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
|
||||
});
|
||||
});
|
||||
});
|
||||
102
src/components/v-tabs/v-tabs.vue
Normal file
102
src/components/v-tabs/v-tabs.vue
Normal 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>
|
||||
Reference in New Issue
Block a user