mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-23 02:53:29 +00:00
Enhance StructurePreview navigation by adding sectionId handling and updating state management
This commit is contained in:
@@ -157,6 +157,11 @@ export function SiteSectionTabs(props: {
|
||||
? structureItem.url
|
||||
: undefined
|
||||
}
|
||||
sectionId={
|
||||
structureItem.object === 'site-section'
|
||||
? structureItem.id
|
||||
: undefined
|
||||
}
|
||||
isActive={isActive}
|
||||
title={title}
|
||||
icon={icon ? (icon as IconName) : undefined}
|
||||
@@ -233,10 +238,10 @@ function useNavigationMenuViewportOffset(args: {
|
||||
* A tab representing a section or section group
|
||||
*/
|
||||
const SectionTab = React.forwardRef(function SectionTab(
|
||||
props: { isActive: boolean; title: string; icon?: IconName; url?: string },
|
||||
props: { isActive: boolean; title: string; icon?: IconName; url?: string; sectionId?: string },
|
||||
ref: React.Ref<HTMLAnchorElement>
|
||||
) {
|
||||
const { isActive, title, icon, url, ...rest } = props;
|
||||
const { isActive, title, icon, url, sectionId, ...rest } = props;
|
||||
const isGroup = url === undefined;
|
||||
return (
|
||||
<Button
|
||||
@@ -248,6 +253,7 @@ const SectionTab = React.forwardRef(function SectionTab(
|
||||
label={title}
|
||||
trailing={isGroup ? <ToggleChevron /> : null}
|
||||
active={isActive}
|
||||
data-gb-site-section-id={sectionId}
|
||||
className={tcls(
|
||||
'group/dropdown relative my-1.5 overflow-visible',
|
||||
isActive
|
||||
@@ -356,6 +362,7 @@ function SectionGroupTile(props: {
|
||||
<li className="group/section-tile flex w-full min-w-0 shrink-0 grow md:max-w-[var(--site-section-column-width)]">
|
||||
<Link
|
||||
href={url}
|
||||
data-gb-site-section-id={child.id}
|
||||
className={tcls(
|
||||
'grow circular-corners:rounded-2xl rounded-corners:rounded-lg px-2.5 py-1.5 transition-colors',
|
||||
isActive
|
||||
|
||||
@@ -49,11 +49,16 @@ import { SearchHeaderInput } from '../Search';
|
||||
import { CONTAINER_STYLE } from '../layout';
|
||||
import { Button, type ButtonProps, ToggleChevron } from '../primitives';
|
||||
import { DropdownMenu, DropdownMenuItem } from '../primitives/DropdownMenu';
|
||||
import { SOCIAL_PLATFORM_ICONS, isStructurePreviewMessage } from './state';
|
||||
import {
|
||||
SOCIAL_PLATFORM_ICONS,
|
||||
isStructurePreviewMessage,
|
||||
selectStructurePreviewSection,
|
||||
} from './state';
|
||||
import type {
|
||||
PreviewContentLink,
|
||||
PreviewDropdownSpace,
|
||||
PreviewHeaderLink,
|
||||
StructurePreviewNavigationMessage,
|
||||
StructurePreviewSnapshot,
|
||||
} from './types';
|
||||
|
||||
@@ -79,19 +84,47 @@ export function StructurePreview(props: { initialSnapshot: StructurePreviewSnaps
|
||||
return () => window.removeEventListener('message', handleMessage);
|
||||
}, []);
|
||||
|
||||
const postNavigationChange = (sectionId: string) => {
|
||||
const message: StructurePreviewNavigationMessage = {
|
||||
type: 'gitbook.structure.navigate',
|
||||
payload: { sectionId },
|
||||
};
|
||||
|
||||
window.parent.postMessage(message, window.location.origin);
|
||||
};
|
||||
|
||||
const preventNavigation = (event: React.MouseEvent<HTMLElement>) => {
|
||||
const target = event.target;
|
||||
if (!(target instanceof Element)) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
const anchor = target.closest('a');
|
||||
if (!anchor) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
return anchor;
|
||||
};
|
||||
|
||||
const fakeSectionNavigation = (event: React.MouseEvent<HTMLElement>) => {
|
||||
const anchor = preventNavigation(event);
|
||||
const sectionId = anchor?.getAttribute('data-gb-site-section-id');
|
||||
if (!sectionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSnapshot((currentSnapshot) => {
|
||||
const nextSnapshot = selectStructurePreviewSection(currentSnapshot, sectionId);
|
||||
if (nextSnapshot !== currentSnapshot) {
|
||||
postNavigationChange(sectionId);
|
||||
}
|
||||
|
||||
return nextSnapshot;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -99,7 +132,7 @@ export function StructurePreview(props: { initialSnapshot: StructurePreviewSnaps
|
||||
data-gb-structure-preview
|
||||
data-viewport-mode="desktop"
|
||||
className="site-background min-h-screen min-w-[1024px] overflow-hidden"
|
||||
onClickCapture={preventNavigation}
|
||||
onClickCapture={fakeSectionNavigation}
|
||||
onAuxClickCapture={preventNavigation}
|
||||
>
|
||||
<StructurePreviewHeader snapshot={snapshot} />
|
||||
|
||||
@@ -3,10 +3,10 @@ import { describe, expect, it } from 'bun:test';
|
||||
import { getStructurePreviewSnapshot } from '@/app/sites/dynamic/[mode]/[siteURL]/[siteData]/~gitbook/structure/snapshot';
|
||||
import { languages } from '@/intl/translations';
|
||||
import type { GitBookSiteContext } from '@/lib/context';
|
||||
import { defaultCustomization } from '@/lib/utils';
|
||||
import { defaultCustomization, findSectionInGroup } from '@/lib/utils';
|
||||
import { SiteSocialAccountPlatform, TranslationLanguage } from '@gitbook/api';
|
||||
|
||||
import { isStructurePreviewMessage } from './state';
|
||||
import { isStructurePreviewMessage, selectStructurePreviewSection } from './state';
|
||||
|
||||
function createContext(overrides: Partial<GitBookSiteContext> = {}): GitBookSiteContext {
|
||||
const siteSpace = {
|
||||
@@ -58,6 +58,12 @@ describe('structure preview state', () => {
|
||||
expect('visibleSiteSpaces' in snapshot).toBe(false);
|
||||
expect(isStructurePreviewMessage({ type: 'gitbook.structure.update' })).toBe(false);
|
||||
expect(isStructurePreviewMessage({ type: 'other', payload: snapshot })).toBe(false);
|
||||
expect(
|
||||
isStructurePreviewMessage({
|
||||
type: 'gitbook.structure.navigate',
|
||||
payload: { sectionId: 'section-1' },
|
||||
})
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('stores pre-encoded section structures with inert URLs', () => {
|
||||
@@ -176,4 +182,81 @@ describe('structure preview state', () => {
|
||||
{ platform: SiteSocialAccountPlatform.Github, handle: 'gitbook' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('selects a top-level section in the local snapshot', () => {
|
||||
const snapshot = createSnapshotWithSections();
|
||||
const nextSnapshot = selectStructurePreviewSection(snapshot, 'reference');
|
||||
|
||||
expect(nextSnapshot).not.toBe(snapshot);
|
||||
expect(nextSnapshot.sections?.current.id).toBe('reference');
|
||||
expect(nextSnapshot.sections?.current.title).toBe('Reference');
|
||||
});
|
||||
|
||||
it('selects a nested section in the local snapshot', () => {
|
||||
const snapshot = createSnapshotWithSections();
|
||||
const nextSnapshot = selectStructurePreviewSection(snapshot, 'api');
|
||||
const currentSection = nextSnapshot.sections?.current;
|
||||
const group = nextSnapshot.sections?.list[1];
|
||||
|
||||
expect(currentSection?.id).toBe('api');
|
||||
expect(group?.object).toBe('site-section-group');
|
||||
if (!currentSection || group?.object !== 'site-section-group') {
|
||||
throw new Error('Expected a nested section inside a section group');
|
||||
}
|
||||
|
||||
expect(findSectionInGroup(group, currentSection.id)?.id).toBe('api');
|
||||
});
|
||||
|
||||
it('keeps the current snapshot when selecting an unknown section', () => {
|
||||
const snapshot = createSnapshotWithSections();
|
||||
const nextSnapshot = selectStructurePreviewSection(snapshot, 'missing');
|
||||
|
||||
expect(nextSnapshot).toBe(snapshot);
|
||||
expect(nextSnapshot.sections?.current.id).toBe('intro');
|
||||
});
|
||||
|
||||
it('keeps snapshots without sections unchanged', () => {
|
||||
const snapshot = getStructurePreviewSnapshot(createContext());
|
||||
const nextSnapshot = selectStructurePreviewSection(snapshot, 'reference');
|
||||
|
||||
expect(nextSnapshot).toBe(snapshot);
|
||||
expect(nextSnapshot.sections).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
function createSnapshotWithSections() {
|
||||
const intro = createSection('intro', 'Intro');
|
||||
const reference = createSection('reference', 'Reference');
|
||||
const api = createSection('api', 'API');
|
||||
|
||||
return getStructurePreviewSnapshot(
|
||||
createContext({
|
||||
sections: {
|
||||
list: [
|
||||
intro,
|
||||
{
|
||||
object: 'site-section-group',
|
||||
id: 'developers',
|
||||
title: 'Developers',
|
||||
children: [api],
|
||||
},
|
||||
reference,
|
||||
],
|
||||
current: intro,
|
||||
},
|
||||
} as unknown as Partial<GitBookSiteContext>)
|
||||
);
|
||||
}
|
||||
|
||||
function createSection(id: string, title: string) {
|
||||
return {
|
||||
object: 'site-section',
|
||||
id,
|
||||
title,
|
||||
description: '',
|
||||
path: id,
|
||||
default: false,
|
||||
siteSpaces: [],
|
||||
urls: {},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { SiteSocialAccountPlatform } from '@gitbook/api';
|
||||
import type { IconName } from '@gitbook/icons';
|
||||
|
||||
import type { ClientSiteSection, ClientSiteSectionGroup } from '../SiteSections';
|
||||
import type { StructurePreviewMessage, StructurePreviewSnapshot } from './types';
|
||||
|
||||
export function isStructurePreviewMessage(value: unknown): value is StructurePreviewMessage {
|
||||
@@ -39,6 +40,50 @@ export function isStructurePreviewSnapshot(value: unknown): value is StructurePr
|
||||
);
|
||||
}
|
||||
|
||||
export function selectStructurePreviewSection(
|
||||
snapshot: StructurePreviewSnapshot,
|
||||
sectionId: string
|
||||
): StructurePreviewSnapshot {
|
||||
const sections = snapshot.sections;
|
||||
if (!sections || sections.current.id === sectionId) {
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
const selectedSection = findPreviewSection(sections.list, sectionId);
|
||||
if (!selectedSection) {
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
return {
|
||||
...snapshot,
|
||||
sections: {
|
||||
...sections,
|
||||
current: selectedSection,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function findPreviewSection(
|
||||
items: (ClientSiteSection | ClientSiteSectionGroup)[],
|
||||
sectionId: string
|
||||
): ClientSiteSection | null {
|
||||
for (const item of items) {
|
||||
if (item.object === 'site-section') {
|
||||
if (item.id === sectionId) {
|
||||
return item;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const childSection = findPreviewSection(item.children, sectionId);
|
||||
if (childSection) {
|
||||
return childSection;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export const SOCIAL_PLATFORM_ICONS: Partial<Record<SiteSocialAccountPlatform, IconName>> = {
|
||||
twitter: 'x-twitter',
|
||||
instagram: 'instagram',
|
||||
|
||||
@@ -80,3 +80,10 @@ export type StructurePreviewMessage = {
|
||||
type: 'gitbook.structure.update';
|
||||
payload: StructurePreviewSnapshot;
|
||||
};
|
||||
|
||||
export type StructurePreviewNavigationMessage = {
|
||||
type: 'gitbook.structure.navigate';
|
||||
payload: {
|
||||
sectionId: string;
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user