Skip to content

Commit b39ea57

Browse files
author
berdysheva
committed
feat(Navigation): use react-hook
1 parent 82ff477 commit b39ea57

File tree

9 files changed

+149
-165
lines changed

9 files changed

+149
-165
lines changed

src/components/RouterLink/RouterLink.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {WithChildren} from '../../models';
44

55
export interface RouterLinkProps {
66
href: string;
7+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
78
[key: string]: any;
89
}
910

src/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,6 @@ export {default as OverflowScroller} from './OverflowScroller/OverflowScroller';
3333
export {default as Author} from './Author/Author';
3434
export {default as RouterLink} from './RouterLink/RouterLink';
3535
export {default as HTML} from './HTML/HTML';
36+
export {default as Header} from './navigation/components/Header/Header';
3637

3738
export type {RouterLinkProps} from './RouterLink/RouterLink';
Lines changed: 115 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -1,120 +1,63 @@
11
import _ from 'lodash';
22
import block from 'bem-cn-lite';
3-
import React, {RefObject, createRef, Fragment, MouseEventHandler} from 'react';
4-
import OverflowScroller from '../../../OverflowScroller/OverflowScroller';
5-
// import {LocationContext} from '../../context/locationContext';
3+
import React, {
4+
Fragment,
5+
MouseEventHandler,
6+
useState,
7+
useEffect,
8+
useCallback,
9+
useContext,
10+
useRef,
11+
} from 'react';
612

13+
import OverflowScroller from '../../../OverflowScroller/OverflowScroller';
714
import {
815
NavigationDropdownItem,
916
NavigationItem as NavigationItemModel,
1017
NavigationItemType,
1118
} from '../../../../models/navigation';
1219
import NavigationPopup from '../NavigationPopup/NavigationPopup';
1320
import NavigationItem from '../NavigationItem/NavigationItem';
21+
import {LocationContext} from '../../../../context/locationContext';
1422

1523
import './Navigation.scss';
1624

1725
const b = block('navigation');
1826

