Skip to content

Commit ccb8c3c

Browse files
committed
update tabs component
1 parent e8fb7e2 commit ccb8c3c

File tree

6 files changed

+163
-138
lines changed

6 files changed

+163
-138
lines changed

mocks/tabs.json

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
[
2+
{
3+
"name": "Tab 1",
4+
"selected": true,
5+
"paragraph": "Toe in voluptate velit esse cillum dolore eu fugiat nulla pariatur.",
6+
"button": "Alpaca button"
7+
},
8+
{
9+
10+
"name": "Tab 2",
11+
"selected": false,
12+
"paragraph": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.",
13+
"button": ""
14+
},
15+
{
16+
"name": "Tab 3",
17+
"selected": false,
18+
"paragraph": "Sed do eiusmod tempor incididunt ut labo. Ut enim ad minim veniam, re et dolore magna aliqua. quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.",
19+
"button": ""
20+
},
21+
{
22+
"name": "Tab 4",
23+
"selected": false,
24+
"paragraph": "Toe in voluptate velit esse cillum dolore eu fugiat nulla pariatur.",
25+
"button": ""
26+
},
27+
{
28+
"name": "Tab 5",
29+
"selected": false,
30+
"paragraph": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.",
31+
"button": ""
32+
}
33+
]

src/atoms/tab/Tab.spec.js

+20-22
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,41 @@
1-
import { mount } from '@vue/test-utils'
1+
import { shallowMount } from '@vue/test-utils'
22
import ATab from './Tab.vue'
33

4+
const factory = () => {
5+
const defaultData = {
6+
slots: {
7+
default: '<span>Tab default text</span>'
8+
},
9+
propsData: {
10+
name: 'Tab'
11+
}
12+
}
13+
14+
return shallowMount(ATab, defaultData)
15+
}
16+
417
describe('Tab', () => {
518
it('has default structure', () => {
6-
const wrapper = mount(ATab, {
7-
propsData: {
8-
name: 'Tab'
9-
}
10-
})
19+
const wrapper = factory()
1120

1221
expect(wrapper).toBe('DIV')
1322
expect(wrapper.attributes().role).toBeDefined()
14-
expect(wrapper.attributes().role).toEqual('tabpanel')
23+
expect(wrapper.attributes().role).toEqual('tab')
1524
expect(wrapper.attributes('data-tab')).toBeDefined()
1625
})
1726

1827
it('renders slot text when passed', () => {
19-
const wrapper = mount(ATab, {
20-
slots: {
21-
default: '<span>Tab default text</span>'
22-
},
23-
propsData: {
24-
name: 'Tab'
25-
}
26-
})
28+
const wrapper = factory()
2729

2830
expect(wrapper.find('div > span').exists()).toBe(true)
2931
expect(wrapper.find('div > span').text()).toEqual('Tab default text')
3032
})
3133

3234
it('has correct id attribute', () => {
33-
const wrapper = mount(ATab, {
34-
propsData: {
35-
name: 'Sample'
36-
}
37-
})
35+
const wrapper = factory()
3836

39-
expect(wrapper.props().name).toBe('Sample')
37+
expect(wrapper.props().name).toBe('Tab')
4038
expect(wrapper.attributes().id).toBeDefined()
41-
expect(wrapper.attributes().id).toEqual('sample-tab')
39+
expect(wrapper.attributes().id).toEqual('tab-tab')
4240
})
4341
})

src/molecules/tabs/Tabs.html

