Skip to content

Commit f3b5265

Browse files
committed
Improve tabs link support
1 parent f7a3470 commit f3b5265

File tree

4 files changed

+170
-133
lines changed

4 files changed

+170
-133
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { type ClassValue, tcls } from '@/lib/tailwind';
2+
import type { DocumentBlockHeading, DocumentBlockTabs } from '@gitbook/api';
3+
import { Icon } from '@gitbook/icons';
4+
import { getBlockTextStyle } from './spacing';
5+
6+
/**
7+
* A hash icon which adds the block or active block item's ID in the URL hash.
8+
*/
9+
export function HashLinkButton(props: {
10+
id: string;
11+
block: DocumentBlockTabs | DocumentBlockHeading;
12+
label?: string;
13+
className?: ClassValue;
14+
}) {
15+
const { id, block, className, label = 'Direct link to block' } = props;
16+
const textStyle = getBlockTextStyle(block);
17+
return (
18+
<div
19+
className={tcls(
20+
'hash',
21+
'grid',
22+
'grid-area-1-1',
23+
// 'absolute',
24+
// 'left-0',
25+
// 'top-1',
26+
'w-7',
27+
'border-0',
28+
'opacity-0',
29+
'group-hover:opacity-[0]',
30+
'group-focus:opacity-[0]',
31+
'md:group-hover:md:opacity-[1]',
32+
'md:group-focus:md:opacity-[1]',
33+
className
34+
)}
35+
>
36+
<a
37+
href={`#${id}`}
38+
aria-label={label}
39+
className={tcls('inline-flex', 'h-full', 'items-start', textStyle.lineHeight)}
40+
>
41+
<Icon
42+
icon="hashtag"
43+
className={tcls(
44+
'size-4',
45+
'transition-colors',
46+
'text-transparent',
47+
'group-hover:text-tint-subtle',
48+
'contrast-more:group-hover:text-tint-strong'
49+
)}
50+
/>
51+
</a>
52+
</div>
53+
);
54+
}

packages/gitbook/src/components/DocumentView/Heading.tsx

Lines changed: 7 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import type { DocumentBlockHeading } from '@gitbook/api';
2-
import { Icon } from '@gitbook/icons';
32

43
import { tcls } from '@/lib/tailwind';
54

65
import type { BlockProps } from './Block';
6+
import { HashLinkButton } from './HashLinkButton';
77
import { Inlines } from './Inlines';
88
import { getBlockTextStyle } from './spacing';
99

