Skip to content

Commit 08382ce

Browse files
authored
Improve tabs link support (#3211)
1 parent 80cb52a commit 08382ce

File tree

5 files changed

+120
-63
lines changed

5 files changed

+120
-63
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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+
* The button needs to be wrapped in a container with `hashLinkButtonWrapperStyles`.
9+
*/
10+
export const hashLinkButtonWrapperStyles = tcls('relative', 'group/hash');
11+
12+
export function HashLinkButton(props: {
13+
id: string;
14+
block: DocumentBlockTabs | DocumentBlockHeading;
15+
label?: string;
16+
className?: ClassValue;
17+
iconClassName?: ClassValue;
18+
}) {
19+
const { id, block, className, iconClassName, label = 'Direct link to block' } = props;
20+
const textStyle = getBlockTextStyle(block);
21+
return (
22+
<div
23+
className={tcls(
24+
'relative',
25+
'hash',
26+
'grid',
27+
'grid-area-1-1',
28+
'h-[1em]',
29+
'border-0',
30+
'opacity-0',
31+
'group-hover/hash:opacity-[0]',
32+
'group-focus/hash:opacity-[0]',
33+
'md:group-hover/hash:md:opacity-[1]',
34+
'md:group-focus/hash:md:opacity-[1]',
35+
className
36+
)}
37+
>
38+
<a
39+
href={`#${id}`}
40+
aria-label={label}
41+
className={tcls('inline-flex', 'h-full', 'items-start', textStyle.lineHeight)}
42+
>
43+
<Icon
44+
icon="hashtag"
45+
className={tcls(
46+
'size-3',
47+
'self-center',
48+
'transition-colors',
49+
'text-transparent',
50+
'group-hover/hash:text-tint-subtle',
51+
'contrast-more:group-hover/hash:text-tint-strong',
52+
iconClassName
53+
)}
54+
/>
55+
</a>
56+
</div>
57+
);
58+
}

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

+10-40
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, hashLinkButtonWrapperStyles } from './HashLinkButton';
77
import { Inlines } from './Inlines';
88
import { getBlockTextStyle } from './spacing';
99

@@ -23,50 +23,20 @@ export function Heading(props: BlockProps<DocumentBlockHeading>) {
2323
className={tcls(
2424
textStyle.textSize,
2525
'heading',
26-
'group',
27-
'relative',
2826
'grid',
2927
'scroll-m-12',
28+
hashLinkButtonWrapperStyles,
3029
style
3130
)}
3231
>
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>
32+
<HashLinkButton
33+
id={id}
34+
block={block}
35+
className={tcls('-ml-6', textStyle.anchorButtonMarginTop)}
36+
iconClassName={tcls('size-4')}
37+
label="Direct link to heading"
38+
/>
39+
7040
<div
7141
className={tcls(
7242
'grid-area-1-1',

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

+43-22
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, hashLinkButtonWrapperStyles } 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();
@@ -146,8 +149,8 @@ export function DynamicTabs(
146149
'ring-inset',
147150
'ring-tint-subtle',
148151
'flex',
149-
'overflow-hidden',
150152
'flex-col',
153+
'overflow-hidden',
151154
style
152155
)}
153156
>
@@ -165,16 +168,14 @@ export function DynamicTabs(
165168
)}
166169
>
167170
{tabs.map((tab) => (
168-
<button
171+
<div
169172
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-
}}
177173
className={tcls(
174+
hashLinkButtonWrapperStyles,
175+
'flex',
176+
'items-center',
177+
'gap-3.5',
178+
178179
//prev from active-tab
179180
'[&:has(+_.active-tab)]:rounded-br-md',
180181

@@ -184,14 +185,6 @@ export function DynamicTabs(
184185
//next from active-tab
185186
'[.active-tab_+_:after]:rounded-br-md',
186187

187-
'inline-block',
188-
'text-sm',
189-
'px-3.5',
190-
'py-2',
191-
'transition-[color]',
192-
'font-[500]',
193-
'relative',
194-
195188
'after:transition-colors',
196189
'after:border-r',
197190
'after:absolute',
@@ -202,14 +195,16 @@ export function DynamicTabs(
202195
'after:h-[70%]',
203196
'after:w-[1px]',
204197

198+
'px-3.5',
199+
'py-2',
200+
205201
'last:after:border-transparent',
206202

207203
'text-tint',
208204
'bg-tint-12/1',
209205
'hover:text-tint-strong',
210-
211-
'truncate',
212206
'max-w-full',
207+
'truncate',
213208

214209
active.id === tab.id
215210
? [
@@ -224,8 +219,34 @@ export function DynamicTabs(
224219
: null
225220
)}
226221
>
227-
{tab.title}
228-
</button>
222+
<button
223+
type="button"
224+
role="tab"
225+
aria-selected={active.id === tab.id}
226+
aria-controls={getTabPanelId(tab.id)}
227+
id={getTabButtonId(tab.id)}
228+
onClick={() => {
229+
onSelectTab(tab);
230+
}}
231+
className={tcls(
232+
'inline-block',
233+
'text-sm',
234+
'transition-[color]',
235+
'font-[500]',
236+
'relative',
237+
'max-w-full',
238+
'truncate'
239+
)}
240+
>
241+
{tab.title}
242+
</button>
243+
244+
<HashLinkButton
245+
id={getTabButtonId(tab.id)}
246+
block={block}
247+
label="Direct link to tab"
248+
/>
249+
</div>
229250
))}
230251
</div>
231252
{tabs.map((tab, index) => (

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

+4-1
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
}

packages/gitbook/src/components/DocumentView/spacing.ts

+5
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ export function getBlockTextStyle(block: DocumentBlock): {
1010
lineHeight: string;
1111
/** Tailwind class for the margin top (mt-*) */
1212
marginTop?: string;
13+
/** Tailwind class for the margin top to apply on the anchor link button */
14+
anchorButtonMarginTop?: string;
1315
} {
1416
switch (block.type) {
1517
case 'paragraph':
@@ -22,18 +24,21 @@ export function getBlockTextStyle(block: DocumentBlock): {
2224
textSize: 'text-3xl font-semibold',
2325
lineHeight: 'leading-tight',
2426
marginTop: 'mt-[1em]',
27+
anchorButtonMarginTop: 'mt-[1.05em]',
2528
};
2629
case 'heading-2':
2730
return {
2831
textSize: 'text-2xl font-semibold',
2932
lineHeight: 'leading-snug',
3033
marginTop: 'mt-[0.75em]',
34+
anchorButtonMarginTop: 'mt-[0.9em]',
3135
};
3236
case 'heading-3':
3337
return {
3438
textSize: 'text-xl font-semibold',
3539
lineHeight: 'leading-snug',
3640
marginTop: 'mt-[0.5em]',
41+
anchorButtonMarginTop: 'mt-[0.65em]',
3742
};
3843
case 'divider':
3944
return {

0 commit comments

Comments
 (0)