19-
export interface NavigationOwnProps {
27+
export interface NavigationProps {
2028
links: NavigationItemModel[];
2129
activeItemIndex: number;
2230
onActiveItemChange: (index: number) => void;
2331
className?: string;
2432
highlightActiveItem?: boolean;
2533
}
2634

27-
// interface WithRouterProps {
28-
// router: NextRouter;
29-
// }
30-
31-
// export type NavigationProps = NavigationOwnProps & WithRouterProps;
32-
export type NavigationProps = NavigationOwnProps;
33-
34-
interface NavigationState {
35-
itemPositions: number[];
36-
}
37-
38-
class Navigation extends React.Component<NavigationProps, NavigationState> {
39-
itemRefs: RefObject<HTMLLIElement>[] = [];
40-
state = {
41-
itemPositions: [],
42-
};
43-
lastLeftScroll = 0;
44-
45-
componentDidMount() {
46-
this.calculateItemPositions();
47-
this.lastLeftScroll = window.pageXOffset;
48-
49-
window.addEventListener('resize', this.calculateItemPositions);
50-
window.addEventListener('scroll', this.calculateOnScroll);
51-
}
52-
53-
// componentDidUpdate(prevProps: NavigationProps) {
54-
// //use locationContext
55-
// const {router} = this.props;
56-
//
57-
// if (router.asPath !== prevProps.router.asPath) {
58-
// this.hidePopup();
59-
// }
60-
// }
61-
62-
componentWillUnmount() {
63-
window.removeEventListener('resize', this.calculateItemPositions);
64-
window.removeEventListener('scroll', this.calculateOnScroll);
65-
}
66-
67-
render() {
68-
const {className} = this.props;
69-
70-
return (
71-
<OverflowScroller
72-
className={b(null, className)}
73-
onScrollStart={this.hidePopup}
74-
onScrollEnd={this.calculateItemPositions}
75-
>
76-
{this.renderContent()}
77-
</OverflowScroller>
78-
);
79-
}
80-
81-
renderContent() {
82-
const {links, activeItemIndex, highlightActiveItem} = this.props;
83-
const {itemPositions} = this.state;
84-
85-
return (
86-
<nav>
87-
<ul className={b('links')}>
88-
{links.map((link, index) => {
89-
const isActive = index === activeItemIndex;
90-
const onClick = this.getItemClickHandler(index);
91-
92-
if (!this.itemRefs[index]) {
93-
this.itemRefs[index] = createRef();
94-
}
95-
96-
return (
97-
<li ref={this.itemRefs[index]} key={index} className={b('links-item')}>
98-
{link.type === NavigationItemType.Dropdown ? (
99-
this.renderNavDropdown(
100-
link,
101-
onClick,
102-
isActive,
103-
itemPositions[index],
104-
)
105-
) : (
106-
<NavigationItem data={link} onClick={onClick} />
107-
)}
108-
{highlightActiveItem && isActive && this.renderSlider()}
109-
</li>
110-
);
111-
})}
112-
</ul>
113-
</nav>
114-
);
115-
}
116-
117-
private renderNavDropdown = (
35+
const Navigation: React.FC<NavigationProps> = ({
36+
className,
37+
onActiveItemChange,
38+
links,
39+
activeItemIndex,
40+
highlightActiveItem,
41+
}) => {
42+
const {asPath, pathname} = useContext(LocationContext);
43+
const itemRefs = useRef<(HTMLLIElement | null)[]>([]);
44+
const [itemPositions, setItemPosition] = useState<number[]>([]);
45+
46+
const [lastLeftScroll, setLastLeftScroll] = useState(0);
47+
48+
const hidePopup = useCallback(() => {
49+
onActiveItemChange(-1);
50+
}, [onActiveItemChange]);
51+
52+
const getItemClickHandler = useCallback<(index: number) => MouseEventHandler>(
53+
(index) => (e) => {
54+
e.stopPropagation();
55+
onActiveItemChange(index === activeItemIndex ? -1 : index);
56+
},
57+
[activeItemIndex, onActiveItemChange],
58+
);
59+
60+
const renderNavDropdown = (
11861
data: NavigationDropdownItem,
11962
onClick: MouseEventHandler,
12063
isActive: boolean,
@@ -133,7 +76,7 @@ class Navigation extends React.Component<NavigationProps, NavigationState> {
13376
{isActive && (
13477
<NavigationPopup
13578
left={position}
136-
onClose={this.hidePopup}
79+
onClose={hidePopup}
13780
items={items}
13881
{...popupProps}
13982
/>
@@ -142,44 +85,84 @@ class Navigation extends React.Component<NavigationProps, NavigationState> {
14285
);
14386
};
14487

145-
private renderSlider() {
146-
return (
147-
<div className={b('slider-container')}>
148-
<div className={b('slider')} />
149-
</div>
150-
);
151-
}
152-
153-
private getItemClickHandler: (index: number) => MouseEventHandler = (index) => (e) => {
154-
e.stopPropagation();
155-
const {activeItemIndex, onActiveItemChange} = this.props;
156-
onActiveItemChange(index === activeItemIndex ? -1 : index);
157-
};
158-
159-
private hidePopup = () => {
160-
this.props.onActiveItemChange(-1);
161-
};
162-
163-
// eslint-disable-next-line @typescript-eslint/member-ordering
164-
private calculateItemPositions = _.debounce(() => {
165-
if (this.itemRefs.length) {
166-
const itemPositions = this.itemRefs.map(
167-
(itemRef) => (itemRef.current && itemRef.current.getBoundingClientRect().left) || 0,
88+
const slider = (
89+
<div className={b('slider-container')}>
90+
<div className={b('slider')} />
91+
</div>
92+
);
93+
94+
const content = (
95+
<nav>
96+
<ul className={b('links')}>
97+
{links.map((link, index) => {
98+
const isActive = index === activeItemIndex;
99+
const onClick = getItemClickHandler(index);
100+
101+
return (
102+
<li
103+
ref={(el) => itemRefs.current.push(el)}
104+
key={index}
105+
className={b('links-item')}
106+
>
107+
{link.type === NavigationItemType.Dropdown ? (
108+
renderNavDropdown(link, onClick, isActive, itemPositions[index])
109+
) : (
110+
<NavigationItem data={link} onClick={onClick} />
111+
)}
112+
{highlightActiveItem && isActive && slider}
113+
</li>
114+
);
115+
})}
116+
</ul>
117+
</nav>
118+
);
119+
120+
const calculateItemPositions = useCallback(() => {
121+
if (itemRefs.current.length) {
122+
const currentItemPositions = itemRefs.current.map(
123+
(itemRef) => (itemRef && itemRef.getBoundingClientRect().left) || 0,
168124
);
169-
this.setState({itemPositions});
170-
}
171-
}, 100);
172-
173-
// eslint-disable-next-line @typescript-eslint/member-ordering
174-
private calculateOnScroll = _.debounce(() => {
175-
const curLeftScroll = window.pageXOffset;
176125

177-
if (curLeftScroll !== this.lastLeftScroll) {
178-
this.lastLeftScroll = curLeftScroll;
179-
this.calculateItemPositions();
126+
setItemPosition(currentItemPositions);
180127
}
181-
}, 100);
182-
}
128+
}, []);
129+
130+
useEffect(() => {
131+
const debouncedCalculateItemPositions = _.debounce(calculateItemPositions, 100);
132+
const calculateOnScroll = _.debounce(() => {
133+
const curLeftScroll = window.pageXOffset;
134+
135+
if (curLeftScroll !== lastLeftScroll) {
136+
setLastLeftScroll(window.pageXOffset);
137+
calculateItemPositions();
138+
}
139+
}, 100);
140+
141+
calculateItemPositions();
142+
setLastLeftScroll(window.pageXOffset);
143+
144+
window.addEventListener('resize', debouncedCalculateItemPositions);
145+
window.addEventListener('scroll', calculateOnScroll);
146+
147+
return () => {
148+
window.removeEventListener(`resize`, calculateItemPositions);
149+
window.removeEventListener('scroll', calculateOnScroll);
150+
};
151+
}, [calculateItemPositions, itemRefs, lastLeftScroll]);
152+
153+
useEffect(() => {
154+
hidePopup();
155+
}, [hidePopup, asPath, pathname]);
156+
157+
return (
158+
<OverflowScroller
159+
className={b(null, className)}
160+
onScrollStart={hidePopup}
161+
onScrollEnd={calculateItemPositions}
162+
>
163+
{content}
164+
</OverflowScroller>
165+
);
166+
};
183167

184-
// export default withRouter(Navigation);
185168
export default Navigation;

src/components/navigation/components/NavigationPopup/NavigationPopup.tsx

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,19 @@ export default class NavigationPopup extends React.Component<
3131
calculatedLeft: this.props.left,
3232
};
3333

34+
private calculateLeft = _.debounce(() => {
35+
const {left} = this.props;
36+
37+
if (this.ref && this.ref.current && left) {
38+
const right = left + this.ref.current.offsetWidth;
39+
const docWidth = document.body.clientWidth;
40+
const calculatedLeft = right > docWidth ? left - (right - docWidth) : left;
41+
this.setState({calculatedLeft});
42+
} else {
43+
this.setState({calculatedLeft: left});
44+
}
45+
}, 100);
46+
3447
componentDidMount() {
3548
this.calculateLeft();
3649
window.addEventListener('resize', this.calculateLeft);
@@ -74,18 +87,4 @@ export default class NavigationPopup extends React.Component<
7487
</Fragment>
7588
);
7689
}
77-
78-
// eslint-disable-next-line @typescript-eslint/member-ordering
79-
private calculateLeft = _.debounce(() => {
80-
const {left} = this.props;
81-
82-
if (this.ref && this.ref.current && left) {
83-
const right = left + this.ref.current.offsetWidth;
84-
const docWidth = document.body.clientWidth;
85-
const calculatedLeft = right > docWidth ? left - (right - docWidth) : left;
86-
this.setState({calculatedLeft});
87-
} else {
88-
this.setState({calculatedLeft: left});
89-
}
90-
}, 100);
9190
}

src/containers/PageConstructor/PageConstructor.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,12 @@ import {ConstructorRow} from './components/ConstructorRow';
2727
import {ConstructorFootnotes} from './components/ConstructorFootnotes';
2828
import {ConstructorHeader} from './components/ConstructorItem';
2929
import {ConstructorBlocks} from './components/ConstructorBlocks';
30+
import Header from '../../components/navigation/components/Header/Header';
31+
import {NavigationData} from '../../models/navigation';
3032

3133
import '@doc-tools/transform/dist/js/yfm';
3234

3335
import './PageConstructor.scss';
34-
import Header from '../../components/navigation/components/Header/Header';
3536

3637
const b = cnBlock('page-constructor');
3738

@@ -42,6 +43,7 @@ export interface PageConstructorProps {
4243
shouldRenderBlock?: ShouldRenderBlock;
4344
custom?: CustomConfig;
4445
renderMenu?: () => React.ReactNode;
46+
navigationData?: NavigationData;
4547
}
4648

4749
export const Constructor = (props: PageConstructorProps) => {
@@ -63,9 +65,10 @@ export const Constructor = (props: PageConstructorProps) => {
6365

6466
const {themeValue: theme} = useContext(ThemeValueContext);
6567
const {
66-
content: {blocks = [], background = {}, footnotes = [], navigationData} = {},
68+
content: {blocks = [], background = {}, footnotes = []} = {},
6769
renderMenu,
6870
shouldRenderBlock,
71+
navigationData,
6972
} = props;
7073

7174
const hasFootnotes = footnotes.length > 0;
@@ -74,8 +77,6 @@ export const Constructor = (props: PageConstructorProps) => {
7477
const restBlocks = blocks?.filter((block) => !isHeaderBlock(block));
7578
const themedBackground = getThemedValue(background, theme);
7679

77-
console.log(navigationData);
78-
7980
return (
8081
<InnerContext.Provider value={context}>
8182
<div className={b()}>

src/containers/PageConstructor/__stories__/PageConstructor.stories.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ const NavigationTemplate: Story<TemplateProps> = (args) => (
3939
<PageConstructor
4040
content={{
4141
blocks: args.items,
42-
navigationData: data.navigation as NavigationData,
4342
}}
43+
navigationData={data.navigation as NavigationData}
4444
/>
4545
);
4646

0 commit comments

Comments
 (0)