@@ -30,43 +30,12 @@ export function Heading(props: BlockProps<DocumentBlockHeading>) {
3030
style
3131
)}
3232
>
33-
<div
34-
className={tcls(
35-
'hash',
36-
'grid',
37-
'grid-area-1-1',
38-
'relative',
39-
'-ml-6',
40-
'w-7',
41-
'border-0',
42-
'opacity-0',
43-
'group-hover:opacity-[0]',
44-
'group-focus:opacity-[0]',
45-
'md:group-hover:md:opacity-[1]',
46-
'md:group-focus:md:opacity-[1]',
47-
textStyle.marginTop
48-
)}
49-
>
50-
<a
51-
href={`#${id}`}
52-
aria-label="Direct link to heading"
53-
className={tcls('inline-flex', 'h-full', 'items-start', textStyle.lineHeight)}
54-
>
55-
<Icon
56-
icon="hashtag"
57-
className={tcls(
58-
'w-3.5',
59-
'h-[1em]',
60-
'mt-0.5',
61-
'transition-colors',
62-
'text-transparent',
63-
'group-hover:text-tint-subtle',
64-
'contrast-more:group-hover:text-tint-strong',
65-
'lg:w-4'
66-
)}
67-
/>
68-
</a>
69-
</div>
33+
<HashLinkButton
34+
id={id}
35+
block={block}
36+
className={tcls('relative', '-ml-6', 'top-1', textStyle.marginTop)}
37+
/>
38+
7039
<div
7140
className={tcls(
7241
'grid-area-1-1',

packages/gitbook/src/components/DocumentView/Tabs/DynamicTabs.tsx

Lines changed: 105 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import React, { useCallback, useMemo } from 'react';
55
import { useHash, useIsMounted } from '@/components/hooks';
66
import * as storage from '@/lib/local-storage';
77
import { type ClassValue, tcls } from '@/lib/tailwind';
8+
import type { DocumentBlockTabs } from '@gitbook/api';
9+
import { HashLinkButton } from '../HashLinkButton';
810

911
interface TabsState {
1012
activeIds: {
@@ -68,9 +70,10 @@ export function DynamicTabs(
6870
props: TabsInput & {
6971
tabsBody: React.ReactNode[];
7072
style: ClassValue;
73+
block: DocumentBlockTabs;
7174
}
7275
) {
73-
const { id, tabs, tabsBody, style } = props;
76+
const { id, block, tabs, tabsBody, style } = props;
7477

7578
const hash = useHash();
7679
const [tabsState, setTabsState] = useTabsState();
@@ -138,107 +141,115 @@ export function DynamicTabs(
138141
}, [hash, tabs, onSelectTab]);
139142

140143
return (
141-
<div
142-
className={tcls(
143-
'rounded-lg',
144-
'straight-corners:rounded-sm',
145-
'ring-1',
146-
'ring-inset',
147-
'ring-tint-subtle',
148-
'flex',
149-
'overflow-hidden',
150-
'flex-col',
151-
style
152-
)}
153-
>
144+
<div className={tcls('relative', 'group')}>
145+
<HashLinkButton
146+
id={getTabButtonId(active.id)}
147+
block={block}
148+
className={tcls('absolute', 'left-0', 'top-1')}
149+
/>
150+
154151
<div
155-
role="tablist"
156152
className={tcls(
157-
'group/tabs',
158-
'inline-flex',
159-
'flex-row',
160-
'self-stretch',
161-
'after:flex-[1]',
162-
'after:bg-tint-12/1',
163-
// if last tab is selected, apply rounded to :after element
164-
'[&:has(button.active-tab:last-of-type):after]:rounded-bl-md'
153+
'rounded-lg',
154+
'straight-corners:rounded-sm',
155+
'ring-1',
156+
'ring-inset',
157+
'ring-tint-subtle',
158+
'flex',
159+
'overflow-hidden',
160+
'flex-col',
161+
style
165162
)}
166163
>
167-
{tabs.map((tab) => (
168-
<button
164+
<div
165+
role="tablist"
166+
className={tcls(
167+
'group/tabs',
168+
'inline-flex',
169+
'flex-row',
170+
'self-stretch',
171+
'after:flex-[1]',
172+
'after:bg-tint-12/1',
173+
// if last tab is selected, apply rounded to :after element
174+
'[&:has(button.active-tab:last-of-type):after]:rounded-bl-md'
175+
)}
176+
>
177+
{tabs.map((tab) => (
178+
<button
179+
key={tab.id}
180+
role="tab"
181+
aria-selected={active.id === tab.id}
182+
aria-controls={getTabPanelId(tab.id)}
183+
id={getTabButtonId(tab.id)}
184+
onClick={() => {
185+
onSelectTab(tab);
186+
}}
187+
className={tcls(
188+
//prev from active-tab
189+
'[&:has(+_.active-tab)]:rounded-br-md',
190+
191+
//next from active-tab
192+
'[.active-tab_+_&]:rounded-bl-md',
193+
194+
//next from active-tab
195+
'[.active-tab_+_:after]:rounded-br-md',
196+
197+
'inline-block',
198+
'text-sm',
199+
'px-3.5',
200+
'py-2',
201+
'transition-[color]',
202+
'font-[500]',
203+
'relative',
204+
205+
'after:transition-colors',
206+
'after:border-r',
207+
'after:absolute',
208+
'after:left-[unset]',
209+
'after:right-0',
210+
'after:border-tint',
211+
'after:top-[15%]',
212+
'after:h-[70%]',
213+
'after:w-[1px]',
214+
215+
'last:after:border-transparent',
216+
217+
'text-tint',
218+
'bg-tint-12/1',
219+
'hover:text-tint-strong',
220+
221+
'truncate',
222+
'max-w-full',
223+
224+
active.id === tab.id
225+
? [
226+
'shrink-0',
227+
'active-tab',
228+
'text-tint-strong',
229+
'bg-transparent',
230+
'after:[&.active-tab]:border-transparent',
231+
'after:[:has(+_&.active-tab)]:border-transparent',
232+
'after:[:has(&_+)]:border-transparent',
233+
]
234+
: null
235+
)}
236+
>
237+
{tab.title}
238+
</button>
239+
))}
240+
</div>
241+
{tabs.map((tab, index) => (
242+
<div
169243
key={tab.id}
170-
role="tab"
171-
aria-selected={active.id === tab.id}
172-
aria-controls={getTabPanelId(tab.id)}
173-
id={getTabButtonId(tab.id)}
174-
onClick={() => {
175-
onSelectTab(tab);
176-
}}
177-
className={tcls(
178-
//prev from active-tab
179-
'[&:has(+_.active-tab)]:rounded-br-md',
180-
181-
//next from active-tab
182-
'[.active-tab_+_&]:rounded-bl-md',
183-
184-
//next from active-tab
185-
'[.active-tab_+_:after]:rounded-br-md',
186-
187-
'inline-block',
188-
'text-sm',
189-
'px-3.5',
190-
'py-2',
191-
'transition-[color]',
192-
'font-[500]',
193-
'relative',
194-
195-
'after:transition-colors',
196-
'after:border-r',
197-
'after:absolute',
198-
'after:left-[unset]',
199-
'after:right-0',
200-
'after:border-tint',
201-
'after:top-[15%]',
202-
'after:h-[70%]',
203-
'after:w-[1px]',
204-
205-
'last:after:border-transparent',
206-
207-
'text-tint',
208-
'bg-tint-12/1',
209-
'hover:text-tint-strong',
210-
211-
'truncate',
212-
'max-w-full',
213-
214-
active.id === tab.id
215-
? [
216-
'shrink-0',
217-
'active-tab',
218-
'text-tint-strong',
219-
'bg-transparent',
220-
'after:[&.active-tab]:border-transparent',
221-
'after:[:has(+_&.active-tab)]:border-transparent',
222-
'after:[:has(&_+)]:border-transparent',
223-
]
224-
: null
225-
)}
244+
role="tabpanel"
245+
id={getTabPanelId(tab.id)}
246+
aria-labelledby={getTabButtonId(tab.id)}
247+
className={tcls('p-4', tab.id !== active.id ? 'hidden' : null)}
226248
>
227-
{tab.title}
228-
</button>
249+
{tabsBody[index]}
250+
</div>
229251
))}
230252
</div>
231-
{tabs.map((tab, index) => (
232-
<div
233-
key={tab.id}
234-
role="tabpanel"
235-
id={getTabPanelId(tab.id)}
236-
aria-labelledby={getTabButtonId(tab.id)}
237-
className={tcls('p-4', tab.id !== active.id ? 'hidden' : null)}
238-
>
239-
{tabsBody[index]}
240-
</div>
241-
))}
242253
</div>
243254
);
244255
}

packages/gitbook/src/components/DocumentView/Tabs/Tabs.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export function Tabs(props: BlockProps<DocumentBlockTabs>) {
3939
<DynamicTabs
4040
key={tab.id}
4141
id={block.key!}
42+
block={block}
4243
tabs={[tab]}
4344
tabsBody={[tabsBody[index]]}
4445
style={style}
@@ -48,5 +49,7 @@ export function Tabs(props: BlockProps<DocumentBlockTabs>) {
4849
);
4950
}
5051

51-
return <DynamicTabs id={block.key!} tabs={tabs} tabsBody={tabsBody} style={style} />;
52+
return (
53+
<DynamicTabs id={block.key!} block={block} tabs={tabs} tabsBody={tabsBody} style={style} />
54+
);
5255
}

0 commit comments

Comments
 (0)