+32-32
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,40 @@
1-
<div
2-
role="tablist"
3-
:class="getClass('tabs')"
4-
>
5-
<button
6-
v-for="(tab, i) in tabs"
7-
:key="`${tab.id}-tab-trigger`"
8-
:id="`${tab.id}-tab-trigger`"
9-
:aria-controls="`${tab.id}-tab`"
10-
:aria-selected="tab.isActive || 'false'"
11-
:aria-expanded="tab.isActive || 'false'"
12-
type="button"
13-
role="tab"
14-
:ref="`button_${i}`"
15-
:class="getClass(tab.isActive ? 'tabs__nav-button--active' : 'tabs__nav-button')""
16-
@click="selectTab(tab)"
17-
@keydown="onKeydown"
1+
<div :class="getClass('tabs')">
2+
<div
3+
role="tablist"
4+
:class="getClass('tabs__tablist')"
185
>
19-
<!-- @slot Custom tab button content (Scoped slot) -->
20-
<slot
21-
name="button"
22-
:tab="tab"
6+
<button
7+
v-for="(tab, i) in tabs"
8+
:key="`${tab.id}-tab-trigger`"
9+
:id="`${tab.id}-tab-trigger`"
10+
:aria-controls="`${tab.id}-tab`"
11+
:aria-selected="`${tab.isActive}`"
12+
:aria-expanded="`${tab.isActive}`"
13+
type="button"
14+
role="tab"
15+
:ref="`button_${i}`"
16+
:class="[
17+
getClass('tabs__nav-button'),
18+
tab.isActive ? getClass('tabs__nav-button--active') : getClass('tabs__nav-button--inactive')
19+
]"
20+
:tabindex="!tab.isActive && -1"
21+
@click="selectTab(tab)"
22+
@keydown="onKeydown"
2323
>
24-
{{ tab.name }}
25-
</slot>
26-
<!-- @slot Custom accordion tab icon (Named slot) -->
27-
<slot name="icon">
28-
29-
</slot>
30-
</button>
24+
<!-- @slot Custom tab button content (Scoped slot) -->
25+
<slot
26+
name="button"
27+
:tab="tab"
28+
>
29+
{{ tab.name }}
30+
</slot>
31+
</button>
32+
</div>
3133
<div
32-
ref="content"
33-
class="a-tabs__content"
3434
:class="getClass('tabs__content')"
35-
tabindex="-1"
35+
tabindex="0"
3636
>
3737
<!-- @slot Tab components -->
38-
<slot/>
38+
<slot />
3939
</div>
4040
</div>

src/molecules/tabs/Tabs.js

+33-36
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import KeyCodes from '../../utils/key-codes'
21
import getClass from '../../../utils/helpers/get-class.js'
32

