From 47207f9ae69e24ecc20c6803842e4297a35b4b4e Mon Sep 17 00:00:00 2001 From: J Alfonso Martinez Date: Fri, 5 Sep 2025 10:35:41 -0600 Subject: [PATCH 1/6] refactor(types): improve type definitions for DualScrollSync components --- .../DualScrollSync/DualScrollSync.types.ts | 50 ++++++++++--------- .../DualScrollSyncContentSection.types.ts | 9 ++-- .../DualScrollSyncNav.types.ts | 3 +- .../DualScrollSyncNavItem.types.ts | 5 +- 4 files changed, 36 insertions(+), 31 deletions(-) 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.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 +>; From 8761dc9b956c42426798428d8ebca80b17d53a51 Mon Sep 17 00:00:00 2001 From: J Alfonso Martinez Date: Fri, 5 Sep 2025 10:36:01 -0600 Subject: [PATCH 2/6] fix(context): add maxVisibleItems to DualScrollSyncContextProps interface --- lib/contexts/DualScrollSyncContext.ts | 1 + 1 file changed, 1 insertion(+) 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; } From 444198904af448287c9e7019c801494c845232dd Mon Sep 17 00:00:00 2001 From: J Alfonso Martinez Date: Fri, 5 Sep 2025 10:36:17 -0600 Subject: [PATCH 3/6] fix(DualScrollSync): ensure maxVisibleItems is correctly passed from context --- lib/components/DualScrollSync/DualScrollSync.tsx | 5 ++++- .../DualScrollSync/DualScrollSyncNav/DualScrollSyncNav.tsx | 3 +-- 2 files changed, 5 insertions(+), 3 deletions(-) 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/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); From 8b2588cf8124fa9a61c95ac79814a5b0b482a63d Mon Sep 17 00:00:00 2001 From: J Alfonso Martinez Date: Fri, 5 Sep 2025 10:36:31 -0600 Subject: [PATCH 4/6] test(DualScrollSyncNav): update tests to use context mock for maxVisibleItems --- .../DualScrollSyncNav/DualScrollSyncNav.test.tsx | 11 +++++++++-- lib/hooks/useDualScrollSyncContext.test.tsx | 4 +++- lib/setupTests.ts | 11 +++++++++-- 3 files changed, 21 insertions(+), 5 deletions(-) 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/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); From ba7d08f69e6df6f477f801e2ffadfc3aa3fb36a7 Mon Sep 17 00:00:00 2001 From: J Alfonso Martinez Date: Fri, 5 Sep 2025 10:36:40 -0600 Subject: [PATCH 5/6] docs(README): update usage patterns and examples for DualScrollSync component --- README.md | 49 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 8f4fb28..a295fc0 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,13 @@ 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 +93,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 From 1b596e499e9bf7db7d2699a38b1dc181d7d19e33 Mon Sep 17 00:00:00 2001 From: J Alfonso Martinez Date: Fri, 5 Sep 2025 11:02:43 -0600 Subject: [PATCH 6/6] chore(README): correct JSX syntax for DualScrollSync component example --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index a295fc0..bbed09e 100644 --- a/README.md +++ b/README.md @@ -81,9 +81,7 @@ const items = [ { sectionKey: 'details', label: 'Details', children:
...
} ]; -return ( - ; -); +return ; ``` ### Declarative