diff --git a/README.md b/README.md
index 8f4fb28..bbed09e 100644
--- a/README.md
+++ b/README.md
@@ -15,6 +15,7 @@ A lightweight React library to synchronize a vertical navigation menu with scrol
- [Installation](#-installation)
- [Styles](#-styles)
- [Quick Start](#-quick-start)
+- [Usage Patterns](#-usage-patterns)
- [Props Overview](#-props-overview)
- [Customization](#-customization)
- [Docs](#-documentation)
@@ -76,11 +77,11 @@ Define your sections in an array and let the component generate both nav items a
```tsx
const items = [
- { sectionKey: 'intro', label: 'Introduction', children:
...
},
- { sectionKey: 'details', label: 'Details', children: ...
}
+ { sectionKey: 'intro', label: 'Introduction', children: ...
},
+ { sectionKey: 'details', label: 'Details', children: ...
}
];
- console.log(k)} />;
+return ;
```
### Declarative
@@ -90,24 +91,36 @@ Write the structure directly in JSX using `DualScrollSync.NavItem` and `DualScro
✅ Best for static pages where you want **full control**.
```tsx
-
-
- Section A
- Section B
-
-
-
- ...
- ...
-
-
+return (
+
+
+ Introduction
+ Details
+
+
+
+
+ Introduction
+ ...
+
+
+ Details
+ ...
+
+
+
+);
```
## ⚖️ When to use
-✅ Long scrollable pages with sticky nav
-✅ Catalog filters, docs sidebars, multi-section layouts
-❌ Very short pages (anchors may suffice)
+✅ Long scrollable pages with sticky nav
+
+✅ Catalog filters, docs sidebars, multi-section layouts
+
+❌ Very short content (no scrolling needed)
+
+❌ Complex nested navs (not supported)
## 📘 Documentation
diff --git a/lib/components/DualScrollSync/DualScrollSync.tsx b/lib/components/DualScrollSync/DualScrollSync.tsx
index c0fd19a..2ab80c2 100644
--- a/lib/components/DualScrollSync/DualScrollSync.tsx
+++ b/lib/components/DualScrollSync/DualScrollSync.tsx
@@ -20,9 +20,10 @@ export const DualScrollSyncBase: FC = ({
id,
items,
onItemClick,
+ maxVisibleItems = 6,
style = {}
}) => {
- const baseId = id ?? 'dual-scroll-sync';
+ const baseId = id || 'dual-scroll-sync';
const navId = `${baseId}-nav`;
const contentId = `${baseId}-content`;
@@ -39,6 +40,7 @@ export const DualScrollSyncBase: FC = ({
sectionRefs,
navItemRefs,
navRef,
+ maxVisibleItems,
onMenuItemSelect,
onItemClick
}),
@@ -51,6 +53,7 @@ export const DualScrollSyncBase: FC = ({
sectionRefs,
navItemRefs,
navRef,
+ maxVisibleItems,
onMenuItemSelect,
onItemClick
]
diff --git a/lib/components/DualScrollSync/DualScrollSync.types.ts b/lib/components/DualScrollSync/DualScrollSync.types.ts
index ee76bb7..fb10359 100644
--- a/lib/components/DualScrollSync/DualScrollSync.types.ts
+++ b/lib/components/DualScrollSync/DualScrollSync.types.ts
@@ -18,30 +18,32 @@ export type DualScrollSyncOptions = {
export type DualScrollSyncItem = PropsWithChildren;
-export type DualScrollSyncProps = PropsWithChildren & {
- /**
- * Unique identifier for the DualScrollSync component. (Optional)
- * @default 'dual-scroll-sync'
- */
- id?: string;
- /**
- * Array of `DualScrollSyncItem` objects.
- * If provided, the component will auto-generate the navigation menu and content sections and ignore any children passed directly to it. (Optional)
- * @default []
- */
- items?: DualScrollSyncItem[];
- /**
- * Maximum visible items in the navigation menu. If the number of items exceeds this value, scrolling is activated. (Optional)
- * @default 6
- */
- maxVisibleItems?: number;
- /**
- * Callback function triggered when active section changes.
- * @param activeKey - The key of the active section.
- * @default () => {}
- */
- onItemClick?: (activeKey: string) => void;
-};
+export type DualScrollSyncProps = PropsWithChildren<
+ DualScrollSyncStyleProps & {
+ /**
+ * Unique identifier for the DualScrollSync component. (Optional)
+ * @default 'dual-scroll-sync'
+ */
+ id?: string;
+ /**
+ * Array of `DualScrollSyncItem` objects.
+ * If provided, the component will auto-generate the navigation menu and content sections and ignore any children passed directly to it. (Optional)
+ * @default []
+ */
+ items?: DualScrollSyncItem[];
+ /**
+ * Maximum visible items in the navigation menu. If the number of items exceeds this value, scrolling is activated. (Optional)
+ * @default 6
+ */
+ maxVisibleItems?: number;
+ /**
+ * Callback function triggered when active section changes.
+ * @param activeKey - The key of the active section.
+ * @default () => {}
+ */
+ onItemClick?: (activeKey: string) => void;
+ }
+>;
export type DualScrollSyncType = FC & {
Nav: FC;
diff --git a/lib/components/DualScrollSync/DualScrollSyncContentSection/DualScrollSyncContentSection.types.ts b/lib/components/DualScrollSync/DualScrollSyncContentSection/DualScrollSyncContentSection.types.ts
index 504272e..6453887 100644
--- a/lib/components/DualScrollSync/DualScrollSyncContentSection/DualScrollSyncContentSection.types.ts
+++ b/lib/components/DualScrollSync/DualScrollSyncContentSection/DualScrollSyncContentSection.types.ts
@@ -1,4 +1,7 @@
-import type { DualScrollSyncItem, DualScrollSyncStyleProps } from '../DualScrollSync.types';
+import type { PropsWithChildren } from 'react';
-export type DualScrollSyncContentSectionProps = Omit &
- DualScrollSyncStyleProps;
+import type { DualScrollSyncOptions, DualScrollSyncStyleProps } from '../DualScrollSync.types';
+
+export type DualScrollSyncContentSectionProps = PropsWithChildren<
+ DualScrollSyncStyleProps & Pick
+>;
diff --git a/lib/components/DualScrollSync/DualScrollSyncNav/DualScrollSyncNav.test.tsx b/lib/components/DualScrollSync/DualScrollSyncNav/DualScrollSyncNav.test.tsx
index 81adff5..df924b3 100644
--- a/lib/components/DualScrollSync/DualScrollSyncNav/DualScrollSyncNav.test.tsx
+++ b/lib/components/DualScrollSync/DualScrollSyncNav/DualScrollSyncNav.test.tsx
@@ -1,6 +1,8 @@
import { render } from '@testing-library/react';
import { vi } from 'vitest';
+import { mockScrollSyncContextProps, spyUseDualScrollSyncContext } from '@/setupTests';
+
import { DualScrollSyncNav } from './DualScrollSyncNav';
describe('DualScrollSyncNav', () => {
@@ -19,8 +21,13 @@ describe('DualScrollSyncNav', () => {
});
it('should apply maxVisibleItems correctly', () => {
+ spyUseDualScrollSyncContext.mockReturnValueOnce({
+ ...mockScrollSyncContextProps,
+ maxVisibleItems: 3
+ });
+
const { getByTestId } = render(
-
+
Item 1
Item 2
Item 3
@@ -34,7 +41,7 @@ describe('DualScrollSyncNav', () => {
it('should limit visible items to the number of children if fewer than maxVisibleItems', () => {
const { getByTestId } = render(
-
+
Item 1
Item 2
Item 3
diff --git a/lib/components/DualScrollSync/DualScrollSyncNav/DualScrollSyncNav.tsx b/lib/components/DualScrollSync/DualScrollSyncNav/DualScrollSyncNav.tsx
index d12ebc5..13eedd3 100644
--- a/lib/components/DualScrollSync/DualScrollSyncNav/DualScrollSyncNav.tsx
+++ b/lib/components/DualScrollSync/DualScrollSyncNav/DualScrollSyncNav.tsx
@@ -10,10 +10,9 @@ import type { DualScrollSyncNavProps } from './DualScrollSyncNav.types';
export const DualScrollSyncNav: FC = ({
children,
className,
- maxVisibleItems = 6,
style = {}
}) => {
- const { navId, navRef } = useDualScrollSyncContext();
+ const { navId, navRef, maxVisibleItems } = useDualScrollSyncContext();
const navItemCount = Children.count(children);
const visibleItemsCount = Math.min(maxVisibleItems, navItemCount);
diff --git a/lib/components/DualScrollSync/DualScrollSyncNav/DualScrollSyncNav.types.ts b/lib/components/DualScrollSync/DualScrollSyncNav/DualScrollSyncNav.types.ts
index 4db6508..a76c583 100644
--- a/lib/components/DualScrollSync/DualScrollSyncNav/DualScrollSyncNav.types.ts
+++ b/lib/components/DualScrollSync/DualScrollSyncNav/DualScrollSyncNav.types.ts
@@ -2,5 +2,4 @@ import type { PropsWithChildren } from 'react';
import type { DualScrollSyncStyleProps } from '../DualScrollSync.types';
-export type DualScrollSyncNavProps = DualScrollSyncStyleProps &
- PropsWithChildren<{ maxVisibleItems?: number }>;
+export type DualScrollSyncNavProps = PropsWithChildren;
diff --git a/lib/components/DualScrollSync/DualScrollSyncNavItem/DualScrollSyncNavItem.types.ts b/lib/components/DualScrollSync/DualScrollSyncNavItem/DualScrollSyncNavItem.types.ts
index 0ddd17e..885a110 100644
--- a/lib/components/DualScrollSync/DualScrollSyncNavItem/DualScrollSyncNavItem.types.ts
+++ b/lib/components/DualScrollSync/DualScrollSyncNavItem/DualScrollSyncNavItem.types.ts
@@ -2,5 +2,6 @@ import type { PropsWithChildren } from 'react';
import type { DualScrollSyncOptions, DualScrollSyncStyleProps } from '../DualScrollSync.types';
-export type DualScrollSyncNavItemProps = Pick &
- PropsWithChildren;
+export type DualScrollSyncNavItemProps = PropsWithChildren<
+ DualScrollSyncStyleProps & Pick
+>;
diff --git a/lib/contexts/DualScrollSyncContext.ts b/lib/contexts/DualScrollSyncContext.ts
index c9fffc3..b639c95 100644
--- a/lib/contexts/DualScrollSyncContext.ts
+++ b/lib/contexts/DualScrollSyncContext.ts
@@ -6,6 +6,7 @@ export interface DualScrollSyncContextProps extends UseScrollSyncObserverReturn
baseId: string;
navId: string;
contentId: string;
+ maxVisibleItems: number;
onItemClick?: (activeKey: string) => void;
}
diff --git a/lib/hooks/useDualScrollSyncContext.test.tsx b/lib/hooks/useDualScrollSyncContext.test.tsx
index bb085ab..d46de1c 100644
--- a/lib/hooks/useDualScrollSyncContext.test.tsx
+++ b/lib/hooks/useDualScrollSyncContext.test.tsx
@@ -15,7 +15,9 @@ const mockValue = {
navItemRefs: { current: {} },
navRef: { current: null },
sectionRefs: { current: {} },
- onMenuItemSelect: vi.fn()
+ onMenuItemSelect: vi.fn(),
+ onItemClick: vi.fn(),
+ maxVisibleItems: 6
};
const Wrapper: FC = ({ children }) => {
diff --git a/lib/setupTests.ts b/lib/setupTests.ts
index 3c886af..f8f23ee 100644
--- a/lib/setupTests.ts
+++ b/lib/setupTests.ts
@@ -5,6 +5,8 @@ import { afterEach, vi } from 'vitest';
import * as hooks from '@/hooks';
+import type { DualScrollSyncContextProps } from './contexts';
+
beforeEach(() => {
expect.hasAssertions();
});
@@ -57,7 +59,7 @@ vi.stubGlobal('IntersectionObserver', IntersectionObserverMock);
export const mockOnItemClick = vi.fn();
export const mockOnMenuItemSelect = vi.fn();
-vi.spyOn(hooks, 'useDualScrollSyncContext').mockReturnValue({
+export const mockScrollSyncContextProps: DualScrollSyncContextProps = {
contentId: 'test-content-id',
navId: 'test-nav-id',
baseId: 'test',
@@ -66,6 +68,11 @@ vi.spyOn(hooks, 'useDualScrollSyncContext').mockReturnValue({
navItemRefs: { current: {} },
navRef: { current: null },
sectionRefs: { current: {} },
+ maxVisibleItems: 6,
onMenuItemSelect: mockOnMenuItemSelect,
onItemClick: mockOnItemClick
-});
+};
+
+export const spyUseDualScrollSyncContext = vi.spyOn(hooks, 'useDualScrollSyncContext');
+
+spyUseDualScrollSyncContext.mockReturnValue(mockScrollSyncContextProps);