Break ScrollableList component into parts (#38059)
This commit is contained in:
1 parent
3fbb7424fa
commit
a70079968c
11 files changed
+219
-93
No files matched your search
@@ -0,0 +1,85 @@
|
||||
import type { ComponentPropsWithoutRef } from 'react';
|
||||
import { Children, forwardRef } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { LoadingIndicator } from '../loading_indicator';
|
||||
|
||||
export const Scrollable = forwardRef<
|
||||
HTMLDivElement,
|
||||
ComponentPropsWithoutRef<'div'> & {
|
||||
flex?: boolean;
|
||||
fullscreen?: boolean;
|
||||
}
|
||||
>(({ flex = true, fullscreen, className, children, ...otherProps }, ref) => {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'scrollable',
|
||||
{ 'scrollable--flex': flex, fullscreen },
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...otherProps}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
Scrollable.displayName = 'Scrollable';
|
||||
|
||||
export const ItemList = forwardRef<
|
||||
HTMLDivElement,
|
||||
ComponentPropsWithoutRef<'div'> & {
|
||||
isLoading?: boolean;
|
||||
emptyMessage?: React.ReactNode;
|
||||
}
|
||||
>(({ isLoading, emptyMessage, className, children, ...otherProps }, ref) => {
|
||||
if (Children.count(children) === 0 && emptyMessage) {
|
||||
return <div className='empty-column-indicator'>{emptyMessage}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
role='feed'
|
||||
className={classNames('item-list', className)}
|
||||
ref={ref}
|
||||
{...otherProps}
|
||||
>
|
||||
{!isLoading && children}
|
||||
</div>
|
||||
{isLoading && (
|
||||
<div className='scrollable__append'>
|
||||
<LoadingIndicator />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
ItemList.displayName = 'ItemList';
|
||||
|
||||
export const Article = forwardRef<
|
||||
HTMLElement,
|
||||
ComponentPropsWithoutRef<'article'> & {
|
||||
focusable?: boolean;
|
||||
'data-id'?: string;
|
||||
'aria-posinset': number;
|
||||
'aria-setsize': number;
|
||||
}
|
||||
>(({ focusable, className, children, ...otherProps }, ref) => {
|
||||
return (
|
||||
<article
|
||||
ref={ref}
|
||||
className={classNames(className, { focusable })}
|
||||
tabIndex={-1}
|
||||
{...otherProps}
|
||||
>
|
||||
{children}
|
||||
</article>
|
||||
);
|
||||
});
|
||||
|
||||
Article.displayName = 'Article';
|
||||
@@ -0,0 +1,409 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { Children, cloneElement, PureComponent } from 'react';
|
||||
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { supportsPassiveEvents } from 'detect-passive-events';
|
||||
import { throttle } from 'lodash';
|
||||
|
||||
import { ScrollContainer } from 'mastodon/containers/scroll_container';
|
||||
|
||||
import IntersectionObserverArticleContainer from '../../containers/intersection_observer_article_container';
|
||||
import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../../features/ui/util/fullscreen';
|
||||
import IntersectionObserverWrapper from '../../features/ui/util/intersection_observer_wrapper';
|
||||
|
||||
import { LoadMore } from '../load_more';
|
||||
import { LoadPending } from '../load_pending';
|
||||
import { LoadingIndicator } from '../loading_indicator';
|
||||
import { Scrollable, ItemList } from './components';
|
||||
|
||||
const MOUSE_IDLE_DELAY = 300;
|
||||
|
||||
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import('mastodon/store').RootState} state
|
||||
* @param {*} props
|
||||
*/
|
||||
const mapStateToProps = (state, { scrollKey }) => {
|
||||
return {
|
||||
preventScroll: scrollKey === state.dropdownMenu.scrollKey,
|
||||
};
|
||||
};
|
||||
|
||||
// This component only exists to be able to call useLocation()
|
||||
const IOArticleContainerWrapper = ({id, index, listLength, intersectionObserverWrapper, trackScroll, scrollKey, children}) => {
|
||||
const location = useLocation();
|
||||
|
||||
return (<IntersectionObserverArticleContainer
|
||||
id={id}
|
||||
index={index}
|
||||
listLength={listLength}
|
||||
intersectionObserverWrapper={intersectionObserverWrapper}
|
||||
saveHeightKey={trackScroll ? `${location.key}:${scrollKey}` : null}
|
||||
>
|
||||
{children}
|
||||
</IntersectionObserverArticleContainer>);
|
||||
};
|
||||
|
||||
IOArticleContainerWrapper.propTypes = {
|
||||
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
index: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
listLength: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
scrollKey: PropTypes.string.isRequired,
|
||||
intersectionObserverWrapper: PropTypes.object.isRequired,
|
||||
trackScroll: PropTypes.bool.isRequired,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
class ScrollableList extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
scrollKey: PropTypes.string.isRequired,
|
||||
onLoadMore: PropTypes.func,
|
||||
onLoadPending: PropTypes.func,
|
||||
onScrollToTop: PropTypes.func,
|
||||
onScroll: PropTypes.func,
|
||||
trackScroll: PropTypes.bool,
|
||||
isLoading: PropTypes.bool,
|
||||
showLoading: PropTypes.bool,
|
||||
hasMore: PropTypes.bool,
|
||||
numPending: PropTypes.number,
|
||||
prepend: PropTypes.node,
|
||||
append: PropTypes.node,
|
||||
alwaysPrepend: PropTypes.bool,
|
||||
emptyMessage: PropTypes.node,
|
||||
children: PropTypes.node,
|
||||
bindToDocument: PropTypes.bool,
|
||||
preventScroll: PropTypes.bool,
|
||||
footer: PropTypes.node,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
trackScroll: true,
|
||||
};
|
||||
|
||||
state = {
|
||||
fullscreen: null,
|
||||
cachedMediaWidth: 250, // Default media/card width using default Mastodon theme
|
||||
};
|
||||
|
||||
intersectionObserverWrapper = new IntersectionObserverWrapper();
|
||||
|
||||
handleScroll = throttle(() => {
|
||||
if (this.node) {
|
||||
const scrollTop = this.getScrollTop();
|
||||
const scrollHeight = this.getScrollHeight();
|
||||
const clientHeight = this.getClientHeight();
|
||||
const offset = scrollHeight - scrollTop - clientHeight;
|
||||
|
||||
if (scrollTop > 0 && offset < 400 && this.props.onLoadMore && this.props.hasMore && !this.props.isLoading) {
|
||||
this.props.onLoadMore();
|
||||
}
|
||||
|
||||
if (scrollTop < 100 && this.props.onScrollToTop) {
|
||||
this.props.onScrollToTop();
|
||||
} else if (this.props.onScroll) {
|
||||
this.props.onScroll();
|
||||
}
|
||||
|
||||
if (!this.lastScrollWasSynthetic) {
|
||||
// If the last scroll wasn't caused by setScrollTop(), assume it was
|
||||
// intentional and cancel any pending scroll reset on mouse idle
|
||||
this.scrollToTopOnMouseIdle = false;
|
||||
}
|
||||
this.lastScrollWasSynthetic = false;
|
||||
}
|
||||
}, 150, {
|
||||
trailing: true,
|
||||
});
|
||||
|
||||
mouseIdleTimer = null;
|
||||
mouseMovedRecently = false;
|
||||
lastScrollWasSynthetic = false;
|
||||
scrollToTopOnMouseIdle = false;
|
||||
|
||||
_getScrollingElement = () => {
|
||||
if (this.props.bindToDocument) {
|
||||
return (document.scrollingElement || document.body);
|
||||
} else {
|
||||
return this.node;
|
||||
}
|
||||
};
|
||||
|
||||
setScrollTop = newScrollTop => {
|
||||
if (this.getScrollTop() !== newScrollTop) {
|
||||
this.lastScrollWasSynthetic = true;
|
||||
|
||||
this._getScrollingElement().scrollTop = newScrollTop;
|
||||
}
|
||||
};
|
||||
|
||||
clearMouseIdleTimer = () => {
|
||||
if (this.mouseIdleTimer === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearTimeout(this.mouseIdleTimer);
|
||||
this.mouseIdleTimer = null;
|
||||
};
|
||||
|
||||
handleMouseMove = throttle(() => {
|
||||
// As long as the mouse keeps moving, clear and restart the idle timer.
|
||||
this.clearMouseIdleTimer();
|
||||
this.mouseIdleTimer = setTimeout(this.handleMouseIdle, MOUSE_IDLE_DELAY);
|
||||
|
||||
if (!this.mouseMovedRecently && this.getScrollTop() === 0) {
|
||||
// Only set if we just started moving and are scrolled to the top.
|
||||
this.scrollToTopOnMouseIdle = true;
|
||||
}
|
||||
|
||||
// Save setting this flag for last, so we can do the comparison above.
|
||||
this.mouseMovedRecently = true;
|
||||
}, MOUSE_IDLE_DELAY / 2);
|
||||
|
||||
handleWheel = throttle(() => {
|
||||
this.scrollToTopOnMouseIdle = false;
|
||||
}, 150, {
|
||||
trailing: true,
|
||||
});
|
||||
|
||||
handleMouseIdle = () => {
|
||||
if (this.scrollToTopOnMouseIdle && !this.props.preventScroll) {
|
||||
this.setScrollTop(0);
|
||||
}
|
||||
|
||||
this.mouseMovedRecently = false;
|
||||
this.scrollToTopOnMouseIdle = false;
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
this.attachScrollListener();
|
||||
this.attachIntersectionObserver();
|
||||
|
||||
attachFullscreenListener(this.onFullScreenChange);
|
||||
|
||||
// Handle initial scroll position
|
||||
this.handleScroll();
|
||||
}
|
||||
|
||||
getScrollPosition = () => {
|
||||
if (this.node && (this.getScrollTop() > 0 || this.mouseMovedRecently)) {
|
||||
return { height: this.getScrollHeight(), top: this.getScrollTop() };
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
getScrollTop = () => {
|
||||
return this._getScrollingElement().scrollTop;
|
||||
};
|
||||
|
||||
getScrollHeight = () => {
|
||||
return this._getScrollingElement().scrollHeight;
|
||||
};
|
||||
|
||||
getClientHeight = () => {
|
||||
return this._getScrollingElement().clientHeight;
|
||||
};
|
||||
|
||||
updateScrollBottom = (snapshot) => {
|
||||
const newScrollTop = this.getScrollHeight() - snapshot;
|
||||
|
||||
this.setScrollTop(newScrollTop);
|
||||
};
|
||||
|
||||
getSnapshotBeforeUpdate (prevProps) {
|
||||
const someItemInserted = Children.count(prevProps.children) > 0 &&
|
||||
Children.count(prevProps.children) < Children.count(this.props.children) &&
|
||||
this.getFirstChildKey(prevProps) !== this.getFirstChildKey(this.props);
|
||||
const pendingChanged = (prevProps.numPending > 0) !== (this.props.numPending > 0);
|
||||
|
||||
if (pendingChanged || someItemInserted && (this.getScrollTop() > 0 || this.mouseMovedRecently || this.props.preventScroll)) {
|
||||
return this.getScrollHeight() - this.getScrollTop();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps, prevState, snapshot) {
|
||||
// Reset the scroll position when a new child comes in in order not to
|
||||
// jerk the scrollbar around if you're already scrolled down the page.
|
||||
if (snapshot !== null) {
|
||||
this.setScrollTop(this.getScrollHeight() - snapshot);
|
||||
}
|
||||
}
|
||||
|
||||
cacheMediaWidth = (width) => {
|
||||
if (width && this.state.cachedMediaWidth !== width) {
|
||||
this.setState({ cachedMediaWidth: width });
|
||||
}
|
||||
};
|
||||
|
||||
componentWillUnmount () {
|
||||
this.clearMouseIdleTimer();
|
||||
this.detachScrollListener();
|
||||
this.detachIntersectionObserver();
|
||||
|
||||
detachFullscreenListener(this.onFullScreenChange);
|
||||
}
|
||||
|
||||
onFullScreenChange = () => {
|
||||
this.setState({ fullscreen: isFullscreen() });
|
||||
};
|
||||
|
||||
attachIntersectionObserver () {
|
||||
let nodeOptions = {
|
||||
root: this.node,
|
||||
rootMargin: '300% 0px',
|
||||
};
|
||||
|
||||
this.intersectionObserverWrapper
|
||||
.connect(this.props.bindToDocument ? {} : nodeOptions);
|
||||
}
|
||||
|
||||
detachIntersectionObserver () {
|
||||
this.intersectionObserverWrapper.disconnect();
|
||||
}
|
||||
|
||||
attachScrollListener () {
|
||||
if (this.props.bindToDocument) {
|
||||
document.addEventListener('scroll', this.handleScroll);
|
||||
document.addEventListener('wheel', this.handleWheel, listenerOptions);
|
||||
} else {
|
||||
this.node.addEventListener('scroll', this.handleScroll);
|
||||
this.node.addEventListener('wheel', this.handleWheel, listenerOptions);
|
||||
}
|
||||
}
|
||||
|
||||
detachScrollListener () {
|
||||
if (this.props.bindToDocument) {
|
||||
document.removeEventListener('scroll', this.handleScroll);
|
||||
document.removeEventListener('wheel', this.handleWheel, listenerOptions);
|
||||
} else {
|
||||
this.node.removeEventListener('scroll', this.handleScroll);
|
||||
this.node.removeEventListener('wheel', this.handleWheel, listenerOptions);
|
||||
}
|
||||
}
|
||||
|
||||
getFirstChildKey (props) {
|
||||
const { children } = props;
|
||||
let firstChild = children;
|
||||
|
||||
if (children instanceof ImmutableList) {
|
||||
firstChild = children.get(0);
|
||||
} else if (Array.isArray(children)) {
|
||||
firstChild = children[0];
|
||||
}
|
||||
|
||||
return firstChild && firstChild.key;
|
||||
}
|
||||
|
||||
setRef = (c) => {
|
||||
this.node = c;
|
||||
};
|
||||
|
||||
handleLoadMore = e => {
|
||||
e.preventDefault();
|
||||
this.props.onLoadMore();
|
||||
};
|
||||
|
||||
handleLoadPending = e => {
|
||||
e.preventDefault();
|
||||
this.props.onLoadPending();
|
||||
// Prevent the weird scroll-jumping behavior, as we explicitly don't want to
|
||||
// scroll to top, and we know the scroll height is going to change
|
||||
this.scrollToTopOnMouseIdle = false;
|
||||
this.lastScrollWasSynthetic = false;
|
||||
this.clearMouseIdleTimer();
|
||||
this.mouseIdleTimer = setTimeout(this.handleMouseIdle, MOUSE_IDLE_DELAY);
|
||||
this.mouseMovedRecently = true;
|
||||
};
|
||||
|
||||
render () {
|
||||
const { children, scrollKey, className, trackScroll, showLoading, isLoading, hasMore, numPending, prepend, alwaysPrepend, append, footer, emptyMessage, onLoadMore } = this.props;
|
||||
const { fullscreen } = this.state;
|
||||
const childrenCount = Children.count(children);
|
||||
|
||||
const loadMore = (hasMore && onLoadMore) ? <LoadMore visible={!isLoading} onClick={this.handleLoadMore} /> : null;
|
||||
const loadPending = (numPending > 0) ? <LoadPending count={numPending} onClick={this.handleLoadPending} /> : null;
|
||||
let scrollableArea = null;
|
||||
|
||||
if (showLoading) {
|
||||
scrollableArea = (
|
||||
<Scrollable ref={this.setRef}>
|
||||
{prepend}
|
||||
|
||||
<ItemList isLoading />
|
||||
|
||||
{footer}
|
||||
</Scrollable>
|
||||
);
|
||||
} else if (isLoading || childrenCount > 0 || numPending > 0 || hasMore || !emptyMessage) {
|
||||
scrollableArea = (
|
||||
<Scrollable fullscreen={fullscreen} ref={this.setRef} onMouseMove={this.handleMouseMove}>
|
||||
{prepend}
|
||||
|
||||
<ItemList className={className}>
|
||||
{loadPending}
|
||||
|
||||
{Children.map(this.props.children, (child, index) => (
|
||||
<IOArticleContainerWrapper
|
||||
key={child.key}
|
||||
id={child.key}
|
||||
index={index}
|
||||
listLength={childrenCount}
|
||||
intersectionObserverWrapper={this.intersectionObserverWrapper}
|
||||
trackScroll={trackScroll}
|
||||
scrollKey={scrollKey}
|
||||
>
|
||||
{cloneElement(child, {
|
||||
getScrollPosition: this.getScrollPosition,
|
||||
updateScrollBottom: this.updateScrollBottom,
|
||||
cachedMediaWidth: this.state.cachedMediaWidth,
|
||||
cacheMediaWidth: this.cacheMediaWidth,
|
||||
})}
|
||||
</IOArticleContainerWrapper>
|
||||
))}
|
||||
|
||||
{loadMore}
|
||||
|
||||
{!hasMore && append}
|
||||
</ItemList>
|
||||
|
||||
{footer}
|
||||
</Scrollable>
|
||||
);
|
||||
} else {
|
||||
scrollableArea = (
|
||||
<Scrollable fullscreen={fullscreen} ref={this.setRef}>
|
||||
{alwaysPrepend && prepend}
|
||||
|
||||
<div className='empty-column-indicator'>
|
||||
{emptyMessage}
|
||||
</div>
|
||||
|
||||
{footer}
|
||||
</Scrollable>
|
||||
);
|
||||
}
|
||||
|
||||
if (trackScroll) {
|
||||
return (
|
||||
<ScrollContainer scrollKey={scrollKey} childRef={this.setRef}>
|
||||
{scrollableArea}
|
||||
</ScrollContainer>
|
||||
);
|
||||
} else {
|
||||
return scrollableArea;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, null, null, { forwardRef: true })(ScrollableList);
|
||||
@@ -0,0 +1,131 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { cloneElement, Component } from 'react';
|
||||
|
||||
import getRectFromEntry from '../../features/ui/util/get_rect_from_entry';
|
||||
import scheduleIdleTask from '../../features/ui/util/schedule_idle_task';
|
||||
import { Article } from './components';
|
||||
|
||||
// Diff these props in the "unrendered" state
|
||||
const updateOnPropsForUnrendered = ['id', 'index', 'listLength', 'cachedHeight'];
|
||||
|
||||
export default class IntersectionObserverArticle extends Component {
|
||||
|
||||
static propTypes = {
|
||||
intersectionObserverWrapper: PropTypes.object.isRequired,
|
||||
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
index: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
listLength: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
saveHeightKey: PropTypes.string,
|
||||
cachedHeight: PropTypes.number,
|
||||
onHeightChange: PropTypes.func,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
state = {
|
||||
isHidden: false, // set to true in requestIdleCallback to trigger un-render
|
||||
};
|
||||
|
||||
shouldComponentUpdate (nextProps, nextState) {
|
||||
const isUnrendered = !this.state.isIntersecting && (this.state.isHidden || this.props.cachedHeight);
|
||||
const willBeUnrendered = !nextState.isIntersecting && (nextState.isHidden || nextProps.cachedHeight);
|
||||
if (!!isUnrendered !== !!willBeUnrendered) {
|
||||
// If we're going from rendered to unrendered (or vice versa) then update
|
||||
return true;
|
||||
}
|
||||
// If we are and remain hidden, diff based on props
|
||||
if (isUnrendered) {
|
||||
return !updateOnPropsForUnrendered.every(prop => nextProps[prop] === this.props[prop]);
|
||||
}
|
||||
// Else, assume the children have changed
|
||||
return true;
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
const { intersectionObserverWrapper, id } = this.props;
|
||||
|
||||
intersectionObserverWrapper.observe(
|
||||
id,
|
||||
this.node,
|
||||
this.handleIntersection,
|
||||
);
|
||||
|
||||
this.componentMounted = true;
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
const { intersectionObserverWrapper, id } = this.props;
|
||||
intersectionObserverWrapper.unobserve(id, this.node);
|
||||
|
||||
this.componentMounted = false;
|
||||
}
|
||||
|
||||
handleIntersection = (entry) => {
|
||||
this.entry = entry;
|
||||
|
||||
scheduleIdleTask(this.calculateHeight);
|
||||
this.setState(this.updateStateAfterIntersection);
|
||||
};
|
||||
|
||||
updateStateAfterIntersection = (prevState) => {
|
||||
if (prevState.isIntersecting !== false && !this.entry.isIntersecting) {
|
||||
scheduleIdleTask(this.hideIfNotIntersecting);
|
||||
}
|
||||
return {
|
||||
isIntersecting: this.entry.isIntersecting,
|
||||
isHidden: false,
|
||||
};
|
||||
};
|
||||
|
||||
calculateHeight = () => {
|
||||
const { onHeightChange, saveHeightKey, id } = this.props;
|
||||
// save the height of the fully-rendered element (this is expensive
|
||||
// on Chrome, where we need to fall back to getBoundingClientRect)
|
||||
this.height = getRectFromEntry(this.entry).height;
|
||||
|
||||
if (onHeightChange && saveHeightKey) {
|
||||
onHeightChange(saveHeightKey, id, this.height);
|
||||
}
|
||||
};
|
||||
|
||||
hideIfNotIntersecting = () => {
|
||||
if (!this.componentMounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
// When the browser gets a chance, test if we're still not intersecting,
|
||||
// and if so, set our isHidden to true to trigger an unrender. The point of
|
||||
// this is to save DOM nodes and avoid using up too much memory.
|
||||
// See: https://github.com/mastodon/mastodon/issues/2900
|
||||
this.setState((prevState) => ({ isHidden: !prevState.isIntersecting }));
|
||||
};
|
||||
|
||||
handleRef = (node) => {
|
||||
this.node = node;
|
||||
};
|
||||
|
||||
render () {
|
||||
const { children, id, index, listLength, cachedHeight } = this.props;
|
||||
const { isIntersecting, isHidden } = this.state;
|
||||
|
||||
if (!isIntersecting && (isHidden || cachedHeight)) {
|
||||
return (
|
||||
<Article
|
||||
ref={this.handleRef}
|
||||
aria-posinset={index + 1}
|
||||
aria-setsize={listLength}
|
||||
style={{ height: `${this.height || cachedHeight}px`, opacity: 0, overflow: 'hidden' }}
|
||||
data-id={id}
|
||||
>
|
||||
{children && cloneElement(children, { hidden: true })}
|
||||
</Article>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Article ref={this.handleRef} aria-posinset={index + 1} aria-setsize={listLength} data-id={id}>
|
||||
{children && cloneElement(children, { hidden: false })}
|
||||
</Article>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in new issue
Block a user