Skip to content

Commit d787ed5

Browse files
committed
feat(CTabs): the initial release of the new react tabs component
1 parent 1912045 commit d787ed5

File tree

11 files changed

+866
-102
lines changed

11 files changed

+866
-102
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { defineComponent, h, inject, Ref } from 'vue'
2+
3+
const CTab = defineComponent({
4+
name: 'CTab',
5+
props: {
6+
/**
7+
* Item key.
8+
*/
9+
itemKey: {
10+
type: [Number, String],
11+
required: true,
12+
},
13+
},
14+
setup(props, { slots }) {
15+
const activeItemKey = inject('activeItemKey') as Ref<number | string>
16+
const id = inject('id') as Ref<number | string>
17+
const setActiveItemKey = inject('setActiveItemKey') as (key: number | string) => void
18+
19+
const isActive = () => props.itemKey === activeItemKey.value
20+
21+
return () =>
22+
h(
23+
'button',
24+
{
25+
class: [
26+
'nav-link',
27+
{
28+
active: isActive(),
29+
},
30+
],
31+
id: `${id.value}${props.itemKey}-tab`,
32+
role: 'tab',
33+
tabindex: isActive() ? 0 : -1,
34+
type: 'button',
35+
'aria-controls': `${id.value}${props.itemKey}-tab-pane`,
36+
'aria-selected': isActive(),
37+
onClick: () => setActiveItemKey(props.itemKey),
38+
onFocus: () => setActiveItemKey(props.itemKey),
39+
},
40+
slots.default && slots.default(),
41+
)
42+
},
43+
})
44+
45+
export { CTab }
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { defineComponent, h, ref } from 'vue'
2+
import { getNextActiveElement } from '../dropdown/utils'
3+
4+
const CTabList = defineComponent({
5+
name: 'CTabList',
6+
props: {
7+
/**
8+
* Specify a layout type for component.
9+
*
10+
* @values 'fill', 'justified'
11+
*/
12+
layout: {
13+
type: String,
14+
validator: (value: string) => {
15+
return ['fill', 'justified'].includes(value)
16+
},
17+
},
18+
/**
19+
* Set the nav variant to tabs or pills.
20+
*
21+
* @values 'pills', 'tabs', 'underline', 'underline-border'
22+
*/
23+
variant: {
24+
type: String,
25+
validator: (value: string) => {
26+
return ['pills', 'tabs', 'underline', 'underline-border'].includes(value)
27+
},
28+
},
29+
},
30+
setup(props, { slots }) {
31+
const tabListRef = ref<HTMLDivElement>()
32+
33+
const handleKeydown = (event: KeyboardEvent) => {
34+
if (
35+
tabListRef.value &&
36+
(event.key === 'ArrowDown' ||
37+
event.key === 'ArrowUp' ||
38+
event.key === 'ArrowLeft' ||
39+
event.key === 'ArrowRight' ||
40+
event.key === 'Home' ||
41+
event.key === 'End')
42+
) {
43+
event.preventDefault()
44+
const target = event.target as HTMLElement
45+
// eslint-disable-next-line unicorn/prefer-spread
46+
const items: HTMLElement[] = Array.from(
47+
tabListRef.value.querySelectorAll('.nav-link:not(.disabled):not(:disabled)'),
48+
)
49+
50+
let nextActiveElement
51+
52+
if (event.key === 'Home' || event.key === 'End') {
53+
nextActiveElement = event.key === 'End' ? items.at(-1) : items[0]
54+
} else {
55+
nextActiveElement = getNextActiveElement(
56+
items,
57+
target,
58+
event.key === 'ArrowDown' || event.key === 'ArrowRight',
59+
true,
60+
)
61+
}
62+
63+
if (nextActiveElement) {
64+
nextActiveElement.focus({ preventScroll: true })
65+
}
66+
}
67+
}
68+
69+
return () =>
70+
h(
71+
'div',
72+
{
73+
class: [
74+
'nav',
75+
{
76+
[`nav-${props.layout}`]: props.layout,
77+
[`nav-${props.variant}`]: props.variant,
78+
},
79+
],
80+
role: 'tablist',
81+
onKeydown: (event) => handleKeydown(event),
82+
ref: tabListRef,
83+
},
84+
slots.default && slots.default(),
85+
)
86+
},
87+
})
88+
89+
export { CTabList }
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import {
2+
defineComponent,
3+
h,
4+
inject,
5+
ref,
6+
Ref,
7+
RendererElement,
8+
Transition,
9+
vShow,
10+
watch,
11+
withDirectives,
12+
} from 'vue'
13+
14+
import { executeAfterTransition } from '../../utils/transition'
15+
16+
const CTabPanel = defineComponent({
17+
name: 'CTabPanel',
18+
props: {
19+
/**
20+
* Item key.
21+
*/
22+
itemKey: {
23+
type: [Number, String],
24+
required: true,
25+
},
26+
/**
27+
* Enable fade in and fade out transition.
28+
*/
29+
transition: {
30+
type: Boolean,
31+
default: true,
32+
},
33+
/**
34+
* Toggle the visibility of component.
35+
*/
36+
visible: {
37+
type: Boolean,
38+
default: false,
39+
},
40+
},
41+
emits: [
42+
/**
43+
* Callback fired when the component requests to be hidden.
44+
*/
45+
'hide',
46+
/**
47+
* Callback fired when the component requests to be shown.
48+
*/
49+
'show',
50+
],
51+
setup(props, { slots, emit }) {
52+
const activeItemKey = inject('activeItemKey') as Ref<number | string>
53+
const id = inject('id') as Ref<number | string>
54+
55+
const tabPaneRef = ref()
56+
const firstRender = ref(true)
57+
const visible = ref()
58+
59+
watch(
60+
() => props.visible,
61+
() => {
62+
visible.value = props.visible
63+
},
64+
{
65+
immediate: true,
66+
},
67+
)
68+
69+
watch(
70+
activeItemKey,
71+
() => {
72+
visible.value = Boolean(activeItemKey.value === props.itemKey)
73+
},
74+
{
75+
immediate: true,
76+
},
77+
)
78+
79+
const handleEnter = (el: RendererElement, done: () => void) => {
80+
firstRender.value = false
81+
emit('show')
82+
setTimeout(() => {
83+
executeAfterTransition(() => done(), el as HTMLElement)
84+
el.classList.add('show')
85+
}, 1)
86+
}
87+
88+
const handleLeave = (el: RendererElement, done: () => void) => {
89+
firstRender.value = false
90+
emit('hide')
91+
el.classList.remove('show')
92+
executeAfterTransition(() => done(), el as HTMLElement)
93+
}
94+
95+
return () =>
96+
h(
97+
Transition,
98+
{
99+
onEnter: (el, done) => handleEnter(el, done),
100+
onLeave: (el, done) => handleLeave(el, done),
101+
},
102+
() =>
103+
withDirectives(
104+
h(
105+
'div',
106+
{
107+
class: [
108+
'tab-pane',
109+
{
110+
active: visible.value,
111+
fade: props.transition,
112+
show: firstRender.value && visible.value,
113+
},
114+
],
115+
id: `${id.value}${props.itemKey}-tab-pane`,
116+
role: 'tabpanel',
117+
'aria-labelledby': `${id.value}${props.itemKey}-tab`,
118+
tabindex: 0,
119+
ref: tabPaneRef,
120+
},
121+
slots.default && slots.default(),
122+
),
123+
[[vShow, visible.value]],
124+
),
125+
)
126+
},
127+
})
128+
129+
export { CTabPanel }
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { defineComponent, h, provide, ref, watch } from 'vue'
2+
import { getUID } from '../../utils'
3+
4+
const CTabs = defineComponent({
5+
name: 'CTabs',
6+
props: {
7+
/**
8+
* The active item key.
9+
*/
10+
activeItemKey: {
11+
type: [Number, String],
12+
required: true,
13+
}
14+
},
15+
emits: [
16+
/**
17+
* The callback is fired when the active tab changes.
18+
*/
19+
'change',
20+
],
21+
setup(props, { slots, emit }) {
22+
const uID = ref(getUID('t'))
23+
const activeItemKey = ref(props.activeItemKey)
24+
const setActiveItemKey = (key: string | number) => {
25+
activeItemKey.value = key
26+
}
27+
28+
watch(
29+
() => props.activeItemKey,
30+
(value) => {
31+
activeItemKey.value = value
32+
emit('change', value)
33+
},
34+
)
35+
36+
provide('activeItemKey', activeItemKey)
37+
provide('id', uID)
38+
provide('setActiveItemKey', setActiveItemKey)
39+
40+
return () => h('div', { class: 'tabs' }, slots.default && slots.default())
41+
},
42+
})
43+
44+
export { CTabs }
Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
11
import { App } from 'vue'
2+
import { CTab } from './CTab'
23
import { CTabContent } from './CTabContent'
4+
import { CTabList } from './CTabList'
35
import { CTabPane } from './CTabPane'
6+
import { CTabPanel } from './CTabPanel'
7+
import { CTabs } from './CTabs'
48

