Skip to content

Commit 4c13328

Browse files
authored
fix(DualScrollSync): add maxVisibleItems support with docs and tests (#27)
refactor(types): improve type definitions for DualScrollSync components fix(context): add maxVisibleItems to DualScrollSyncContextProps interface fix(DualScrollSync): ensure maxVisibleItems is correctly passed from context test(DualScrollSyncNav): update tests to use context mock for maxVisibleItems docs(README): update usage patterns and examples for DualScrollSync component
1 parent 1f95206 commit 4c13328

File tree

11 files changed

+93
-56
lines changed

11 files changed

+93
-56
lines changed

README.md

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ A lightweight React library to synchronize a vertical navigation menu with scrol
1515
- [Installation](#-installation)
1616
- [Styles](#-styles)
1717
- [Quick Start](#-quick-start)
18+
- [Usage Patterns](#-usage-patterns)
1819
- [Props Overview](#-props-overview)
1920
- [Customization](#-customization)
2021
- [Docs](#-documentation)
@@ -76,11 +77,11 @@ Define your sections in an array and let the component generate both nav items a
7677

7778
```tsx
7879
const items = [
79-
{ sectionKey: 'intro', label: 'Introduction', children: <p>...</p> },
80-
{ sectionKey: 'details', label: 'Details', children: <p>...</p> }
80+
{ sectionKey: 'intro', label: 'Introduction', children: <div>...</div> },
81+
{ sectionKey: 'details', label: 'Details', children: <div>...</div> }
8182
];
8283

83-
<DualScrollSync items={items} onItemClick={(k) => console.log(k)} />;
84+
return <DualScrollSync items={items} onItemClick={handleClick} />;
8485
```
8586

8687
### Declarative
@@ -90,24 +91,36 @@ Write the structure directly in JSX using `DualScrollSync.NavItem` and `DualScro
9091
✅ Best for static pages where you want **full control**.
9192

9293
```tsx
93-
<DualScrollSync>
94-
<DualScrollSync.Nav>
95-
<DualScrollSync.NavItem sectionKey="a">Section A</DualScrollSync.NavItem>
96-
<DualScrollSync.NavItem sectionKey="b">Section B</DualScrollSync.NavItem>
97-
</DualScrollSync.Nav>
98-
99-
<DualScrollSync.Content>
100-
<DualScrollSync.ContentSection sectionKey="a">...</DualScrollSync.ContentSection>
101-
<DualScrollSync.ContentSection sectionKey="b">...</DualScrollSync.ContentSection>
102-
</DualScrollSync.Content>
103-
</DualScrollSync>
94+
return (
95+
<DualScrollSync onItemClick={handleClick}>
96+
<DualScrollSync.Nav>
97+
<DualScrollSync.NavItem sectionKey="intro">Introduction</DualScrollSync.NavItem>
98+
<DualScrollSync.NavItem sectionKey="details">Details</DualScrollSync.NavItem>
99+
</DualScrollSync.Nav>
100+
101+
<DualScrollSync.Content>
102+
<DualScrollSync.ContentSection sectionKey="intro">
103+
<DualScrollSync.Label>Introduction</DualScrollSync.Label>
104+
<div>...</div>
105+
</DualScrollSync.ContentSection>
106+
<DualScrollSync.ContentSection sectionKey="details">
107+
<DualScrollSync.Label>Details</DualScrollSync.Label>
108+
<div>...</div>
109+
</DualScrollSync.ContentSection>
110+
</DualScrollSync.Content>
111+
</DualScrollSync>
112+
);
104113
```
105114

106115
## ⚖️ When to use
107116

108-
✅ Long scrollable pages with sticky nav
109-
✅ Catalog filters, docs sidebars, multi-section layouts
110-
❌ Very short pages (anchors may suffice)
117+
✅ Long scrollable pages with sticky nav
118+
119+
✅ Catalog filters, docs sidebars, multi-section layouts
120+
121+
❌ Very short content (no scrolling needed)
122+
123+
❌ Complex nested navs (not supported)
111124

112125
## 📘 Documentation
113126

lib/components/DualScrollSync/DualScrollSync.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,10 @@ export const DualScrollSyncBase: FC<DualScrollSyncProps> = ({
2020
id,
2121
items,
2222
onItemClick,
23+
maxVisibleItems = 6,
2324
style = {}
2425
}) => {
25-
const baseId = id ?? 'dual-scroll-sync';
26+
const baseId = id || 'dual-scroll-sync';
2627
const navId = `${baseId}-nav`;
2728
const contentId = `${baseId}-content`;
2829

@@ -39,6 +40,7 @@ export const DualScrollSyncBase: FC<DualScrollSyncProps> = ({
3940
sectionRefs,
4041
navItemRefs,
4142
navRef,
43+
maxVisibleItems,
4244
onMenuItemSelect,
4345
onItemClick
4446
}),
@@ -51,6 +53,7 @@ export const DualScrollSyncBase: FC<DualScrollSyncProps> = ({
5153
sectionRefs,
5254
navItemRefs,
5355
navRef,
56+
maxVisibleItems,
5457
onMenuItemSelect,
5558
onItemClick
5659
]

lib/components/DualScrollSync/DualScrollSync.types.ts

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -18,30 +18,32 @@ export type DualScrollSyncOptions = {
1818

1919
export type DualScrollSyncItem = PropsWithChildren<DualScrollSyncOptions>;
2020

21-
export type DualScrollSyncProps = PropsWithChildren<DualScrollSyncStyleProps> & {
22-
/**
23-
* Unique identifier for the DualScrollSync component. (Optional)
24-
* @default 'dual-scroll-sync'
25-
*/
26-
id?: string;
27-
/**
28-
* Array of `DualScrollSyncItem` objects.
29-
* If provided, the component will auto-generate the navigation menu and content sections and ignore any children passed directly to it. (Optional)
30-
* @default []
31-
*/
32-
items?: DualScrollSyncItem[];
33-
/**
34-
* Maximum visible items in the navigation menu. If the number of items exceeds this value, scrolling is activated. (Optional)
35-
* @default 6
36-
*/
37-
maxVisibleItems?: number;
38-
/**
39-
* Callback function triggered when active section changes.
40-
* @param activeKey - The key of the active section.
41-
* @default () => {}
42-
*/
43-
onItemClick?: (activeKey: string) => void;
44-
};
21+
export type DualScrollSyncProps = PropsWithChildren<
22+
DualScrollSyncStyleProps & {
23+
/**
24+
* Unique identifier for the DualScrollSync component. (Optional)
25+
* @default 'dual-scroll-sync'
26+
*/
27+
id?: string;
28+
/**
29+
* Array of `DualScrollSyncItem` objects.
30+
* If provided, the component will auto-generate the navigation menu and content sections and ignore any children passed directly to it. (Optional)
31+
* @default []
32+
*/
33+
items?: DualScrollSyncItem[];
34+
/**
35+
* Maximum visible items in the navigation menu. If the number of items exceeds this value, scrolling is activated. (Optional)
36+
* @default 6
37+
*/
38+
maxVisibleItems?: number;
39+
/**
40+
* Callback function triggered when active section changes.
41+
* @param activeKey - The key of the active section.
42+
* @default () => {}
43+
*/
44+
onItemClick?: (activeKey: string) => void;
45+
}
46+
>;
4547

4648
export type DualScrollSyncType = FC<DualScrollSyncProps> & {
4749
Nav: FC<DualScrollSyncNavProps>;
Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
import type { DualScrollSyncItem, DualScrollSyncStyleProps } from '../DualScrollSync.types';
1+
import type { PropsWithChildren } from 'react';
22

3-
export type DualScrollSyncContentSectionProps = Omit<DualScrollSyncItem, 'label'> &
4-
DualScrollSyncStyleProps;
3+
import type { DualScrollSyncOptions, DualScrollSyncStyleProps } from '../DualScrollSync.types';
4+
5+
export type DualScrollSyncContentSectionProps = PropsWithChildren<
6+
DualScrollSyncStyleProps & Pick<DualScrollSyncOptions, 'sectionKey'>
7+
>;

lib/components/DualScrollSync/DualScrollSyncNav/DualScrollSyncNav.test.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { render } from '@testing-library/react';
22
import { vi } from 'vitest';
33

4+
import { mockScrollSyncContextProps, spyUseDualScrollSyncContext } from '@/setupTests';
5+
46
import { DualScrollSyncNav } from './DualScrollSyncNav';
57

68
describe('DualScrollSyncNav', () => {
@@ -19,8 +21,13 @@ describe('DualScrollSyncNav', () => {
1921
});
2022

2123
it('should apply maxVisibleItems correctly', () => {
24+
spyUseDualScrollSyncContext.mockReturnValueOnce({
25+
...mockScrollSyncContextProps,
26+
maxVisibleItems: 3
27+
});
28+
2229
const { getByTestId } = render(
23-
<DualScrollSyncNav maxVisibleItems={3}>
30+
<DualScrollSyncNav>
2431
<div>Item 1</div>
2532
<div>Item 2</div>
2633
<div>Item 3</div>
@@ -34,7 +41,7 @@ describe('DualScrollSyncNav', () => {
3441

3542
it('should limit visible items to the number of children if fewer than maxVisibleItems', () => {
3643
const { getByTestId } = render(
37-
<DualScrollSyncNav maxVisibleItems={5}>
44+
<DualScrollSyncNav>
3845
<div>Item 1</div>
3946
<div>Item 2</div>
4047
<div>Item 3</div>

lib/components/DualScrollSync/DualScrollSyncNav/DualScrollSyncNav.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,9 @@ import type { DualScrollSyncNavProps } from './DualScrollSyncNav.types';
1010
export const DualScrollSyncNav: FC<DualScrollSyncNavProps> = ({
1111
children,
1212
className,
13-
maxVisibleItems = 6,
1413
style = {}
1514
}) => {
16-
const { navId, navRef } = useDualScrollSyncContext();
15+
const { navId, navRef, maxVisibleItems } = useDualScrollSyncContext();
1716
const navItemCount = Children.count(children);
1817
const visibleItemsCount = Math.min(maxVisibleItems, navItemCount);
1918

lib/components/DualScrollSync/DualScrollSyncNav/DualScrollSyncNav.types.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,4 @@ import type { PropsWithChildren } from 'react';
22

33
import type { DualScrollSyncStyleProps } from '../DualScrollSync.types';
44

5-
export type DualScrollSyncNavProps = DualScrollSyncStyleProps &
6-
PropsWithChildren<{ maxVisibleItems?: number }>;
5+
export type DualScrollSyncNavProps = PropsWithChildren<DualScrollSyncStyleProps>;

lib/components/DualScrollSync/DualScrollSyncNavItem/DualScrollSyncNavItem.types.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@ import type { PropsWithChildren } from 'react';
22

33
import type { DualScrollSyncOptions, DualScrollSyncStyleProps } from '../DualScrollSync.types';
44

5-
export type DualScrollSyncNavItemProps = Pick<DualScrollSyncOptions, 'sectionKey'> &
6-
PropsWithChildren<DualScrollSyncStyleProps>;
5+
export type DualScrollSyncNavItemProps = PropsWithChildren<
6+
DualScrollSyncStyleProps & Pick<DualScrollSyncOptions, 'sectionKey'>
7+
>;

lib/contexts/DualScrollSyncContext.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export interface DualScrollSyncContextProps extends UseScrollSyncObserverReturn
66
baseId: string;
77
navId: string;
88
contentId: string;
9+
maxVisibleItems: number;
910
onItemClick?: (activeKey: string) => void;
1011
}
1112

lib/hooks/useDualScrollSyncContext.test.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ const mockValue = {
1515
navItemRefs: { current: {} },
1616
navRef: { current: null },
1717
sectionRefs: { current: {} },
18-
onMenuItemSelect: vi.fn()
18+
onMenuItemSelect: vi.fn(),
19+
onItemClick: vi.fn(),
20+
maxVisibleItems: 6
1921
};
2022

2123
const Wrapper: FC<PropsWithChildren> = ({ children }) => {

0 commit comments

Comments
 (0)