From e8267df99dde5b4fc5a00c5fc5b4b01cdacdfc24 Mon Sep 17 00:00:00 2001 From: Nitwel Date: Thu, 19 Mar 2020 16:50:09 +0100 Subject: [PATCH] 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 --- src/components/register.ts | 5 + src/components/v-card/v-card.story.ts | 8 ++ src/components/v-tabs/index.ts | 7 ++ src/components/v-tabs/readme.md | 55 ++++++++++ src/components/v-tabs/v-tab-item/index.ts | 4 + src/components/v-tabs/v-tab-item/readme.md | 50 +++++++++ .../v-tabs/v-tab-item/v-tab-item.test.ts | 35 ++++++ .../v-tabs/v-tab-item/v-tab-item.vue | 23 ++++ src/components/v-tabs/v-tab/index.ts | 4 + src/components/v-tabs/v-tab/readme.md | 34 ++++++ src/components/v-tabs/v-tab/v-tab.test.ts | 37 +++++++ src/components/v-tabs/v-tab/v-tab.vue | 56 ++++++++++ src/components/v-tabs/v-tabs-items/index.ts | 4 + src/components/v-tabs/v-tabs-items/readme.md | 34 ++++++ .../v-tabs/v-tabs-items/v-tabs-items.test.ts | 18 ++++ .../v-tabs/v-tabs-items/v-tabs-items.vue | 25 +++++ src/components/v-tabs/v-tabs.story.ts | 91 ++++++++++++++++ src/components/v-tabs/v-tabs.test.ts | 55 ++++++++++ src/components/v-tabs/v-tabs.vue | 102 ++++++++++++++++++ 19 files changed, 647 insertions(+) create mode 100644 src/components/v-tabs/index.ts create mode 100644 src/components/v-tabs/readme.md create mode 100644 src/components/v-tabs/v-tab-item/index.ts create mode 100644 src/components/v-tabs/v-tab-item/readme.md create mode 100644 src/components/v-tabs/v-tab-item/v-tab-item.test.ts create mode 100644 src/components/v-tabs/v-tab-item/v-tab-item.vue create mode 100644 src/components/v-tabs/v-tab/index.ts create mode 100644 src/components/v-tabs/v-tab/readme.md create mode 100644 src/components/v-tabs/v-tab/v-tab.test.ts create mode 100644 src/components/v-tabs/v-tab/v-tab.vue create mode 100644 src/components/v-tabs/v-tabs-items/index.ts create mode 100644 src/components/v-tabs/v-tabs-items/readme.md create mode 100644 src/components/v-tabs/v-tabs-items/v-tabs-items.test.ts create mode 100644 src/components/v-tabs/v-tabs-items/v-tabs-items.vue create mode 100644 src/components/v-tabs/v-tabs.story.ts create mode 100644 src/components/v-tabs/v-tabs.test.ts create mode 100644 src/components/v-tabs/v-tabs.vue diff --git a/src/components/register.ts b/src/components/register.ts index f5931ec473..1863d08e34 100644 --- a/src/components/register.ts +++ b/src/components/register.ts @@ -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/'; diff --git a/src/components/v-card/v-card.story.ts b/src/components/v-card/v-card.story.ts index c9a39a8e71..90a0a2505e 100644 --- a/src/components/v-card/v-card.story.ts +++ b/src/components/v-card/v-card.story.ts @@ -44,6 +44,14 @@ export const basic = () => export const withImage = () => defineComponent({ + props: { + disabled: { + default: boolean('Disabled', false) + }, + tile: { + default: boolean('Tile', false) + } + }, template: ` diff --git a/src/components/v-tabs/index.ts b/src/components/v-tabs/index.ts new file mode 100644 index 0000000000..4a0476a29d --- /dev/null +++ b/src/components/v-tabs/index.ts @@ -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; diff --git a/src/components/v-tabs/readme.md b/src/components/v-tabs/readme.md new file mode 100644 index 0000000000..83c632c3fc --- /dev/null +++ b/src/components/v-tabs/readme.md @@ -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 + + + +``` + +## 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)` | diff --git a/src/components/v-tabs/v-tab-item/index.ts b/src/components/v-tabs/v-tab-item/index.ts new file mode 100644 index 0000000000..504fcf8d5e --- /dev/null +++ b/src/components/v-tabs/v-tab-item/index.ts @@ -0,0 +1,4 @@ +import VTabItem from './v-tab-item.vue'; + +export { VTabItem }; +export default VTabItem; diff --git a/src/components/v-tabs/v-tab-item/readme.md b/src/components/v-tabs/v-tab-item/readme.md new file mode 100644 index 0000000000..e5319cd4a7 --- /dev/null +++ b/src/components/v-tabs/v-tab-item/readme.md @@ -0,0 +1,50 @@ +# Tab Item + +Individual tab content. To be used in a `v-tabs-items` context. + +## Usage + +```html + + + This is the content for the first tab. + + + This is the content for the second tab. + + +``` + +If you're using a custom value in the `value` prop, make sure the corresponding tab uses the same value to match: + +```html + + Home + Settings + + + + + This is the content for home. + + + Settings content + + +``` + +## 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 diff --git a/src/components/v-tabs/v-tab-item/v-tab-item.test.ts b/src/components/v-tabs/v-tab-item/v-tab-item.test.ts new file mode 100644 index 0000000000..5559b56f23 --- /dev/null +++ b/src/components/v-tabs/v-tab-item/v-tab-item.test.ts @@ -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); + }); +}); diff --git a/src/components/v-tabs/v-tab-item/v-tab-item.vue b/src/components/v-tabs/v-tab-item/v-tab-item.vue new file mode 100644 index 0000000000..f6c7f1d586 --- /dev/null +++ b/src/components/v-tabs/v-tab-item/v-tab-item.vue @@ -0,0 +1,23 @@ + + + diff --git a/src/components/v-tabs/v-tab/index.ts b/src/components/v-tabs/v-tab/index.ts new file mode 100644 index 0000000000..a9deffa4f9 --- /dev/null +++ b/src/components/v-tabs/v-tab/index.ts @@ -0,0 +1,4 @@ +import VTab from './v-tab.vue'; + +export { VTab }; +export default VTab; diff --git a/src/components/v-tabs/v-tab/readme.md b/src/components/v-tabs/v-tab/readme.md new file mode 100644 index 0000000000..d96985d7d2 --- /dev/null +++ b/src/components/v-tabs/v-tab/readme.md @@ -0,0 +1,34 @@ +# Tab + +Individual tab. To be used inside a `v-tabs` context. + +## Usage + +```html + + Schema + Options + +``` + +## 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)` | diff --git a/src/components/v-tabs/v-tab/v-tab.test.ts b/src/components/v-tabs/v-tab/v-tab.test.ts new file mode 100644 index 0000000000..768b182c89 --- /dev/null +++ b/src/components/v-tabs/v-tab/v-tab.test.ts @@ -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(); + }); +}); diff --git a/src/components/v-tabs/v-tab/v-tab.vue b/src/components/v-tabs/v-tab/v-tab.vue new file mode 100644 index 0000000000..b106488181 --- /dev/null +++ b/src/components/v-tabs/v-tab/v-tab.vue @@ -0,0 +1,56 @@ + + + + + diff --git a/src/components/v-tabs/v-tabs-items/index.ts b/src/components/v-tabs/v-tabs-items/index.ts new file mode 100644 index 0000000000..97d5417f83 --- /dev/null +++ b/src/components/v-tabs/v-tabs-items/index.ts @@ -0,0 +1,4 @@ +import VTabsItems from './v-tabs-items.vue'; + +export { VTabsItems }; +export default VTabsItems; diff --git a/src/components/v-tabs/v-tabs-items/readme.md b/src/components/v-tabs/v-tabs-items/readme.md new file mode 100644 index 0000000000..8bca6d0268 --- /dev/null +++ b/src/components/v-tabs/v-tabs-items/readme.md @@ -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 + + Home Section + News Section + Help Section + +``` + +## 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 diff --git a/src/components/v-tabs/v-tabs-items/v-tabs-items.test.ts b/src/components/v-tabs/v-tabs-items/v-tabs-items.test.ts new file mode 100644 index 0000000000..d2283c68d1 --- /dev/null +++ b/src/components/v-tabs/v-tabs-items/v-tabs-items.test.ts @@ -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]); + }); +}); diff --git a/src/components/v-tabs/v-tabs-items/v-tabs-items.vue b/src/components/v-tabs/v-tabs-items/v-tabs-items.vue new file mode 100644 index 0000000000..25dea30c59 --- /dev/null +++ b/src/components/v-tabs/v-tabs-items/v-tabs-items.vue @@ -0,0 +1,25 @@ + + + diff --git a/src/components/v-tabs/v-tabs.story.ts b/src/components/v-tabs/v-tabs.story.ts new file mode 100644 index 0000000000..d479f48dca --- /dev/null +++ b/src/components/v-tabs/v-tabs.story.ts @@ -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: ` +
+ + Home + News + Help + Chat + Settings + + + + Home Section + News Section + Help Section + Chat Section + Settings Section + + +
v-model value: {{JSON.stringify(selection)}}
+
+ ` + }); + +export const vertical = () => + defineComponent({ + components: { VTabs, VTab }, + props: { + withIcons: { + default: boolean('With Icons', false) + } + }, + setup() { + const selection = ref([]); + return { selection }; + }, + template: ` +
+
+ + Home + News + Help + Chat + Settings + + + + Home Section + News Section + Help Section + Chat Section + Settings Section + +
+ +
v-model value: {{JSON.stringify(selection)}}
+
+ ` + }); diff --git a/src/components/v-tabs/v-tabs.test.ts b/src/components/v-tabs/v-tabs.test.ts new file mode 100644 index 0000000000..86050488dc --- /dev/null +++ b/src/components/v-tabs/v-tabs.test.ts @@ -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 + }); + }); +}); diff --git a/src/components/v-tabs/v-tabs.vue b/src/components/v-tabs/v-tabs.vue new file mode 100644 index 0000000000..0d09632517 --- /dev/null +++ b/src/components/v-tabs/v-tabs.vue @@ -0,0 +1,102 @@ + + + +