Skip to content

Commit 6494d32

Browse files
committed
✨ Add Tab component improvements
1 parent 7bbc151 commit 6494d32

11 files changed

Lines changed: 290 additions & 51 deletions

File tree

scripts/createComponent.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,10 @@ const templates = {
5353
className
5454
}: Props = $props()
5555
56-
const classes = classNames([
56+
const classes = $derived(classNames([
5757
styles.${lowerCaseComponent},
5858
className
59-
])
59+
]))
6060
</script>
6161
`,
6262
react: `

src/components/Tab/Tab.astro

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
import type { HTMLAttributes } from 'astro/types'
3+
import type { TabProps } from './tab'
4+
5+
export type Props = TabProps<HTMLAttributes<'section'>>
6+
7+
const {
8+
element = 'div',
9+
id,
10+
active,
11+
className,
12+
...rest
13+
} = Astro.props
14+
15+
const Element = element
16+
17+
const props = {
18+
...rest,
19+
'class': className,
20+
'data-tab': id,
21+
'data-active': active
22+
}
23+
---
24+
25+
<Element {...props}><slot /></Element>

src/components/Tab/Tab.svelte

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<script lang="ts">
2+
import type { Snippet } from 'svelte'
3+
import type { HTMLAttributes } from 'svelte/elements'
4+
import type { TabProps } from './tab'
5+
6+
export type Props = TabProps<HTMLAttributes<HTMLElement>> & {
7+
children: Snippet
8+
}
9+
10+
const {
11+
element = 'div',
12+
id,
13+
active,
14+
className,
15+
children,
16+
...rest
17+
}: Props = $props()
18+
</script>
19+
20+
<svelte:element
21+
{...rest}
22+
{...(className && { class: className })}
23+
this={element}
24+
data-tab={id}
25+
data-active={active}
26+
>
27+
{@render children?.()}
28+
</svelte:element>

src/components/Tab/Tab.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import React from 'react'
2+
import type { TabProps } from './tab'
3+
4+
export type Props = TabProps<React.HTMLAttributes<HTMLElement>> & {
5+
children: React.ReactNode
6+
}
7+
8+
const Tab = ({
9+
element = 'div',
10+
id,
11+
active,
12+
className,
13+
children,
14+
...rest
15+
}: Props) => {
16+
const Element = element as React.ElementType<React.HTMLAttributes<HTMLElement>>
17+
18+
return (
19+
<Element
20+
{...rest}
21+
className={className}
22+
data-tab={id}
23+
data-active={active}
24+
>
25+
{children}
26+
</Element>
27+
)
28+
}
29+
30+
export default Tab

src/components/Tab/tab.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export type TabProps<T extends object = object> = {
2+
element?: string
3+
id?: string
4+
active?: boolean
5+
className?: string
6+
} & T

src/components/Tabs/Tabs.astro

Lines changed: 55 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ const classes = [
2020
even && styles.even,
2121
className
2222
]
23+
24+
const hasActive = items.some(item => item.active)
25+
26+
if (!hasActive) {
27+
items[0].active = true
28+
}
2329
---
2430

2531
<section class:list={classes} data-id="w-tabs">
@@ -36,45 +42,70 @@ const classes = [
3642
))}
3743
</div>
3844
</div>
39-
<div class={styles.content}>
45+
<div
46+
class={styles.content}
47+
data-id="w-tabs-content"
48+
data-active-index={items.findIndex(item => item.active)}
49+
>
4050
<slot />
4151
</div>
4252
</section>
4353

4454
<script>
45-
import { on } from '../../utils/DOMUtils'
55+
import { get, on } from '../../utils/DOMUtils'
56+
57+
const setActiveTab = () => {
58+
const tabContents = get('[data-id="w-tabs-content"]', true) as NodeListOf<HTMLDivElement>
59+
60+
tabContents.forEach((tabContent) => {
61+
const contentChildren = Array.from(tabContent.children) as HTMLElement[]
62+
63+
if (!contentChildren.some(element => element.dataset.active === 'true')) {
64+
const index = Number(tabContent.dataset.activeIndex)
65+
66+
contentChildren[index].dataset.active = 'true'
67+
}
68+
})
69+
}
4670

