replace waypoint with event handling autoloading
On the dashboard list view, instead of triggering the loading of future items when a waypoint comes into view, make it respond to attempts to scroll into the future, like we do with scrolling into the past. This makes it nicer for screenreader and keyboard only users so they can actually reach the load more button. Note that this change affects the initial load: it won't keep loading until the window is full. There is another ticket to handle this: ADMIN-894 closes ADMIN-892 test plan: * make sure attempts to scroll into the past and future still work via scroll wheel, touch events. * make sure you navigate to the load more button via tab and screenreaders Change-Id: Id0af1682bfec67f29f7e392672ae49812f4ad19a Reviewed-on: https://gerrit.instructure.com/144549 Reviewed-by: Mysti Sadler <mysti@instructure.com> Tested-by: Jenkins QA-Review: Dan Sasaki <dsasaki@instructure.com> Product-Review: Jon Willesen <jonw+gerrit@instructure.com>
This commit is contained in:
parent
028169d345
commit
cbd39bb1f4
|
@ -59,7 +59,6 @@
|
|||
"react-dom": "^0.14.7 || ^15",
|
||||
"react-moment-proptypes": "^1.4.0",
|
||||
"react-redux": "^5.0.3",
|
||||
"react-waypoint": "^7.0.3",
|
||||
"redux": "^3.5.2",
|
||||
"redux-actions": "^2.0.1",
|
||||
"redux-logger": "^3.0.1",
|
||||
|
|
|
@ -34,23 +34,7 @@ it('renders all future items loaded regardless of other props', () => {
|
|||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('invokes the callback when the waypoint is triggered', () => {
|
||||
const mockLoad = jest.fn();
|
||||
const activeFunc = () => {return true;};
|
||||
const wrapper = shallow(<LoadingFutureIndicator onLoadMore={mockLoad} plannerActive={activeFunc} />);
|
||||
wrapper.instance().handleWaypoint();
|
||||
expect(mockLoad).toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
it('does not invoke the callback when the waypoint is triggered, but the planner is not active', () => {
|
||||
const mockLoad = jest.fn();
|
||||
const activeFunc = () => {return false;};
|
||||
const wrapper = shallow(<LoadingFutureIndicator onLoadMore={mockLoad} plannerActive={activeFunc} />);
|
||||
wrapper.instance().handleWaypoint();
|
||||
expect(mockLoad.mock.calls.length).toBe(0);
|
||||
});
|
||||
|
||||
it('invokes the callback when loading more button is clicked', () => {
|
||||
it('invokes the callback when the load more button is clicked', () => {
|
||||
const mockLoad = jest.fn();
|
||||
const wrapper = shallow(<LoadingFutureIndicator onLoadMore={mockLoad} />);
|
||||
wrapper.find('Button').simulate('click');
|
||||
|
|
|
@ -22,15 +22,6 @@ exports[`renders all future items loaded regardless of other props 1`] = `
|
|||
|
||||
exports[`renders load more by default 1`] = `
|
||||
<div>
|
||||
<Waypoint
|
||||
bottomOffset="0px"
|
||||
fireOnRapidScroll={true}
|
||||
horizontal={false}
|
||||
onEnter={[Function]}
|
||||
onLeave={[Function]}
|
||||
onPositionChange={[Function]}
|
||||
topOffset="0px"
|
||||
/>
|
||||
<Container
|
||||
as="div"
|
||||
display={null}
|
||||
|
@ -89,15 +80,6 @@ exports[`renders loading when indicated 1`] = `
|
|||
|
||||
exports[`shows an Alert when there's a query error 1`] = `
|
||||
<div>
|
||||
<Waypoint
|
||||
bottomOffset="0px"
|
||||
fireOnRapidScroll={true}
|
||||
horizontal={false}
|
||||
onEnter={[Function]}
|
||||
onLeave={[Function]}
|
||||
onPositionChange={[Function]}
|
||||
topOffset="0px"
|
||||
/>
|
||||
<Container
|
||||
as="div"
|
||||
display={null}
|
||||
|
|
|
@ -22,7 +22,6 @@ import Container from '@instructure/ui-core/lib/components/Container';
|
|||
import Spinner from '@instructure/ui-core/lib/components/Spinner';
|
||||
import Text from '@instructure/ui-core/lib/components/Text';
|
||||
import ErrorAlert from '../ErrorAlert';
|
||||
import Waypoint from 'react-waypoint';
|
||||
import formatMessage from '../../format-message';
|
||||
|
||||
export default class LoadingFutureIndicator extends Component {
|
||||
|
@ -46,12 +45,6 @@ export default class LoadingFutureIndicator extends Component {
|
|||
this.props.onLoadMore({});
|
||||
}
|
||||
|
||||
handleWaypoint = () => {
|
||||
if (!this.props.loadingError && this.props.plannerActive()) {
|
||||
this.props.onLoadMore();
|
||||
}
|
||||
}
|
||||
|
||||
renderLoadMore () {
|
||||
if (!this.props.loadingFuture && !this.props.allFutureItemsLoaded) {
|
||||
return <Button variant="link" onClick={this.handleLoadMoreButton}>
|
||||
|
@ -93,15 +86,8 @@ export default class LoadingFutureIndicator extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
renderWaypoint () {
|
||||
if (!this.props.loadingFuture && !this.props.allFutureItemsLoaded){
|
||||
return <Waypoint onEnter={this.handleWaypoint} />;
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
return <div>
|
||||
{this.renderWaypoint()}
|
||||
<Container as="div" padding="x-large" textAlign="center">
|
||||
{this.renderError()}
|
||||
{this.renderLoadMore()}
|
||||
|
|
|
@ -16,17 +16,14 @@
|
|||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import changeCase from 'change-case';
|
||||
import {AnimatableRegistry} from './animatable-registry';
|
||||
import {Animator} from './animator';
|
||||
import {AnimationCollection} from './animation-collection';
|
||||
import {isNewActivityItem} from '../utilities/statusUtils';
|
||||
import {daysToItems} from '../utilities/daysUtils';
|
||||
import {srAlert} from '../utilities/alertUtils';
|
||||
import formatMessage from '../format-message';
|
||||
import {setNaiAboveScreen} from '../actions';
|
||||
import {loadPastUntilNewActivity} from '../actions/loading-actions';
|
||||
|
||||
export function specialFallbackFocusId (type) {
|
||||
return `~~~${type}-fallback-focus~~~`;
|
||||
|
|
|
@ -27,7 +27,7 @@ import PlannerHeader from './components/PlannerHeader';
|
|||
import ApplyTheme from '@instructure/ui-core/lib/components/ApplyTheme';
|
||||
import i18n from './i18n';
|
||||
import configureStore from './store/configureStore';
|
||||
import { initialOptions, getPlannerItems, scrollIntoPast } from './actions';
|
||||
import { initialOptions, getPlannerItems, scrollIntoPast, loadFutureItems } from './actions';
|
||||
import { registerScrollEvents } from './utilities/scrollUtils';
|
||||
import { initialize as initializeAlerts } from './utilities/alertUtils';
|
||||
import moment from 'moment-timezone';
|
||||
|
@ -56,6 +56,13 @@ function handleScrollIntoPastAttempt () {
|
|||
}
|
||||
}
|
||||
|
||||
function handleScrollIntoFutureAttempt () {
|
||||
if (!plannerActive()) return;
|
||||
if (!store.getState().loading.loadingPast && !store.getState().loading.loadingFuture && !store.getState().loading.allFutureItemsLoaded) {
|
||||
store.dispatch(loadFutureItems());
|
||||
}
|
||||
}
|
||||
|
||||
export function render (element, options) {
|
||||
// Using this pattern because default params don't merge objects
|
||||
const opts = { ...defaultOptions, ...options };
|
||||
|
@ -63,7 +70,11 @@ export function render (element, options) {
|
|||
moment.locale(opts.locale);
|
||||
moment.tz.setDefault(opts.timeZone);
|
||||
dynamicUiManager.setStickyOffset(opts.stickyOffset);
|
||||
registerScrollEvents(handleScrollIntoPastAttempt, pos => dynamicUiManager.handleScrollPositionChange(pos));
|
||||
registerScrollEvents({
|
||||
scrollIntoPast: handleScrollIntoPastAttempt,
|
||||
scrollIntoFuture: handleScrollIntoFutureAttempt,
|
||||
scrollPositionChange: pos => dynamicUiManager.handleScrollPositionChange(pos),
|
||||
});
|
||||
if (!opts.flashAlertFunctions) {
|
||||
throw new Error('You must provide callbacks to handle flash messages');
|
||||
}
|
||||
|
|
|
@ -18,93 +18,258 @@
|
|||
import {registerScrollEvents} from '../scrollUtils';
|
||||
|
||||
function createMockWindow (opts) {
|
||||
const callbacks = {};
|
||||
return {
|
||||
addEventListener: jest.fn(),
|
||||
addEventListener: jest.fn((event, callback) => callbacks[event] = callback),
|
||||
pageYOffset: 0,
|
||||
setTimeout: jest.fn(),
|
||||
document: {
|
||||
documentElement: {
|
||||
clientHeight: 42,
|
||||
getBoundingClientRect: () => ({bottom: 42}),
|
||||
},
|
||||
},
|
||||
callbacks,
|
||||
...opts,
|
||||
};
|
||||
}
|
||||
|
||||
function mockRegister () {
|
||||
const wind = createMockWindow();
|
||||
const pastCb = jest.fn();
|
||||
const futureCb = jest.fn();
|
||||
const scrollPositionChange = jest.fn();
|
||||
const callbacks = {};
|
||||
|
||||
registerScrollEvents({
|
||||
window: wind,
|
||||
scrollIntoPast: pastCb,
|
||||
scrollIntoFuture: futureCb,
|
||||
scrollPositionChange,
|
||||
callbacks,
|
||||
});
|
||||
return {wind, pastCb, futureCb, scrollPositionChange};
|
||||
}
|
||||
|
||||
it('registers proper events', () => {
|
||||
const mockWindow = createMockWindow();
|
||||
registerScrollEvents(jest.fn, jest.fn(), mockWindow);
|
||||
expect(mockWindow.addEventListener.mock.calls[0][0]).toBe('wheel');
|
||||
expect(mockWindow.addEventListener.mock.calls[1][0]).toBe('keydown');
|
||||
const {wind} = mockRegister();
|
||||
expect(wind.addEventListener).toHaveBeenCalledWith('wheel', expect.anything());
|
||||
expect(wind.addEventListener).toHaveBeenCalledWith('keydown', expect.anything());
|
||||
expect(wind.addEventListener).toHaveBeenCalledWith('touchstart', expect.anything());
|
||||
expect(wind.addEventListener).toHaveBeenCalledWith('touchmove', expect.anything());
|
||||
expect(wind.addEventListener).toHaveBeenCalledWith('touchend', expect.anything());
|
||||
});
|
||||
|
||||
describe('wheel events', () => {
|
||||
it('invokes the callback and preventDefault when a wheel event happens at the top of the page', () => {
|
||||
const mockWindow = createMockWindow();
|
||||
const mockCb = jest.fn();
|
||||
const mockPreventDefault = jest.fn();
|
||||
registerScrollEvents(mockCb, jest.fn(), mockWindow);
|
||||
const wheelHandler = mockWindow.addEventListener.mock.calls[0][1];
|
||||
wheelHandler({deltaY: -42, preventDefault: mockPreventDefault});
|
||||
expect(mockPreventDefault).toHaveBeenCalled();
|
||||
expect(mockCb).toHaveBeenCalled();
|
||||
describe('scrolling into the past', () => {
|
||||
it('invokes the callback and preventDefault when a wheel event happens at the top of the page', () => {
|
||||
const {wind, pastCb} = mockRegister();
|
||||
const mockPreventDefault = jest.fn();
|
||||
const wheelHandler = wind.callbacks.wheel;
|
||||
wheelHandler({deltaY: -42, preventDefault: mockPreventDefault});
|
||||
expect(mockPreventDefault).toHaveBeenCalled();
|
||||
expect(pastCb).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not invoke the callback when the window is not scrolled to the top', () => {
|
||||
const {wind, pastCb} = mockRegister();
|
||||
wind.pageYOffset = 42;
|
||||
const mockPreventDefault = jest.fn();
|
||||
const wheelHandler = wind.callbacks.wheel;
|
||||
wheelHandler({deltaY: -42, preventDefault: mockPreventDefault});
|
||||
expect(mockPreventDefault).not.toHaveBeenCalled();
|
||||
expect(pastCb).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('does not invoke the callback when the window is not scrolled to the top', () => {
|
||||
const mockWindow = createMockWindow({ pageYOffset: 42 });
|
||||
const mockCb = jest.fn();
|
||||
const mockPreventDefault = jest.fn();
|
||||
registerScrollEvents(mockCb, jest.fn(), mockWindow);
|
||||
const wheelHandler = mockWindow.addEventListener.mock.calls[0][1];
|
||||
wheelHandler({deltaY: -42, preventDefault: mockPreventDefault});
|
||||
expect(mockPreventDefault).not.toHaveBeenCalled();
|
||||
expect(mockCb).not.toHaveBeenCalled();
|
||||
describe('scrolling into the future', () => {
|
||||
it('invokes the callback and preventDefault when a wheel event happens at the bottom of the page', () => {
|
||||
const {wind, futureCb} = mockRegister();
|
||||
const mockPreventDefault = jest.fn();
|
||||
const wheelHandler = wind.callbacks.wheel;
|
||||
wheelHandler({deltaY: 42, preventDefault: mockPreventDefault});
|
||||
expect(mockPreventDefault).toHaveBeenCalled();
|
||||
expect(futureCb).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('invokes the callback and preventDefault when the document is shorter than the window', () => {
|
||||
const {wind, futureCb} = mockRegister();
|
||||
wind.document.documentElement.getBoundingClientRect = () => ({bottom: 15});
|
||||
const mockPreventDefault = jest.fn();
|
||||
const wheelHandler = wind.callbacks.wheel;
|
||||
wheelHandler({deltaY: 42, preventDefault: mockPreventDefault});
|
||||
expect(mockPreventDefault).toHaveBeenCalled();
|
||||
expect(futureCb).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not invoke the callback when the window is not scrolled to the bottom', () => {
|
||||
const {wind, futureCb} = mockRegister();
|
||||
wind.document.documentElement.getBoundingClientRect = () => ({bottom: 100});
|
||||
const mockPreventDefault = jest.fn();
|
||||
const wheelHandler = wind.callbacks.wheel;
|
||||
wheelHandler({deltaY: 42, preventDefault: mockPreventDefault});
|
||||
expect(mockPreventDefault).not.toHaveBeenCalled();
|
||||
expect(futureCb).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('touch events', () => {
|
||||
let mockWindow = null;
|
||||
afterEach(() => {
|
||||
// need to reset global touch state after each test
|
||||
mockWindow.callbacks.touchend({});
|
||||
});
|
||||
|
||||
describe('scrolling into the past', () => {
|
||||
it('invokes the callback and preventDefault when touch events happen at the top of the page', () => {
|
||||
const {wind, pastCb} = mockRegister();
|
||||
mockWindow = wind;
|
||||
const {touchstart, touchmove} = wind.callbacks;
|
||||
touchstart({changedTouches: [{screenY: 10, identifier: 'touchid'}]});
|
||||
touchmove({changedTouches: {touchid: {screenY: 14}}});
|
||||
expect(pastCb).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not invoke the callback when the window is not scrolled to the top', () => {
|
||||
const {wind, pastCb} = mockRegister();
|
||||
mockWindow = wind;
|
||||
wind.pageYOffset = 42;
|
||||
const {touchstart, touchmove} = wind.callbacks;
|
||||
touchstart({changedTouches: [{screenY: 10, identifier: 'touchid'}]});
|
||||
touchmove({changedTouches: {touchid: {screenY: 14}}});
|
||||
expect(pastCb).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not invoke the callback when the scroll is not large enough', () => {
|
||||
const {wind, pastCb} = mockRegister();
|
||||
mockWindow = wind;
|
||||
wind.pageYOffset = 42;
|
||||
const {touchstart, touchmove} = wind.callbacks;
|
||||
touchstart({changedTouches: [{screenY: 10, identifier: 'touchid'}]});
|
||||
touchmove({changedTouches: {touchid: {screenY: 13}}});
|
||||
expect(pastCb).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('scrolling into the future', () => {
|
||||
it('invokes the callback and preventDefault when touch events happen at the bottom of the page', () => {
|
||||
const {wind, futureCb} = mockRegister();
|
||||
mockWindow = wind;
|
||||
const {touchstart, touchmove} = wind.callbacks;
|
||||
touchstart({changedTouches: [{screenY: 10, identifier: 'touchid'}]});
|
||||
touchmove({changedTouches: {touchid: {screenY: 6}}});
|
||||
expect(futureCb).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not invoke the callback when the window is not scrolled to the bottom', () => {
|
||||
const {wind, futureCb} = mockRegister();
|
||||
mockWindow = wind;
|
||||
const {touchstart, touchmove} = wind.callbacks;
|
||||
wind.document.documentElement.getBoundingClientRect = () => ({bottom: 84});
|
||||
touchstart({changedTouches: [{screenY: 10, identifier: 'touchid'}]});
|
||||
touchmove({changedTouches: {touchid: {screenY: 6}}});
|
||||
expect(futureCb).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not invoke the callback when the scroll is not large enough', () => {
|
||||
const {wind, futureCb} = mockRegister();
|
||||
mockWindow = wind;
|
||||
const {touchstart, touchmove} = wind.callbacks;
|
||||
touchstart({changedTouches: [{screenY: 10, identifier: 'touchid'}]});
|
||||
touchmove({changedTouches: {touchid: {screenY: 7}}});
|
||||
expect(futureCb).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('key events', () => {
|
||||
it('invokes the callback and preventDefault when a wheel event happens at the top of the page', () => {
|
||||
const mockWindow = createMockWindow();
|
||||
const mockCb = jest.fn();
|
||||
const mockPreventDefault = jest.fn();
|
||||
registerScrollEvents(mockCb, jest.fn(), mockWindow);
|
||||
const keyHandler = mockWindow.addEventListener.mock.calls[1][1];
|
||||
keyHandler({key: 'ArrowUp', preventDefault: mockPreventDefault});
|
||||
expect(mockPreventDefault).toHaveBeenCalled();
|
||||
expect(mockCb).toHaveBeenCalled();
|
||||
describe('scrolling into the past', () => {
|
||||
it('invokes the callback and preventDefault when a key event happens at the top of the page', () => {
|
||||
const {wind, pastCb} = mockRegister();
|
||||
const mockPreventDefault = jest.fn();
|
||||
const keyHandler = wind.callbacks.keydown;
|
||||
keyHandler({key: 'ArrowUp', preventDefault: mockPreventDefault});
|
||||
expect(mockPreventDefault).toHaveBeenCalled();
|
||||
expect(pastCb).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not invoke the past callback or preventDefault on other keys', () => {
|
||||
const {wind, pastCb} = mockRegister();
|
||||
const mockPreventDefault = jest.fn();
|
||||
const keyHandler = wind.callbacks.keydown;
|
||||
keyHandler({key: 'Home', preventDefault: mockPreventDefault});
|
||||
expect(mockPreventDefault).not.toHaveBeenCalled();
|
||||
expect(pastCb).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not invoke the past callback if window is not at the top', () => {
|
||||
const {wind, pastCb} = mockRegister();
|
||||
wind.pageYOffset = 42;
|
||||
const mockPreventDefault = jest.fn();
|
||||
const keyHandler = wind.callbacks.keydown;
|
||||
keyHandler({key: 'ArrowUp', preventDefault: mockPreventDefault});
|
||||
expect(mockPreventDefault).not.toHaveBeenCalled();
|
||||
expect(pastCb).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('does not invoke the callback or preventDefault on other keys', () => {
|
||||
const mockWindow = createMockWindow();
|
||||
const mockCb = jest.fn();
|
||||
const mockPreventDefault = jest.fn();
|
||||
registerScrollEvents(mockCb, jest.fn(), mockWindow);
|
||||
const keyHandler = mockWindow.addEventListener.mock.calls[1][1];
|
||||
keyHandler({key: 'Home', preventDefault: mockPreventDefault});
|
||||
expect(mockPreventDefault).not.toHaveBeenCalled();
|
||||
expect(mockCb).not.toHaveBeenCalled();
|
||||
});
|
||||
describe('scrolling into the future', () => {
|
||||
it('invokes the future callback and preventDefault when a key event happens at the bottom of the page', () => {
|
||||
const {wind, futureCb} = mockRegister();
|
||||
const mockPreventDefault = jest.fn();
|
||||
const keyHandler = wind.callbacks.keydown;
|
||||
keyHandler({key: 'ArrowDown', preventDefault: mockPreventDefault});
|
||||
expect(mockPreventDefault).toHaveBeenCalled();
|
||||
expect(futureCb).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not invoke the callback if window is not at the top', () => {
|
||||
const mockWindow = createMockWindow({ pageYOffset: 42 });
|
||||
const mockCb = jest.fn();
|
||||
const mockPreventDefault = jest.fn();
|
||||
registerScrollEvents(mockCb, jest.fn(), mockWindow);
|
||||
const keyHandler = mockWindow.addEventListener.mock.calls[1][1];
|
||||
keyHandler({key: 'ArrowUp', preventDefault: mockPreventDefault});
|
||||
expect(mockPreventDefault).not.toHaveBeenCalled();
|
||||
expect(mockCb).not.toHaveBeenCalled();
|
||||
it('invokes the future callback and preventDefault when a key event happens when the document is shorter than the window', () => {
|
||||
const {wind, futureCb} = mockRegister();
|
||||
wind.document.documentElement.getBoundingClientRect = () => ({bottom: 24});
|
||||
const mockPreventDefault = jest.fn();
|
||||
const keyHandler = wind.callbacks.keydown;
|
||||
keyHandler({key: 'ArrowDown', preventDefault: mockPreventDefault});
|
||||
expect(mockPreventDefault).toHaveBeenCalled();
|
||||
expect(futureCb).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not invoke the future callback or preventDefault on other keys', () => {
|
||||
const {wind, futureCb} = mockRegister();
|
||||
const mockPreventDefault = jest.fn();
|
||||
const keyHandler = wind.callbacks.keydown;
|
||||
keyHandler({key: 'End', preventDefault: mockPreventDefault});
|
||||
expect(mockPreventDefault).not.toHaveBeenCalled();
|
||||
expect(futureCb).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not invoke the callback if window is not at the bottom', () => {
|
||||
const {wind, futureCb} = mockRegister();
|
||||
wind.document.documentElement.getBoundingClientRect = () => ({bottom: 100});
|
||||
const mockPreventDefault = jest.fn();
|
||||
const keyHandler = wind.callbacks.keydown;
|
||||
keyHandler({key: 'ArrowDown', preventDefault: mockPreventDefault});
|
||||
expect(mockPreventDefault).not.toHaveBeenCalled();
|
||||
expect(futureCb).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('scroll events', () => {
|
||||
it('throttles the callback', () => {
|
||||
const mockWindow = createMockWindow();
|
||||
mockWindow.addEventListener = (event, cb) => {
|
||||
if (event === 'scroll') { mockWindow.registeredScrollHandler = cb; }
|
||||
};
|
||||
const mockScrollCb = jest.fn();
|
||||
registerScrollEvents(jest.fn(), mockScrollCb, mockWindow);
|
||||
registerScrollEvents({
|
||||
window: mockWindow,
|
||||
scrollIntoPast: jest.fn(),
|
||||
scrollIntoFuture: jest.fn(),
|
||||
scrollPositionChange: mockScrollCb
|
||||
});
|
||||
|
||||
mockWindow.pageYOffset = 42;
|
||||
mockWindow.registeredScrollHandler();
|
||||
mockWindow.callbacks.scroll();
|
||||
mockWindow.pageYOffset = 84;
|
||||
mockWindow.registeredScrollHandler();
|
||||
mockWindow.callbacks.scroll();
|
||||
|
||||
const setTimeoutMock = mockWindow.setTimeout;
|
||||
expect(setTimeoutMock).toHaveBeenCalledTimes(1);
|
||||
|
|
|
@ -15,46 +15,74 @@
|
|||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import Velocity from 'velocity-animate';
|
||||
|
||||
export function animateSlideDown (elt) {
|
||||
Velocity(elt, 'slideDown');
|
||||
function isScrollPositionAtTop (wind) {
|
||||
return wind.pageYOffset === 0;
|
||||
}
|
||||
|
||||
function handleScrollUpAttempt (cb, e) {
|
||||
function isScrollPositionAtBottom (wind) {
|
||||
const doc = wind.document.documentElement;
|
||||
const docBottom = doc.getBoundingClientRect().bottom;
|
||||
const clientHeight = doc.clientHeight;
|
||||
|
||||
return docBottom <= clientHeight;
|
||||
}
|
||||
|
||||
function isScrollUpKey (key) {
|
||||
return key === 'PageUp' || key === 'ArrowUp' || key === 'Up';
|
||||
}
|
||||
|
||||
function isScrollDownKey(key) {
|
||||
return key === 'PageDown' || key === 'ArrowDown' || key === 'Down';
|
||||
}
|
||||
|
||||
function isWheelUpEvent (e) {
|
||||
return e.deltaY < 0;
|
||||
}
|
||||
|
||||
function isWheelDownEvent (e) {
|
||||
return e.deltaY > 0;
|
||||
}
|
||||
|
||||
function handleScrollAttempt (cb, e) {
|
||||
e.preventDefault();
|
||||
cb();
|
||||
}
|
||||
|
||||
function handleWindowWheel (cb, wind, e) {
|
||||
if (wind.pageYOffset === 0 && e.deltaY < 0) {
|
||||
handleScrollUpAttempt(cb, e);
|
||||
function handleWindowWheel (pastCb, futureCb, wind, e) {
|
||||
if (isScrollPositionAtTop(wind) && isWheelUpEvent(e)) {
|
||||
handleScrollAttempt(pastCb, e);
|
||||
} else if (isScrollPositionAtBottom(wind) && isWheelDownEvent(e)) {
|
||||
handleScrollAttempt(futureCb, e);
|
||||
}
|
||||
}
|
||||
|
||||
function handleWindowScrollKey (cb, wind, e) {
|
||||
if (wind.pageYOffset === 0 &&
|
||||
(e.key === 'PageUp' || e.key === 'ArrowUp' || e.key === 'Up')) {
|
||||
handleScrollUpAttempt(cb, e);
|
||||
function handleWindowScrollKey (pastCb, futureCb, wind, e) {
|
||||
if (isScrollPositionAtTop(wind) && isScrollUpKey(e.key)) {
|
||||
handleScrollAttempt(pastCb, e);
|
||||
} else if (isScrollPositionAtBottom(wind) && isScrollDownKey(e.key)) {
|
||||
handleScrollAttempt(futureCb, e);
|
||||
}
|
||||
}
|
||||
|
||||
// User drags a finger down the screen to scroll up.
|
||||
// When she gets to the top, and keeps on pulling down, call the callback
|
||||
// and vice versa
|
||||
let ongoingTouch = null;
|
||||
function handleTouchStart (e) {
|
||||
if (ongoingTouch === null) {
|
||||
ongoingTouch = e.changedTouches[0];
|
||||
}
|
||||
}
|
||||
function handleWindowTouchMove (cb, wind, e) {
|
||||
if (wind.pageYOffset === 0 && ongoingTouch) {
|
||||
const thisTouch = e.changedTouches[ongoingTouch.identifier];
|
||||
if (thisTouch) {
|
||||
if (thisTouch.screenY - ongoingTouch.screenY > 3) {
|
||||
cb();
|
||||
}
|
||||
}
|
||||
|
||||
function handleWindowTouchMove (pastCb, futureCb, wind, e) {
|
||||
if (!ongoingTouch) return;
|
||||
const thisTouch = e.changedTouches[ongoingTouch.identifier];
|
||||
if (!thisTouch) return;
|
||||
if (isScrollPositionAtTop(wind) && thisTouch.screenY - ongoingTouch.screenY > 3) {
|
||||
pastCb();
|
||||
} else if (isScrollPositionAtBottom(wind) && thisTouch.screenY - ongoingTouch.screenY < -3) {
|
||||
futureCb();
|
||||
}
|
||||
}
|
||||
function handleTouchEnd (e) {
|
||||
|
@ -88,16 +116,21 @@ class ScrollHandler {
|
|||
}
|
||||
}
|
||||
|
||||
export function registerScrollEvents (scrollIntoPastCb, scrollCb, wind = window) {
|
||||
const boundWindowWheel = handleWindowWheel.bind(undefined, scrollIntoPastCb, wind);
|
||||
export function registerScrollEvents ({
|
||||
scrollIntoPast: scrollIntoPastCb,
|
||||
scrollIntoFuture: scrollIntoFutureCb,
|
||||
scrollPositionChange: scrollCb,
|
||||
window: wind}) {
|
||||
wind = wind || window;
|
||||
const boundWindowWheel = handleWindowWheel.bind(undefined, scrollIntoPastCb, scrollIntoFutureCb, wind);
|
||||
wind.addEventListener('wheel', boundWindowWheel);
|
||||
|
||||
const boundScrollKey = handleWindowScrollKey.bind(undefined, scrollIntoPastCb, wind);
|
||||
const boundScrollKey = handleWindowScrollKey.bind(undefined, scrollIntoPastCb, scrollIntoFutureCb, wind);
|
||||
wind.addEventListener('keydown', boundScrollKey);
|
||||
|
||||
wind.addEventListener('touchstart', handleTouchStart);
|
||||
wind.addEventListener('touchend', handleTouchEnd);
|
||||
const boundTouchMove = handleWindowTouchMove.bind(undefined, scrollIntoPastCb, wind);
|
||||
const boundTouchMove = handleWindowTouchMove.bind(undefined, scrollIntoPastCb, scrollIntoFutureCb, wind);
|
||||
wind.addEventListener('touchmove', boundTouchMove);
|
||||
|
||||
new ScrollHandler(scrollCb, wind);
|
||||
|
|
|
@ -2279,10 +2279,6 @@ console-control-strings@^1.0.0, console-control-strings@~1.1.0:
|
|||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
|
||||
|
||||
consolidated-events@^1.1.0:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/consolidated-events/-/consolidated-events-1.1.1.tgz#25395465b35e531395418b7bbecb5ecaf198d179"
|
||||
|
||||
constant-case@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/constant-case/-/constant-case-2.0.0.tgz#4175764d389d3fa9c8ecd29186ed6005243b6a46"
|
||||
|
@ -7613,7 +7609,7 @@ promise@^7.1.1:
|
|||
dependencies:
|
||||
asap "~2.0.3"
|
||||
|
||||
prop-types@^15.0.0, prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.5.9, prop-types@^15.6.0:
|
||||
prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.5.9, prop-types@^15.6.0:
|
||||
version "15.6.0"
|
||||
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.0.tgz#ceaf083022fc46b4a35f69e13ef75aed0d639856"
|
||||
dependencies:
|
||||
|
@ -7818,13 +7814,6 @@ react-test-renderer@15.6.1:
|
|||
fbjs "^0.8.9"
|
||||
object-assign "^4.1.0"
|
||||
|
||||
react-waypoint@^7.0.3:
|
||||
version "7.3.4"
|
||||
resolved "https://registry.yarnpkg.com/react-waypoint/-/react-waypoint-7.3.4.tgz#6f4a167ca71c0877576699d6980089f001137f90"
|
||||
dependencies:
|
||||
consolidated-events "^1.1.0"
|
||||
prop-types "^15.0.0"
|
||||
|
||||
"react@^0.14.7 || ^15":
|
||||
version "15.6.2"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-15.6.2.tgz#dba0434ab439cfe82f108f0f511663908179aa72"
|
||||
|
|
|
@ -500,6 +500,7 @@ describe "student planner" do
|
|||
go_to_list_view
|
||||
current_items = items_displayed.count
|
||||
driver.execute_script("window.scrollTo(0, document.documentElement.scrollHeight);")
|
||||
f('body').send_keys(:arrow_down)
|
||||
wait_for_spinner
|
||||
expect(items_displayed.count).to be > current_items
|
||||
end
|
||||
|
|
14
yarn.lock
14
yarn.lock
|
@ -2004,7 +2004,6 @@ caniuse-lite@^1.0.30000792, caniuse-lite@^1.0.30000805, caniuse-lite@^1.0.300008
|
|||
react-dom "^0.14.7 || ^15"
|
||||
react-moment-proptypes "^1.4.0"
|
||||
react-redux "^5.0.3"
|
||||
react-waypoint "^7.0.3"
|
||||
redux "^3.5.2"
|
||||
redux-actions "^2.0.1"
|
||||
redux-logger "^3.0.1"
|
||||
|
@ -2570,10 +2569,6 @@ console-control-strings@^1.0.0, console-control-strings@~1.1.0:
|
|||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
|
||||
|
||||
consolidated-events@^1.1.0:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/consolidated-events/-/consolidated-events-1.1.1.tgz#25395465b35e531395418b7bbecb5ecaf198d179"
|
||||
|
||||
constant-case@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/constant-case/-/constant-case-2.0.0.tgz#4175764d389d3fa9c8ecd29186ed6005243b6a46"
|
||||
|
@ -8531,7 +8526,7 @@ promise@^7.0.3, promise@^7.1.1:
|
|||
dependencies:
|
||||
asap "~2.0.3"
|
||||
|
||||
prop-types@^15, prop-types@^15.0.0, prop-types@^15.5.10, prop-types@^15.5.6, prop-types@^15.5.8, prop-types@^15.5.9, prop-types@^15.6.0:
|
||||
prop-types@^15, prop-types@^15.5.10, prop-types@^15.5.6, prop-types@^15.5.8, prop-types@^15.5.9, prop-types@^15.6.0:
|
||||
version "15.6.1"
|
||||
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.1.tgz#36644453564255ddda391191fb3a125cbdf654ca"
|
||||
dependencies:
|
||||
|
@ -8833,13 +8828,6 @@ react-transition-group@1.1.3:
|
|||
prop-types "^15.5.6"
|
||||
warning "^3.0.0"
|
||||
|
||||
react-waypoint@^7.0.3:
|
||||
version "7.3.4"
|
||||
resolved "https://registry.yarnpkg.com/react-waypoint/-/react-waypoint-7.3.4.tgz#6f4a167ca71c0877576699d6980089f001137f90"
|
||||
dependencies:
|
||||
consolidated-events "^1.1.0"
|
||||
prop-types "^15.0.0"
|
||||
|
||||
react@0.14.9, "react@^0.14.7 || ^15", "react@^0.14.8 || ^15.0.0":
|
||||
version "0.14.9"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-0.14.9.tgz#9110a6497c49d44ba1c0edd317aec29c2e0d91d1"
|
||||
|
|
Loading…
Reference in New Issue