59
const CTabsPlugin = {
610
install: (app: App): void => {
11+
app.component(CTab.name as string, CTab)
712
app.component(CTabContent.name as string, CTabContent)
13+
app.component(CTabList.name as string, CTabList)
814
app.component(CTabPane.name as string, CTabPane)
15+
app.component(CTabPanel.name as string, CTabPanel)
16+
app.component(CTabs.name as string, CTabs)
917
},
1018
}
1119

12-
export { CTabsPlugin, CTabContent, CTabPane }
20+
export { CTabsPlugin, CTab, CTabContent, CTabList, CTabPane, CTabPanel, CTabs }

packages/docs/.vuepress/config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,10 @@ export default defineUserConfig({
328328
text: 'Table',
329329
link: `/components/table.html`,
330330
},
331+
{
332+
text: 'Tabs',
333+
link: `/components/tabs.html`,
334+
},
331335
{
332336
text: 'Toast',
333337
link: `/components/toast.html`,

packages/docs/api/tabs/CTab.api.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
### CTab
2+
3+
```jsx
4+
import { CTab } from '@coreui/vue'
5+
// or
6+
import CTab from '@coreui/vue/src/components/tabs/CTab'
7+
```
8+
9+
#### Props
10+
11+
| Prop name | Description | Type | Values | Default |
12+
| ------------ | ----------- | -------------- | ------ | ------- |
13+
| **item-key** | Item key. | number\|string | - | - |
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
### CTabList
2+
3+
```jsx
4+
import { CTabList } from '@coreui/vue'
5+
// or
6+
import CTabList from '@coreui/vue/src/components/tabs/CTabList'
7+
```
8+
9+
#### Props
10+
11+
| Prop name | Description | Type | Values | Default |
12+
| ----------- | ------------------------------------- | ------ | -------------------------------------------------------- | ------- |
13+
| **layout** | Specify a layout type for component. | string | `'fill'`, `'justified'` | - |
14+
| **variant** | Set the nav variant to tabs or pills. | string | `'pills'`, `'tabs'`, `'underline'`, `'underline-border'` | - |
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
### CTabPanel
2+
3+
```jsx
4+
import { CTabPanel } from '@coreui/vue'
5+
// or
6+
import CTabPanel from '@coreui/vue/src/components/tabs/CTabPanel'
7+
```
8+
9+
#### Props
10+
11+
| Prop name | Description | Type | Values | Default |
12+
| -------------- | --------------------------------------- | -------------- | ------ | ------- |
13+
| **item-key** | Item key. | number\|string | - | - |
14+
| **transition** | Enable fade in and fade out transition. | boolean | - | true |
15+
| **visible** | Toggle the visibility of component. | boolean | - | false |
16+
17+
#### Events
18+
19+
| Event name | Description | Properties |
20+
| ---------- | -------------------------------------------------------- | ---------- |
21+
| **hide** | Callback fired when the component requests to be hidden. |
22+
| **show** | Callback fired when the component requests to be shown. |

0 commit comments

Comments
 (0)