@shopify/polaris
Version:
Shopify’s product component library
164 lines (163 loc) • 6.34 kB
JavaScript
import * as tslib_1 from "tslib";
import React from 'react';
import debounce from 'lodash/debounce';
import { addEventListener, removeEventListener, } from '@shopify/javascript-utilities/events';
import { closest } from '@shopify/javascript-utilities/dom';
import { classNames } from '../../utilities/css';
import { StickyManager, StickyManagerContext, } from '../../utilities/sticky-manager';
import { scrollable } from '../shared';
import { ScrollTo } from './components';
import { ScrollableContext } from './context';
import styles from './Scrollable.scss';
const MAX_SCROLL_DISTANCE = 100;
const DELTA_THRESHOLD = 0.2;
const DELTA_PERCENTAGE = 0.2;
const EVENTS_TO_LOCK = ['scroll', 'touchmove', 'wheel'];
const PREFERS_REDUCED_MOTION = prefersReducedMotion();
export class Scrollable extends React.Component {
constructor() {
super(...arguments);
this.state = {
topShadow: false,
bottomShadow: false,
scrollPosition: 0,
};
this.stickyManager = new StickyManager();
this.handleResize = debounce(() => {
this.handleScroll();
}, 50, { trailing: true });
this.setScrollArea = (scrollArea) => {
this.scrollArea = scrollArea;
};
this.handleScroll = () => {
const { scrollArea } = this;
const { shadow, onScrolledToBottom } = this.props;
if (scrollArea == null) {
return;
}
const { scrollTop, clientHeight, scrollHeight } = scrollArea;
const shouldBottomShadow = Boolean(shadow && !(scrollTop + clientHeight >= scrollHeight));
const shouldTopShadow = Boolean(shadow && scrollTop > 0);
const canScroll = scrollHeight > clientHeight;
const hasScrolledToBottom = scrollHeight - scrollTop === clientHeight;
if (canScroll && hasScrolledToBottom && onScrolledToBottom) {
onScrolledToBottom();
}
this.setState({
topShadow: shouldTopShadow,
bottomShadow: shouldBottomShadow,
scrollPosition: scrollTop,
});
};
this.scrollHint = () => {
const { scrollArea } = this;
if (scrollArea == null) {
return;
}
const { clientHeight, scrollHeight } = scrollArea;
if (PREFERS_REDUCED_MOTION ||
this.state.scrollPosition > 0 ||
scrollHeight <= clientHeight) {
return;
}
const scrollDistance = scrollHeight - clientHeight;
this.toggleLock();
this.setState({
scrollPosition: scrollDistance > MAX_SCROLL_DISTANCE
? MAX_SCROLL_DISTANCE
: scrollDistance,
}, () => {
window.requestAnimationFrame(this.scrollStep);
});
};
this.scrollStep = () => {
this.setState(({ scrollPosition }) => {
const delta = scrollPosition * DELTA_PERCENTAGE;
return {
scrollPosition: delta < DELTA_THRESHOLD ? 0 : scrollPosition - delta,
};
}, () => {
if (this.state.scrollPosition > 0) {
window.requestAnimationFrame(this.scrollStep);
}
else {
this.toggleLock(false);
}
});
};
this.scrollToPosition = (scrollY) => {
this.setState({ scrollPosition: scrollY });
};
}
static forNode(node) {
return (closest(node, scrollable.selector) || document);
}
componentDidMount() {
if (this.scrollArea == null) {
return;
}
this.stickyManager.setContainer(this.scrollArea);
addEventListener(this.scrollArea, 'scroll', () => {
window.requestAnimationFrame(this.handleScroll);
});
addEventListener(window, 'resize', this.handleResize);
window.requestAnimationFrame(() => {
this.handleScroll();
if (this.props.hint) {
this.scrollHint();
}
});
}
componentWillUnmount() {
if (this.scrollArea == null) {
return;
}
removeEventListener(this.scrollArea, 'scroll', this.handleScroll);
removeEventListener(window, 'resize', this.handleResize);
this.stickyManager.removeScrollListener();
}
componentDidUpdate() {
const { scrollPosition } = this.state;
if (scrollPosition && this.scrollArea && scrollPosition > 0) {
this.scrollArea.scrollTop = scrollPosition;
}
}
render() {
const { topShadow, bottomShadow } = this.state;
const _a = this.props, { children, className, horizontal, vertical = true, shadow, hint, onScrolledToBottom } = _a, rest = tslib_1.__rest(_a, ["children", "className", "horizontal", "vertical", "shadow", "hint", "onScrolledToBottom"]);
const finalClassName = classNames(className, styles.Scrollable, vertical && styles.vertical, horizontal && styles.horizontal, topShadow && styles.hasTopShadow, bottomShadow && styles.hasBottomShadow);
return (<ScrollableContext.Provider value={this.scrollToPosition}>
<StickyManagerContext.Provider value={this.stickyManager}>
<div className={finalClassName} {...scrollable.props} {...rest} ref={this.setScrollArea}>
{children}
</div>
</StickyManagerContext.Provider>
</ScrollableContext.Provider>);
}
toggleLock(shouldLock = true) {
const { scrollArea } = this;
if (scrollArea == null) {
return;
}
EVENTS_TO_LOCK.forEach((eventName) => {
if (shouldLock) {
addEventListener(scrollArea, eventName, prevent);
}
else {
removeEventListener(scrollArea, eventName, prevent);
}
});
}
}
Scrollable.ScrollTo = ScrollTo;
function prevent(evt) {
evt.preventDefault();
}
function prefersReducedMotion() {
try {
return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
}
catch (err) {
return false;
}
}