Skip to content

Commit

Permalink
Improve animation of section tabs (#2553)
Browse files Browse the repository at this point in the history
  • Loading branch information
BrettJephson authored Oct 25, 2024
1 parent ff50ac2 commit 5d72b35
Show file tree
Hide file tree
Showing 5 changed files with 60 additions and 56 deletions.
5 changes: 5 additions & 0 deletions .changeset/khaki-badgers-knock.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'gitbook': patch
---

Smoother tab transition for sections
4 changes: 2 additions & 2 deletions packages/gitbook/src/app/(site)/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export interface PageIdParams {
pageId: string;
}

type SectionsList = { list: SiteSection[]; section: SiteSection };
export type SectionsList = { list: SiteSection[]; section: SiteSection; index: number };

/**
* Fetch all the data needed to render the content layout.
Expand Down Expand Up @@ -70,7 +70,7 @@ export async function fetchContentData() {
function parseSiteSectionsList(siteSectionId: string, sections: SiteSection[]) {
const section = sections.find((section) => section.id === siteSectionId);
assert(sectionIsDefined(section), 'A section must be defined when there are multiple sections');
return { list: sections, section } satisfies SectionsList;
return { list: sections, section, index: sections.indexOf(section) } satisfies SectionsList;
}

function sectionIsDefined(section?: SiteSection): section is NonNullable<SiteSection> {
Expand Down
13 changes: 4 additions & 9 deletions packages/gitbook/src/components/Header/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
import {
CustomizationSettings,
Site,
SiteCustomizationSettings,
SiteSection,
Space,
} from '@gitbook/api';
import { CustomizationSettings, Site, SiteCustomizationSettings, Space } from '@gitbook/api';
import { CustomizationHeaderPreset } from '@gitbook/api';
import { Suspense } from 'react';

import type { SectionsList } from '@/app/(site)/fetch';
import { CONTAINER_STYLE, HEADER_HEIGHT_DESKTOP } from '@/components/layout';
import { t, getSpaceLanguage } from '@/intl/server';
import { ContentRefContext } from '@/lib/references';
Expand All @@ -27,7 +22,7 @@ export function Header(props: {
space: Space;
site: Site | null;
spaces: Space[];
sections: { list: SiteSection[]; section: SiteSection } | null;
sections: SectionsList | null;
context: ContentRefContext;
customization: CustomizationSettings | SiteCustomizationSettings;
withTopHeader?: boolean;
Expand Down Expand Up @@ -153,7 +148,7 @@ export function Header(props: {
)}
>
<div className={tcls(CONTAINER_STYLE)}>
<SiteSectionTabs sections={sections.list} section={sections.section} />
<SiteSectionTabs {...sections} />
</div>
</div>
) : null}
Expand Down
90 changes: 47 additions & 43 deletions packages/gitbook/src/components/SiteSectionTabs/SiteSectionTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,14 @@ import { tcls } from '@/lib/tailwind';
import { Button, Link } from '../primitives';

/**
* A set of tabs representing site sections for multi-section sites
* A set of navigational tabs representing site sections for multi-section sites
*/
export function SiteSectionTabs(props: { sections: SiteSection[]; section: SiteSection }) {
const { sections, section: currentSection } = props;
export function SiteSectionTabs(props: {
list: SiteSection[];
section: SiteSection;
index: number;
}) {
const { list: sections, section: currentSection, index: currentIndex } = props;

const tabs = sections.map((section) => ({
id: section.id,
Expand All @@ -22,34 +26,33 @@ export function SiteSectionTabs(props: { sections: SiteSection[]; section: SiteS
const currentTabRef = React.useRef<HTMLAnchorElement>(null);
const navRef = React.useRef<HTMLDivElement>(null);

const [currentIndex, setCurrentIndex] = React.useState(
sections.findIndex((section) => section.id === currentSection?.id),
);
const [tabDimensions, setTabDimensions] = React.useState<{
left: number;
width: number;
} | null>(null);

React.useEffect(() => {
const updateTabDimensions = React.useCallback(() => {
if (currentTabRef.current && navRef.current) {
const rect = currentTabRef.current.getBoundingClientRect();
const navRect = navRef.current.getBoundingClientRect();
setTabDimensions({ left: rect.left - navRect.left, width: rect.width });
}
}, [currentIndex]);
}, []);

React.useEffect(() => {
updateTabDimensions();
}, [currentIndex, updateTabDimensions]);

React.useLayoutEffect(() => {
function onResize() {
if (currentTabRef.current && navRef.current) {
const rect = currentTabRef.current.getBoundingClientRect();
const navRect = navRef.current.getBoundingClientRect();
setTabDimensions({ left: rect.left - navRect.left, width: rect.width });
}
}
window.addEventListener('resize', onResize);
() => window.removeEventListener('resize', onResize);
}, []);
window.addEventListener('load', updateTabDimensions);
window.addEventListener('resize', updateTabDimensions);
() => {
window.removeEventListener('resize', updateTabDimensions);
window.removeEventListener('load', updateTabDimensions);
};
}, [updateTabDimensions]);

const opacity = Boolean(tabDimensions) ? 1 : 0.0;
const scale = (tabDimensions?.width ?? 0) * 0.01;
const startPos = `${tabDimensions?.left ?? 0}px`;

Expand All @@ -62,6 +65,7 @@ export function SiteSectionTabs(props: { sections: SiteSection[]; section: SiteS
className="flex flex-nowrap items-center max-w-screen mb-px"
style={
{
'--tab-opacity': `${opacity}`,
'--tab-scale': `${scale}`,
'--tab-start': `${startPos}`,
} as React.CSSProperties
Expand All @@ -80,9 +84,12 @@ export function SiteSectionTabs(props: { sections: SiteSection[]; section: SiteS
'after:absolute',
'after:-bottom-px',
'after:left-0',
'after:opacity-[--tab-opacity]',
'after:scale-x-[--tab-scale]',
'after:transition-transform',
'after:[transition:_opacity_150ms_25ms,transform_150ms]',
'after:motion-reduce:transition-none',
'after:translate-x-[var(--tab-start)]',
'after:will-change-transform',
'after:h-0.5',
'after:w-[100px]',
'after:bg-primary',
Expand All @@ -97,7 +104,6 @@ export function SiteSectionTabs(props: { sections: SiteSection[]; section: SiteS
label={tab.label}
href={tab.path}
ref={currentIndex === index ? currentTabRef : null}
onClick={() => setCurrentIndex(index)}
/>
))}
</div>
Expand All @@ -109,29 +115,27 @@ export function SiteSectionTabs(props: { sections: SiteSection[]; section: SiteS
/**
* The tab item - a link to a site section
*/
const Tab = React.forwardRef<
HTMLSpanElement,
{ active: boolean; href: string; label: string; onClick?: () => void }
>(function Tab(props, ref) {
const { active, href, label, onClick } = props;
return (
<Link
className={tcls(
'px-3 py-1 my-2 rounded straight-corners:rounded-none transition-colors',
active && 'text-primary dark:text-primary-400',
!active &&
'text-dark/8 hover:bg-dark/1 hover:text-dark/9 dark:text-light/8 dark:hover:bg-light/2 dark:hover:text-light/9',
)}
role="tab"
href={href}
onClick={onClick}
>
<span ref={ref} className={tcls('inline-flex w-full truncate')}>
{label}
</span>
</Link>
);
});
const Tab = React.forwardRef<HTMLSpanElement, { active: boolean; href: string; label: string }>(
function Tab(props, ref) {
const { active, href, label } = props;
return (
<Link
className={tcls(
'px-3 py-1 my-2 rounded straight-corners:rounded-none transition-colors',
active && 'text-primary dark:text-primary-400',
!active &&
'text-dark/8 hover:bg-dark/1 hover:text-dark/9 dark:text-light/8 dark:hover:bg-light/2 dark:hover:text-light/9',
)}
role="tab"
href={href}
>
<span ref={ref} className={tcls('inline-flex w-full truncate')}>
{label}
</span>
</Link>
);
},
);

/**
* Dropdown trigger for when there are too many sections to show them all
Expand Down
4 changes: 2 additions & 2 deletions packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ import {
RevisionPageGroup,
Site,
SiteCustomizationSettings,
SiteSection,
Space,
} from '@gitbook/api';
import React from 'react';

import { SectionsList } from '@/app/(site)/fetch';
import { Footer } from '@/components/Footer';
import { CompactHeader, Header } from '@/components/Header';
import { CONTAINER_STYLE } from '@/components/layout';
Expand All @@ -31,7 +31,7 @@ export function SpaceLayout(props: {
contentTarget: ContentTarget;
space: Space;
site: Site | null;
sections: { list: SiteSection[]; section: SiteSection } | null;
sections: SectionsList | null;
spaces: Space[];
customization: CustomizationSettings | SiteCustomizationSettings;
pages: Revision['pages'];
Expand Down

0 comments on commit 5d72b35

Please sign in to comment.