UNPKG

9.59 kBJavaScriptView Raw
1/**
2* This source code is quoted from rc-tabs.
3* homepage: https://github.com/react-component/tabs
4*/
5import React from 'react';
6import PropTypes from 'prop-types';
7import classnames from 'classnames';
8import debounce from 'lodash/debounce';
9import ResizeObserver from 'resize-observer-polyfill';
10import { setTransform, isTransformSupported } from './utils';
11
12export default class ScrollableTabBarNode extends React.Component {
13 constructor(props) {
14 super(props);
15 this.offset = 0;
16
17 this.state = {
18 next: false,
19 prev: false,
20 };
21 }
22
23 componentDidMount() {
24 this.componentDidUpdate();
25 this.debouncedResize = debounce(() => {
26 this.setNextPrev();
27 this.scrollToActiveTab();
28 }, 200);
29 this.resizeObserver = new ResizeObserver(this.debouncedResize);
30 this.resizeObserver.observe(this.props.getRef('container'));
31 }
32
33 componentDidUpdate(prevProps) {
34 const props = this.props;
35 if (prevProps && prevProps.tabBarPosition !== props.tabBarPosition) {
36 this.setOffset(0);
37 return;
38 }
39 const nextPrev = this.setNextPrev();
40 // wait next, prev show hide
41 /* eslint react/no-did-update-set-state:0 */
42 if (this.isNextPrevShown(this.state) !== this.isNextPrevShown(nextPrev)) {
43 this.setState({}, this.scrollToActiveTab);
44 } else if (!prevProps || props.activeKey !== prevProps.activeKey) {
45 // can not use props.activeKey
46 this.scrollToActiveTab();
47 }
48 }
49
50 componentWillUnmount() {
51 if (this.resizeObserver) {
52 this.resizeObserver.disconnect();
53 }
54 if (this.debouncedResize && this.debouncedResize.cancel) {
55 this.debouncedResize.cancel();
56 }
57 }
58
59 setNextPrev() {
60 const navNode = this.props.getRef('nav');
61 const navTabsContainer = this.props.getRef('navTabsContainer');
62 const navNodeWH = this.getScrollWH(navTabsContainer || navNode);
63 // Add 1px to fix `offsetWidth` with decimal in Chrome not correct handle
64 // https://github.com/ant-design/ant-design/issues/13423
65 const containerWH = this.getOffsetWH(this.props.getRef('container')) + 1;
66 const navWrapNodeWH = this.getOffsetWH(this.props.getRef('navWrap'));
67 let { offset } = this;
68 const minOffset = containerWH - navNodeWH;
69 let { next, prev } = this.state;
70 if (minOffset >= 0) {
71 next = false;
72 this.setOffset(0, false);
73 offset = 0;
74 } else if (minOffset < offset) {
75 next = true;
76 } else {
77 next = false;
78 // Fix https://github.com/ant-design/ant-design/issues/8861
79 // Test with container offset which is stable
80 // and set the offset of the nav wrap node
81 const realOffset = navWrapNodeWH - navNodeWH;
82 this.setOffset(realOffset, false);
83 offset = realOffset;
84 }
85
86 if (offset < 0) {
87 prev = true;
88 } else {
89 prev = false;
90 }
91
92 this.setNext(next);
93 this.setPrev(prev);
94 return {
95 next,
96 prev,
97 };
98 }
99
100 getOffsetWH(node) {
101 const tabBarPosition = this.props.tabBarPosition;
102 let prop = 'offsetWidth';
103 if (tabBarPosition === 'left' || tabBarPosition === 'right') {
104 prop = 'offsetHeight';
105 }
106 return node[prop];
107 }
108
109 getScrollWH(node) {
110 const tabBarPosition = this.props.tabBarPosition;
111 let prop = 'scrollWidth';
112 if (tabBarPosition === 'left' || tabBarPosition === 'right') {
113 prop = 'scrollHeight';
114 }
115 return node[prop];
116 }
117
118
119 getOffsetLT(node) {
120 const tabBarPosition = this.props.tabBarPosition;
121 let prop = 'left';
122 if (tabBarPosition === 'left' || tabBarPosition === 'right') {
123 prop = 'top';
124 }
125 return node.getBoundingClientRect()[prop];
126 }
127
128 setOffset(offset, checkNextPrev = true) {
129 let target = Math.min(0, offset);
130 if (this.offset !== target) {
131 this.offset = target;
132 let navOffset = {};
133 const tabBarPosition = this.props.tabBarPosition;
134 const navStyle = this.props.getRef('nav').style;
135 const transformSupported = isTransformSupported(navStyle);
136 if (tabBarPosition === 'left' || tabBarPosition === 'right') {
137 if (transformSupported) {
138 navOffset = {
139 value: `translate3d(0,${target}px,0)`,
140 };
141 } else {
142 navOffset = {
143 name: 'top',
144 value: `${target}px`,
145 };
146 }
147 } else if (transformSupported) {
148 if (this.props.direction === 'rtl') {
149 target = -target;
150 }
151 navOffset = {
152 value: `translate3d(${target}px,0,0)`,
153 };
154 } else {
155 navOffset = {
156 name: 'left',
157 value: `${target}px`,
158 };
159 }
160 if (transformSupported) {
161 setTransform(navStyle, navOffset.value);
162 } else {
163 navStyle[navOffset.name] = navOffset.value;
164 }
165 if (checkNextPrev) {
166 this.setNextPrev();
167 }
168 }
169 }
170
171 setPrev(v) {
172 if (this.state.prev !== v) {
173 this.setState({
174 prev: v,
175 });
176 }
177 }
178
179 setNext(v) {
180 if (this.state.next !== v) {
181 this.setState({
182 next: v,
183 });
184 }
185 }
186
187 isNextPrevShown(state) {
188 if (state) {
189 return state.next || state.prev;
190 }
191 return this.state.next || this.state.prev;
192 }
193
194 prevTransitionEnd = (e) => {
195 if (e.propertyName !== 'opacity') {
196 return;
197 }
198 const container = this.props.getRef('container');
199 this.scrollToActiveTab({
200 target: container,
201 currentTarget: container,
202 });
203 }
204
205 scrollToActiveTab = (e) => {
206 const activeTab = this.props.getRef('activeTab');
207 const navWrap = this.props.getRef('navWrap');
208 if (e && e.target !== e.currentTarget || !activeTab) {
209 return;
210 }
211
212 // when not scrollable or enter scrollable first time, don't emit scrolling
213 const needToSroll = this.isNextPrevShown() && this.lastNextPrevShown;
214 this.lastNextPrevShown = this.isNextPrevShown();
215 if (!needToSroll) {
216 return;
217 }
218
219 const activeTabWH = this.getScrollWH(activeTab);
220 const navWrapNodeWH = this.getOffsetWH(navWrap);
221 let { offset } = this;
222 const wrapOffset = this.getOffsetLT(navWrap);
223 const activeTabOffset = this.getOffsetLT(activeTab);
224 if (wrapOffset > activeTabOffset) {
225 offset += (wrapOffset - activeTabOffset);
226 this.setOffset(offset);
227 } else if ((wrapOffset + navWrapNodeWH) < (activeTabOffset + activeTabWH)) {
228 offset -= (activeTabOffset + activeTabWH) - (wrapOffset + navWrapNodeWH);
229 this.setOffset(offset);
230 }
231 }
232
233 prev = (e) => {
234 this.props.onPrevClick(e);
235 const navWrapNode = this.props.getRef('navWrap');
236 const navWrapNodeWH = this.getOffsetWH(navWrapNode);
237 const { offset } = this;
238 this.setOffset(offset + navWrapNodeWH);
239 }
240
241 next = (e) => {
242 this.props.onNextClick(e);
243 const navWrapNode = this.props.getRef('navWrap');
244 const navWrapNodeWH = this.getOffsetWH(navWrapNode);
245 const { offset } = this;
246 this.setOffset(offset - navWrapNodeWH);
247 }
248
249 render() {
250 const { next, prev } = this.state;
251 const { clsPrefix,
252 scrollAnimated,
253 navWrapper,
254 prevIcon,
255 nextIcon,
256 } = this.props;
257 const showNextPrev = prev || next;
258
259 const prevButton = (
260 <span
261 onClick={prev ? this.prev : null}
262 unselectable="unselectable"
263 className={classnames({
264 [`${clsPrefix}-tab-prev`]: 1,
265 [`${clsPrefix}-tab-btn-disabled`]: !prev,
266 [`${clsPrefix}-tab-arrow-show`]: showNextPrev,
267 })}
268 onTransitionEnd={this.prevTransitionEnd}
269 >
270 {prevIcon || <span className={`${clsPrefix}-tab-prev-icon`} />}
271 </span>
272 );
273
274 const nextButton = (
275 <span
276 onClick={next ? this.next : null}
277 unselectable="unselectable"
278 className={classnames({
279 [`${clsPrefix}-tab-next`]: 1,
280 [`${clsPrefix}-tab-btn-disabled`]: !next,
281 [`${clsPrefix}-tab-arrow-show`]: showNextPrev,
282 })}
283 >
284 {nextIcon || <span className={`${clsPrefix}-tab-next-icon`} />}
285 </span>
286 );
287
288 const navClassName = `${clsPrefix}-nav`;
289 const navClasses = classnames({
290 [navClassName]: true,
291 [
292 scrollAnimated ?
293 `${navClassName}-animated` :
294 `${navClassName}-no-animated`
295 ]: true,
296 });
297
298 return (
299 <div
300 className={classnames({
301 [`${clsPrefix}-nav-container`]: 1,
302 [`${clsPrefix}-nav-container-scrolling`]: showNextPrev,
303 })}
304 key="container"
305 ref={this.props.saveRef('container')}
306 >
307 {prevButton}
308 {nextButton}
309 <div className={`${clsPrefix}-nav-wrap`} ref={this.props.saveRef('navWrap')}>
310 <div className={`${clsPrefix}-nav-scroll`}>
311 <div className={navClasses} ref={this.props.saveRef('nav')}>
312 {navWrapper(this.props.children)}
313 </div>
314 </div>
315 </div>
316 </div>
317 );
318 }
319}
320
321ScrollableTabBarNode.propTypes = {
322 activeKey: PropTypes.string,
323 getRef: PropTypes.func.isRequired,
324 saveRef: PropTypes.func.isRequired,
325 tabBarPosition: PropTypes.oneOf(['left', 'right', 'top', 'bottom']),
326 clsPrefix: PropTypes.string,
327 scrollAnimated: PropTypes.bool,
328 onPrevClick: PropTypes.func,
329 onNextClick: PropTypes.func,
330 navWrapper: PropTypes.func,
331 children: PropTypes.node,
332 prevIcon: PropTypes.node,
333 nextIcon: PropTypes.node,
334 direction: PropTypes.node,
335};
336
337ScrollableTabBarNode.defaultProps = {
338 tabBarPosition: 'left',
339 clsPrefix: '',
340 scrollAnimated: true,
341 onPrevClick: () => { },
342 onNextClick: () => { },
343 navWrapper: (ele) => ele,
344};