43
export default {
@@ -9,17 +8,27 @@ export default {
98
activeFocusedTab: 0,
109
config: {
1110
base: {
12-
tabs: [
13-
'lg:flex', 'lg:flex-wrap', 'justify-center'
11+
tabs: [],
12+
tabs__tablist: [
13+
'flex', 'w-full', 'no-wrap',
14+
'overflow-x-scroll'
1415
],
1516
'tabs__nav-button': [
16-
'text-gray-600', 'py-4', 'px-6', 'block', 'hover:text-blue-500'
17+
'flex', 'items-center', 'justify-center', 'flex-grow',
18+
'py-4', 'px-6',
19+
'font-bold', 'uppercase', 'whitespace-nowrap'
20+
],
21+
'tabs__nav-button--inactive': [
22+
'border-b', 'border-gray-200', 'hover:border-primary',
23+
'text-gray-400'
1724
],
1825
'tabs__nav-button--active': [
19-
'text-gray-600', 'py-4', 'px-6', 'block', 'hover:text-blue-500', 'text-blue-500', 'border-b-2', 'font-medium', 'border-blue-500'
26+
'border-b', 'border-primary',
27+
'text-primary'
2028
],
2129
tabs__content: [
22-
'py-4'
30+
'w-full',
31+
'py-10'
2332
]
2433
}
2534
}
@@ -53,38 +62,26 @@ export default {
5362
this.$refs[el][0].focus()
5463
},
5564
onKeydown (e) {
56-
const key = e.keyCode
65+
const key = e.key
66+
const tabsCount = this.tabs.length
5767

58-
if (key === KeyCodes.TAB) {
59-
e.preventDefault()
60-
e.stopPropagation()
61-
this.$refs.content.focus()
62-
} else if (
63-
(key === KeyCodes.LEFT ||
64-
key === KeyCodes.UP ||
65-
key === KeyCodes.HOME) &&
66-
this.activeFocusedTab > 0
67-
) {
68-
if (key === KeyCodes.HOME) {
69-
this.focus('button_0')
70-
} else {
71-
this.activeFocusedTab = this.activeFocusedTab - 1
72-
this.focus(`button_${this.activeFocusedTab}`)
73-
}
74-
} else if (
75-
(key === KeyCodes.RIGHT ||
76-
key === KeyCodes.DOWN ||
77-
key === KeyCodes.END) &&
78-
this.activeFocusedTab < this.tabs.length - 1
79-
) {
80-
if (key === KeyCodes.END) {
81-
this.focus(`button_${this.tabs.length - 1}`)
82-
} else {
83-
this.activeFocusedTab = this.activeFocusedTab + 1
84-
this.focus(`button_${this.activeFocusedTab}`)
85-
}
68+
if (key === 'ArrowRight' || key === 'ArrowDown') {
69+
this.activeFocusedTab = (this.activeFocusedTab + 1) % tabsCount
70+
this.selectTab(this.activeFocusedTab)
71+
this.focus(`button_${this.activeFocusedTab}`)
72+
} else if (key === 'ArrowLeft' || key === 'ArrowUp') {
73+
this.activeFocusedTab = (this.activeFocusedTab - 1 + tabsCount) % tabsCount
74+
this.selectTab(this.activeFocusedTab)
75+
this.focus(`button_${this.activeFocusedTab}`)
76+
} else if (key === 'Home') {
77+
this.activeFocusedTab = 0
78+
this.selectTab(this.activeFocusedTab)
79+
this.focus(`button_${this.activeFocusedTab}`)
80+
} else if (key === 'End') {
81+
this.activeFocusedTab = tabsCount - 1
82+
this.selectTab(this.activeFocusedTab)
83+
this.focus(`button_${this.activeFocusedTab}`)
8684
}
87-
this.selectTab(this.activeFocusedTab)
8885
}
8986
}
9087
}

src/molecules/tabs/Tabs.stories.js

+39-45
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import AParagraph from '@atoms/paragraph/Paragraph.vue'
55
import AIcon from '@atoms/icon/Icon.vue'
66
import AIconStarBorder from '@atoms/icon/templates/IconStarBorder.vue'
77

8+
import tabs from '@mocks/tabs.json'
9+
810
export default {
911
title: 'Molecules/Tabs',
1012
component: ATabs,
@@ -20,31 +22,26 @@ const Template = (args, { argTypes }) => ({
2022
ATabs,
2123
ATab,
2224
AButton,
23-
AParagraph,
24-
AIcon,
25-
AIconStarBorder
25+
AParagraph
2626
},
2727
props: Object.keys(argTypes),
28+
data () {
29+
return {
30+
tabs
31+
}
32+
},
2833
template: `
2934
<a-tabs @click="tabClick">
30-
<a-tab
31-
name="Services"
32-
selected
33-
>
34-
<a-paragraph>
35-
Toe in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
36-
</a-paragraph>
37-
</a-tab>
38-
<a-tab name="Pricing">
39-
<a-paragraph>
40-
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
41-
</a-paragraph>
42-
</a-tab>
43-
<a-tab name="Reviews">
44-
<a-paragraph>
45-
Sed do eiusmod tempor incididunt ut labo. Ut enim ad minim veniam, re et dolore magna aliqua. quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
46-
</a-paragraph>
47-
</a-tab>
35+
<template v-for="(tab, index) in tabs">
36+
<a-tab
37+
:name="tab.name"
38+
:selected="tab.selected"
39+
>
40+
<a-paragraph>
41+
{{ tab.paragraph }}
42+
</a-paragraph>
43+
</a-tab>
44+
</template>
4845
</a-tabs>
4946
`
5047
})
@@ -61,38 +58,35 @@ export const WithSlots = (args, { argTypes }) => ({
6158
AIconStarBorder
6259
},
6360
props: Object.keys(argTypes),
61+
data () {
62+
return {
63+
tabs
64+
}
65+
},
6466
template: `
6567
<a-tabs @click="tabClick">
6668
<template #button="{ tab }">
67-
<a-icon title="Star border icon">
69+
<a-icon
70+
title="Star border icon"
71+
class="mr-2"
72+
>
6873
<a-icon-star-border />
6974
</a-icon>
7075
{{ tab.name }}
71-
<span
72-
v-if="tab.name === 'Reviews'"
73-
style='margin-left: 4px;'
76+
</template>
77+
<template v-for="(tab, index) in tabs">
78+
<a-tab
79+
:name="tab.name"
80+
:selected="tab.selected"
7481
>
75-
(3)
76-
</span>
82+
<a-paragraph>
83+
{{ tab.paragraph }}
84+
</a-paragraph>
85+
<a-button v-if="tab.button">
86+
{{ tab.button}}
87+
</a-button>
88+
</a-tab>
7789
</template>
78-
<a-tab name="Services">
79-
<a-paragraph>
80-
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
81-
</a-paragraph>
82-
</a-tab>
83-
<a-tab
84-
name="Pricing"
85-
selected
86-
>
87-
<a-button>
88-
Alpaca Button
89-
</a-button>
90-
</a-tab>
91-
<a-tab name="Reviews">
92-
<a-paragraph>
93-
Sed do eiusmod tempor incididunt ut labo. Ut enim ad minim veniam, re et dolore magna aliqua. quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
94-
</a-paragraph>
95-
</a-tab>
9690
</a-tabs>
9791
`
9892
})

0 commit comments

Comments
 (0)