4771
const addEventListeners = () => {
4872
on('[data-id="w-tabs"]', 'click', (event: Event) => {
49-
const target = event.target as HTMLDivElement
73+
const target = event.target as HTMLButtonElement
74+
75+
if (!target.dataset.value) {
76+
return
77+
}
5078

51-
if (target.dataset.value) {
52-
const tabContent = target.parentElement
53-
?.parentElement
54-
?.nextElementSibling as HTMLDivElement
79+
const tabContent = target.parentElement
80+
?.parentElement
81+
?.nextElementSibling as HTMLDivElement
5582

56-
Array.from(tabContent.children)
57-
.forEach((element: any) => {
58-
if (element.dataset.tab === target.dataset.value) {
59-
element.dataset.active = true
60-
} else {
61-
element.dataset.active = false
62-
}
63-
})
83+
const btns = Array.from(target.parentElement?.querySelectorAll('button') as NodeListOf<HTMLButtonElement>)
84+
const clickedIndex = btns.indexOf(target)
85+
const contentChildren = Array.from(tabContent.children) as HTMLElement[]
86+
const hasExplicitTabs = contentChildren.some((element: HTMLElement) => element.dataset.tab)
6487

65-
const tabs = target.parentElement?.querySelectorAll('button') as NodeListOf<HTMLButtonElement>
88+
btns.forEach((tab: HTMLElement, index: number) => {
89+
tab.dataset.active = index === clickedIndex ? 'true' : 'false'
6690

67-
Array.from(tabs).forEach((tab: any) => {
68-
tab.dataset.active = 'false'
91+
if (contentChildren[index]) {
92+
const content = contentChildren[index]
6993

70-
if (tab.dataset.value === target.dataset.value) {
71-
tab.dataset.active = 'true'
94+
if (hasExplicitTabs) {
95+
content.dataset.active = content.dataset.tab === target.dataset.value ? 'true' : 'false'
96+
} else {
97+
content.dataset.active = index === clickedIndex ? 'true' : 'false'
7298
}
73-
})
74-
}
99+
}
100+
})
75101
}, true)
76102
}
77103

78-
on(document, 'astro:after-swap', addEventListeners)
79-
addEventListeners()
104+
const initTabs = () => {
105+
setActiveTab()
106+
addEventListeners()
107+
}
108+
109+
on(document, 'astro:after-swap', initTabs)
110+
initTabs()
80111
</script>

src/components/Tabs/Tabs.svelte

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script lang="ts">
2-
import type { Snippet } from 'svelte'
2+
import { onMount, type Snippet } from 'svelte'
33
import type { TabsProps } from './tabs'
44
55
import { classNames } from '../../utils/classNames'
@@ -22,6 +22,16 @@
2222
let active = $state('')
2323
let tabContainer: HTMLDivElement | undefined = $state()
2424
25+
const usedInAstro = $derived(tabContainer?.children[0]?.nodeName === 'ASTRO-SLOT')
26+
const hasActive = $derived(items.some(item => item.active))
27+
const itemsState = $derived.by(() => {
28+
if (!hasActive) {
29+
items[0].active = true
30+
}
31+
32+
return items
33+
})
34+
2535
const classes = $derived(classNames([
2636
styles.tabs,
2737
theme && styles[theme],
@@ -30,29 +40,45 @@
3040
className
3141
]))
3242
33-
const setTab = (tab: string) => {
34-
const tabs = tabContainer!.querySelectorAll('[data-tab]')
43+
const setTab = (tab: string, index: number) => {
44+
const contentChildren = usedInAstro
45+
? Array.from(tabContainer!.children[0].children) as HTMLElement[]
46+
: Array.from(tabContainer!.children) as HTMLElement[]
3547
36-
active = tab
48+
const hasExplicitTabs = contentChildren.some((el: HTMLElement) => el.dataset.tab)
3749
38-
Array.from(tabs).forEach((item: any) => {
39-
item.dataset.active = false
40-
41-
if (item.dataset.tab === active) {
42-
item.dataset.active = true
50+
contentChildren.forEach((item: HTMLElement, i: number) => {
51+
if (hasExplicitTabs) {
52+
item.dataset.active = item.dataset.tab === tab ? 'true' : 'false'
53+
} else {
54+
item.dataset.active = i === index ? 'true' : 'false'
4355
}
4456
})
57+
58+
active = tab
4559
}
60+
61+
onMount(() => {
62+
const contentChildren = usedInAstro
63+
? Array.from(tabContainer!.children[0].children) as HTMLElement[]
64+
: Array.from(tabContainer!.children) as HTMLElement[]
65+
66+
if (!contentChildren.some(element => element.dataset.active === 'true')) {
67+
const index = itemsState.findIndex(item => item.active)
68+
69+
contentChildren[index].dataset.active = 'true'
70+
}
71+
})
4672
</script>
4773

4874
<section class={classes}>
4975
<div class={styles.wrapper}>
5076
<div class={styles.items}>
51-
{#each items as item}
77+
{#each itemsState as item, i}
5278
<button
5379
data-active={active ? active === item.value : item.active}
5480
disabled={item.disabled}
55-
onclick={() => setTab(item.value)}
81+
onclick={() => setTab(item.value, i)}
5682
>
5783
{@html item.label}
5884
</button>

src/components/Tabs/Tabs.tsx

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useRef,useState } from 'react'
1+
import React, { useEffect, useRef, useState } from 'react'
22
import type { TabsProps } from './tabs'
33

44
import { classNames } from '../../utils/classNames'
@@ -18,7 +18,10 @@ const Tabs = ({
1818
children
1919
}: Props) => {
2020
const tabContainer = useRef<HTMLDivElement>(null)
21+
const usedInAstro = useRef(false)
22+
2123
const [active, setActive] = useState('')
24+
const hasActive = items.some(item => item.active)
2225

2326
const classes = classNames([
2427
styles.tabs,
@@ -28,14 +31,18 @@ const Tabs = ({
2831
className
2932
])
3033

31-
const setTab = (tab: string) => {
32-
const tabs = tabContainer.current!.querySelectorAll('[data-tab]')
34+
const setTab = (tab: string, index: number) => {
35+
const contentChildren = usedInAstro.current
36+
? Array.from(tabContainer.current!.children[0].children) as HTMLElement[]
37+
: Array.from(tabContainer.current!.children) as HTMLElement[]
3338

34-
Array.from(tabs).forEach((item: any) => {
35-
item.dataset.active = false
39+
const hasExplicitTabs = contentChildren.some((el: HTMLElement) => el.dataset.tab)
3640

37-
if (item.dataset.tab === tab) {
38-
item.dataset.active = true
41+
contentChildren.forEach((item: HTMLElement, i: number) => {
42+
if (hasExplicitTabs) {
43+
item.dataset.active = item.dataset.tab === tab ? 'true' : 'false'
44+
} else {
45+
item.dataset.active = i === index ? 'true' : 'false'
3946
}
4047
})
4148

@@ -50,6 +57,24 @@ const Tabs = ({
5057
return active === item.value ? 'true' : undefined
5158
}
5259

60+
if (!hasActive) {
61+
items[0].active = true
62+
}
63+
64+
useEffect(() => {
65+
usedInAstro.current = tabContainer.current?.children[0]?.nodeName === 'ASTRO-SLOT'
66+
67+
const contentChildren = usedInAstro.current
68+
? Array.from(tabContainer.current!.children[0].children) as HTMLElement[]
69+
: Array.from(tabContainer.current!.children) as HTMLElement[]
70+
71+
if (!contentChildren.some(element => element.dataset.active === 'true')) {
72+
const index = items.findIndex(item => item.active)
73+
74+
contentChildren[index].dataset.active = 'true'
75+
}
76+
}, [])
77+
5378
return (
5479
<section className={classes}>
5580
<div className={styles.wrapper}>
@@ -59,7 +84,7 @@ const Tabs = ({
5984
key={index}
6085
disabled={item.disabled}
6186
dangerouslySetInnerHTML={{ __html: item.label }}
62-
onClick={() => setTab(item.value)}
87+
onClick={() => setTab(item.value, index)}
6388
data-active={isActive(item)}
6489
/>
6590
))}

src/components/Tabs/tabs.module.scss

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -110,13 +110,14 @@
110110

111111
.content {
112112
@include spacing(mt-default);
113-
}
114-
115-
[data-tab] {
116-
@include visibility(none);
117113

118-
&[data-active="true"] {
119-
@include visibility(block);
114+
& > *:not(astro-slot),
115+
& > astro-slot > * {
116+
@include visibility(none);
117+
118+
&[data-active="true"] {
119+
@include visibility(block);
120+
}
120121
}
121122
}
122123
}

0 commit comments

Comments
 (0)